mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 11:03:05 +00:00
Merge branch 'master' into Move-item-component
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
theme: {
|
||||||
|
name: 'default',
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@@ -2,16 +2,47 @@
|
|||||||
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
||||||
"404.link.home-page": "Take me to the home page",
|
"404.link.home-page": "Take me to the home page",
|
||||||
"404.page-not-found": "page not found",
|
"404.page-not-found": "page not found",
|
||||||
|
"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.head": "Create Bitstream format",
|
||||||
|
"admin.registries.bitstream-formats.create.new": "Add a new bitstream format",
|
||||||
|
"admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.",
|
||||||
|
"admin.registries.bitstream-formats.create.success.head": "Success",
|
||||||
|
"admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)",
|
||||||
|
"admin.registries.bitstream-formats.delete.failure.head": "Failure",
|
||||||
|
"admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)",
|
||||||
|
"admin.registries.bitstream-formats.delete.success.head": "Success",
|
||||||
"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.formats.no-items": "No bitstream formats to show.",
|
"admin.registries.bitstream-formats.edit.description.hint": "",
|
||||||
"admin.registries.bitstream-formats.formats.table.internal": "internal",
|
"admin.registries.bitstream-formats.edit.description.label": "Description",
|
||||||
"admin.registries.bitstream-formats.formats.table.mimetype": "MIME Type",
|
"admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.",
|
||||||
"admin.registries.bitstream-formats.formats.table.name": "Name",
|
"admin.registries.bitstream-formats.edit.extensions.label": "File extensions",
|
||||||
"admin.registries.bitstream-formats.formats.table.supportLevel.0": "Unknown",
|
"admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot",
|
||||||
"admin.registries.bitstream-formats.formats.table.supportLevel.1": "Known",
|
"admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.",
|
||||||
"admin.registries.bitstream-formats.formats.table.supportLevel.2": "Support",
|
"admin.registries.bitstream-formats.edit.failure.head": "Failure",
|
||||||
"admin.registries.bitstream-formats.formats.table.supportLevel.head": "Support Level",
|
"admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}",
|
||||||
|
"admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.",
|
||||||
|
"admin.registries.bitstream-formats.edit.internal.label": "Internal",
|
||||||
|
"admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.",
|
||||||
|
"admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type",
|
||||||
|
"admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)",
|
||||||
|
"admin.registries.bitstream-formats.edit.shortDescription.label": "Name",
|
||||||
|
"admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.",
|
||||||
|
"admin.registries.bitstream-formats.edit.success.head": "Success",
|
||||||
|
"admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.",
|
||||||
|
"admin.registries.bitstream-formats.edit.supportLevel.label": "Support level",
|
||||||
"admin.registries.bitstream-formats.head": "Bitstream Format Registry",
|
"admin.registries.bitstream-formats.head": "Bitstream Format Registry",
|
||||||
|
"admin.registries.bitstream-formats.no-items": "No bitstream formats to show.",
|
||||||
|
"admin.registries.bitstream-formats.table.delete": "Delete selected",
|
||||||
|
"admin.registries.bitstream-formats.table.deselect-all": "Deselect all",
|
||||||
|
"admin.registries.bitstream-formats.table.internal": "internal",
|
||||||
|
"admin.registries.bitstream-formats.table.mimetype": "MIME Type",
|
||||||
|
"admin.registries.bitstream-formats.table.name": "Name",
|
||||||
|
"admin.registries.bitstream-formats.table.return": "Return",
|
||||||
|
"admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known",
|
||||||
|
"admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported",
|
||||||
|
"admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown",
|
||||||
|
"admin.registries.bitstream-formats.table.supportLevel.head": "Support Level",
|
||||||
"admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry",
|
"admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format 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",
|
||||||
@@ -101,6 +132,7 @@
|
|||||||
"collection.form.tableofcontents": "News (HTML)",
|
"collection.form.tableofcontents": "News (HTML)",
|
||||||
"collection.form.title": "Name",
|
"collection.form.title": "Name",
|
||||||
"collection.page.browse.recent.head": "Recent Submissions",
|
"collection.page.browse.recent.head": "Recent Submissions",
|
||||||
|
"collection.page.browse.recent.empty": "No items to show",
|
||||||
"collection.page.license": "License",
|
"collection.page.license": "License",
|
||||||
"collection.page.news": "News",
|
"collection.page.news": "News",
|
||||||
"community.create.head": "Create a Community",
|
"community.create.head": "Create a Community",
|
||||||
@@ -235,12 +267,26 @@
|
|||||||
"item.edit.reinstate.error": "An error occurred while reinstating the item",
|
"item.edit.reinstate.error": "An error occurred while reinstating the item",
|
||||||
"item.edit.reinstate.header": "Reinstate item: {{ id }}",
|
"item.edit.reinstate.header": "Reinstate item: {{ id }}",
|
||||||
"item.edit.reinstate.success": "The item was reinstated successfully",
|
"item.edit.reinstate.success": "The item was reinstated successfully",
|
||||||
|
"item.edit.relationships.discard-button": "Discard",
|
||||||
|
"item.edit.relationships.edit.buttons.remove": "Remove",
|
||||||
|
"item.edit.relationships.edit.buttons.undo": "Undo changes",
|
||||||
|
"item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
|
||||||
|
"item.edit.relationships.notifications.discarded.title": "Changes discarded",
|
||||||
|
"item.edit.relationships.notifications.failed.title": "Error deleting relationship",
|
||||||
|
"item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts",
|
||||||
|
"item.edit.relationships.notifications.outdated.title": "Changes outdated",
|
||||||
|
"item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.",
|
||||||
|
"item.edit.relationships.notifications.saved.title": "Relationships saved",
|
||||||
|
"item.edit.relationships.reinstate-button": "Undo",
|
||||||
|
"item.edit.relationships.save-button": "Save",
|
||||||
"item.edit.tabs.bitstreams.head": "Item Bitstreams",
|
"item.edit.tabs.bitstreams.head": "Item Bitstreams",
|
||||||
"item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams",
|
"item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams",
|
||||||
"item.edit.tabs.curate.head": "Curate",
|
"item.edit.tabs.curate.head": "Curate",
|
||||||
"item.edit.tabs.curate.title": "Item Edit - Curate",
|
"item.edit.tabs.curate.title": "Item Edit - Curate",
|
||||||
"item.edit.tabs.metadata.head": "Item Metadata",
|
"item.edit.tabs.metadata.head": "Item Metadata",
|
||||||
"item.edit.tabs.metadata.title": "Item Edit - Metadata",
|
"item.edit.tabs.metadata.title": "Item Edit - Metadata",
|
||||||
|
"item.edit.tabs.relationships.head": "Item Relationships",
|
||||||
|
"item.edit.tabs.relationships.title": "Item Edit - Relationships",
|
||||||
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
|
"item.edit.tabs.status.buttons.authorizations.button": "Authorizations...",
|
||||||
"item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies",
|
"item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies",
|
||||||
"item.edit.tabs.status.buttons.delete.button": "Permanently delete",
|
"item.edit.tabs.status.buttons.delete.button": "Permanently delete",
|
||||||
|
@@ -2,14 +2,29 @@ import { MetadataRegistryComponent } from './metadata-registry/metadata-registry
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
|
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||||
|
|
||||||
|
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||||
|
|
||||||
|
export function getBitstreamFormatsModulePath() {
|
||||||
|
return new URLCombiner(getRegistriesModulePath(), BITSTREAMFORMATS_MODULE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: 'metadata', component: MetadataRegistryComponent, data: { title: 'admin.registries.metadata.title' } },
|
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
||||||
{ path: 'metadata/:schemaName', component: MetadataSchemaComponent, data: { title: 'admin.registries.schema.title' } },
|
{
|
||||||
{ path: 'bitstream-formats', component: BitstreamFormatsComponent, data: { title: 'admin.registries.bitstream-formats.title' } },
|
path: 'metadata/:schemaName',
|
||||||
|
component: MetadataSchemaComponent,
|
||||||
|
data: {title: 'admin.registries.schema.title'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||||
|
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||||
|
data: {title: 'admin.registries.bitstream-formats.title'}
|
||||||
|
},
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -5,10 +5,10 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
||||||
import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
import { MetadataFieldFormComponent } from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||||
|
import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,12 +16,12 @@ import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
|
BitstreamFormatsModule,
|
||||||
AdminRegistriesRoutingModule
|
AdminRegistriesRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MetadataRegistryComponent,
|
MetadataRegistryComponent,
|
||||||
MetadataSchemaComponent,
|
MetadataSchemaComponent,
|
||||||
BitstreamFormatsComponent,
|
|
||||||
MetadataSchemaFormComponent,
|
MetadataSchemaFormComponent,
|
||||||
MetadataFieldFormComponent
|
MetadataFieldFormComponent
|
||||||
],
|
],
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h2 id="sub-header"
|
||||||
|
class="border-bottom mb-2">{{ 'admin.registries.bitstream-formats.create.new' | translate }}</h2>
|
||||||
|
|
||||||
|
<ds-bitstream-format-form (updatedFormat)="createBitstreamFormat($event)"></ds-bitstream-format-form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,106 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||||
|
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||||
|
import { ResourceType } from '../../../../core/shared/resource-type';
|
||||||
|
import { AddBitstreamFormatComponent } from './add-bitstream-format.component';
|
||||||
|
|
||||||
|
describe('AddBitstreamFormatComponent', () => {
|
||||||
|
let comp: AddBitstreamFormatComponent;
|
||||||
|
let fixture: ComponentFixture<AddBitstreamFormatComponent>;
|
||||||
|
|
||||||
|
const bitstreamFormat = new BitstreamFormat();
|
||||||
|
bitstreamFormat.uuid = 'test-uuid-1';
|
||||||
|
bitstreamFormat.id = 'test-uuid-1';
|
||||||
|
bitstreamFormat.shortDescription = 'Unknown';
|
||||||
|
bitstreamFormat.description = 'Unknown data format';
|
||||||
|
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||||
|
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||||
|
bitstreamFormat.internal = false;
|
||||||
|
bitstreamFormat.extensions = null;
|
||||||
|
|
||||||
|
let router;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let bitstreamFormatDataService: BitstreamFormatDataService;
|
||||||
|
|
||||||
|
const initAsync = () => {
|
||||||
|
router = new RouterStub();
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||||
|
createBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')),
|
||||||
|
clearBitStreamFormatRequests: observableOf(null)
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [AddBitstreamFormatComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: NotificationsService, useValue: notificationService},
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initBeforeEach = () => {
|
||||||
|
fixture = TestBed.createComponent(AddBitstreamFormatComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('createBitstreamFormat success', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||||
|
comp.createBitstreamFormat(bitstreamFormat);
|
||||||
|
|
||||||
|
expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('createBitstreamFormat error', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
router = new RouterStub();
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||||
|
createBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')),
|
||||||
|
clearBitStreamFormatRequests: observableOf(null)
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [AddBitstreamFormatComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: NotificationsService, useValue: notificationService},
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||||
|
comp.createBitstreamFormat(bitstreamFormat);
|
||||||
|
|
||||||
|
expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,49 @@
|
|||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the page to create a new bitstream format.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-add-bitstream-format',
|
||||||
|
templateUrl: './add-bitstream-format.component.html',
|
||||||
|
})
|
||||||
|
export class AddBitstreamFormatComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private notificationService: NotificationsService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new bitstream format based on the provided bitstream format emitted by the form.
|
||||||
|
* When successful, a success notification will be shown and the user will be navigated back to the overview page.
|
||||||
|
* When failed, an error notification will be shown.
|
||||||
|
* @param bitstreamFormat
|
||||||
|
*/
|
||||||
|
createBitstreamFormat(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.bitstreamFormatDataService.createBitstreamFormat(bitstreamFormat).pipe(take(1)
|
||||||
|
).subscribe((response: RestResponse) => {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.create.success.head'),
|
||||||
|
this.translateService.get('admin.registries.bitstream-formats.create.success.content'));
|
||||||
|
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||||
|
this.bitstreamFormatDataService.clearBitStreamFormatRequests().subscribe();
|
||||||
|
} else {
|
||||||
|
this.notificationService.error(this.translateService.get('admin.registries.bitstream-formats.create.failure.head'),
|
||||||
|
this.translateService.get('admin.registries.bitstream-formats.create.failure.content'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { type } from '../../../shared/ngrx/type';
|
||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each action type in an action group, make a simple
|
||||||
|
* enum object for all of this group's action types.
|
||||||
|
*
|
||||||
|
* The 'type' utility function coerces strings into string
|
||||||
|
* literal types and runs a simple check to guarantee all
|
||||||
|
* action types in the application are unique.
|
||||||
|
*/
|
||||||
|
export const BitstreamFormatsRegistryActionTypes = {
|
||||||
|
|
||||||
|
SELECT_FORMAT: type('dspace/bitstream-formats-registry/SELECT_FORMAT'),
|
||||||
|
DESELECT_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_FORMAT'),
|
||||||
|
DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_FORMAT')
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* Used to select a single bitstream format in the bitstream format registry
|
||||||
|
*/
|
||||||
|
export class BitstreamFormatsRegistrySelectAction implements Action {
|
||||||
|
type = BitstreamFormatsRegistryActionTypes.SELECT_FORMAT;
|
||||||
|
|
||||||
|
bitstreamFormat: BitstreamFormat;
|
||||||
|
|
||||||
|
constructor(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.bitstreamFormat = bitstreamFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect a single bitstream format in the bitstream format registry
|
||||||
|
*/
|
||||||
|
export class BitstreamFormatsRegistryDeselectAction implements Action {
|
||||||
|
type = BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT;
|
||||||
|
|
||||||
|
bitstreamFormat: BitstreamFormat;
|
||||||
|
|
||||||
|
constructor(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.bitstreamFormat = bitstreamFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect all bitstream formats in the bitstream format registry
|
||||||
|
*/
|
||||||
|
export class BitstreamFormatsRegistryDeselectAllAction implements Action {
|
||||||
|
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a type alias of all actions in this action group
|
||||||
|
* so that reducers can easily compose action types
|
||||||
|
* These are all the actions to perform on the bitstream format registry state
|
||||||
|
*/
|
||||||
|
export type BitstreamFormatsRegistryAction
|
||||||
|
= BitstreamFormatsRegistrySelectAction
|
||||||
|
| BitstreamFormatsRegistryDeselectAction
|
||||||
|
| BitstreamFormatsRegistryDeselectAllAction
|
@@ -0,0 +1,83 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
import { bitstreamFormatReducer, BitstreamFormatRegistryState } from './bitstream-format.reducers';
|
||||||
|
import {
|
||||||
|
BitstreamFormatsRegistryDeselectAction,
|
||||||
|
BitstreamFormatsRegistryDeselectAllAction,
|
||||||
|
BitstreamFormatsRegistrySelectAction
|
||||||
|
} from './bitstream-format.actions';
|
||||||
|
|
||||||
|
const bitstreamFormat1: BitstreamFormat = new BitstreamFormat();
|
||||||
|
bitstreamFormat1.id = 'test-uuid-1';
|
||||||
|
bitstreamFormat1.shortDescription = 'test-short-1';
|
||||||
|
|
||||||
|
const bitstreamFormat2: BitstreamFormat = new BitstreamFormat();
|
||||||
|
bitstreamFormat2.id = 'test-uuid-2';
|
||||||
|
bitstreamFormat2.shortDescription = 'test-short-2';
|
||||||
|
|
||||||
|
const initialState: BitstreamFormatRegistryState = {
|
||||||
|
selectedBitstreamFormats: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const bitstream1SelectedState: BitstreamFormatRegistryState = {
|
||||||
|
selectedBitstreamFormats: [bitstreamFormat1]
|
||||||
|
};
|
||||||
|
|
||||||
|
const bitstream1and2SelectedState: BitstreamFormatRegistryState = {
|
||||||
|
selectedBitstreamFormats: [bitstreamFormat1, bitstreamFormat2]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('BitstreamFormatReducer', () => {
|
||||||
|
describe('BitstreamFormatsRegistryActionTypes.SELECT_FORMAT', () => {
|
||||||
|
it('should add the format to the list of selected formats when initial list is empty', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new BitstreamFormatsRegistrySelectAction(bitstreamFormat1);
|
||||||
|
const newState = bitstreamFormatReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(bitstream1SelectedState);
|
||||||
|
});
|
||||||
|
it('should add the format to the list of selected formats when formats are already present', () => {
|
||||||
|
const state = bitstream1SelectedState;
|
||||||
|
const action = new BitstreamFormatsRegistrySelectAction(bitstreamFormat2);
|
||||||
|
const newState = bitstreamFormatReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(bitstream1and2SelectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT', () => {
|
||||||
|
it('should deselect a format', () => {
|
||||||
|
const state = bitstream1and2SelectedState;
|
||||||
|
const action = new BitstreamFormatsRegistryDeselectAction(bitstreamFormat2);
|
||||||
|
const newState = bitstreamFormatReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(bitstream1SelectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT', () => {
|
||||||
|
it('should deselect all formats', () => {
|
||||||
|
const state = bitstream1and2SelectedState;
|
||||||
|
const action = new BitstreamFormatsRegistryDeselectAllAction();
|
||||||
|
const newState = bitstreamFormatReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(initialState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Invalid action', () => {
|
||||||
|
it('should return the current state', () => {
|
||||||
|
const state = initialState;
|
||||||
|
const action = new NullAction();
|
||||||
|
|
||||||
|
const newState = bitstreamFormatReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class NullAction implements Action {
|
||||||
|
type = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// empty constructor
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,55 @@
|
|||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
import {
|
||||||
|
BitstreamFormatsRegistryAction,
|
||||||
|
BitstreamFormatsRegistryActionTypes,
|
||||||
|
BitstreamFormatsRegistryDeselectAction,
|
||||||
|
BitstreamFormatsRegistrySelectAction
|
||||||
|
} from './bitstream-format.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream format registry state.
|
||||||
|
* @interface BitstreamFormatRegistryState
|
||||||
|
*/
|
||||||
|
export interface BitstreamFormatRegistryState {
|
||||||
|
selectedBitstreamFormats: BitstreamFormat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial state.
|
||||||
|
*/
|
||||||
|
const initialState: BitstreamFormatRegistryState = {
|
||||||
|
selectedBitstreamFormats: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reducer that handles BitstreamFormatsRegistryActions to modify the bitstream format registry state
|
||||||
|
* @param state The current BitstreamFormatRegistryState
|
||||||
|
* @param action The BitstreamFormatsRegistryAction to perform on the state
|
||||||
|
*/
|
||||||
|
export function bitstreamFormatReducer(state = initialState, action: BitstreamFormatsRegistryAction): BitstreamFormatRegistryState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case BitstreamFormatsRegistryActionTypes.SELECT_FORMAT: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
selectedBitstreamFormats: [...state.selectedBitstreamFormats, (action as BitstreamFormatsRegistrySelectAction).bitstreamFormat]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
selectedBitstreamFormats: state.selectedBitstreamFormats.filter(
|
||||||
|
(selectedBitstreamFormats) => selectedBitstreamFormats !== (action as BitstreamFormatsRegistryDeselectAction).bitstreamFormat
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
selectedBitstreamFormats: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
||||||
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||||
|
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||||
|
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||||
|
|
||||||
|
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||||
|
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: BitstreamFormatsComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: BITSTREAMFORMAT_ADD_PATH,
|
||||||
|
component: AddBitstreamFormatComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||||
|
component: EditBitstreamFormatComponent,
|
||||||
|
resolve: {
|
||||||
|
bitstreamFormat: BitstreamFormatsResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
BitstreamFormatsResolver,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class BitstreamFormatsRoutingModule {
|
||||||
|
|
||||||
|
}
|
@@ -2,13 +2,15 @@
|
|||||||
<div class="bitstream-formats row">
|
<div class="bitstream-formats row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.bitstream-formats.head' | translate}}</h2>
|
<h2 id="header" class="border-bottom pb-2 ">{{'admin.registries.bitstream-formats.head' | translate}}</h2>
|
||||||
|
|
||||||
|
<p id="description">{{'admin.registries.bitstream-formats.description' | translate}}</p>
|
||||||
|
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
|
||||||
|
|
||||||
<p id="description" class="pb-2">{{'admin.registries.bitstream-formats.description' | translate}}</p>
|
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="pageConfig"
|
||||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
@@ -18,25 +20,38 @@
|
|||||||
<table id="formats" class="table table-striped table-hover">
|
<table id="formats" class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.name' | translate}}</th>
|
<th scope="col"></th>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.mimetype' | translate}}</th>
|
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.supportLevel.head' | translate}}</th>
|
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||||
|
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||||
<td>{{bitstreamFormat.shortDescription}}</td>
|
<td>
|
||||||
<td>{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.formats.table.internal' | translate}})</span></td>
|
<label>
|
||||||
<td>{{'admin.registries.bitstream-formats.formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</td>
|
<input type="checkbox"
|
||||||
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||||
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||||
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||||
{{'admin.registries.bitstream-formats.formats.no-items' | translate}}
|
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
||||||
|
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } 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';
|
||||||
@@ -13,68 +12,105 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
import { HostWindowService } from '../../../shared/host-window.service';
|
import { HostWindowService } from '../../../shared/host-window.service';
|
||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
describe('BitstreamFormatsComponent', () => {
|
describe('BitstreamFormatsComponent', () => {
|
||||||
let comp: BitstreamFormatsComponent;
|
let comp: BitstreamFormatsComponent;
|
||||||
let fixture: ComponentFixture<BitstreamFormatsComponent>;
|
let fixture: ComponentFixture<BitstreamFormatsComponent>;
|
||||||
let registryService: RegistryService;
|
let bitstreamFormatService;
|
||||||
const mockFormatsList = [
|
let scheduler: TestScheduler;
|
||||||
{
|
let notificationsServiceStub;
|
||||||
shortDescription: 'Unknown',
|
|
||||||
description: 'Unknown data format',
|
const bitstreamFormat1 = new BitstreamFormat();
|
||||||
mimetype: 'application/octet-stream',
|
bitstreamFormat1.uuid = 'test-uuid-1';
|
||||||
supportLevel: 0,
|
bitstreamFormat1.id = 'test-uuid-1';
|
||||||
internal: false,
|
bitstreamFormat1.shortDescription = 'Unknown';
|
||||||
extensions: null
|
bitstreamFormat1.description = 'Unknown data format';
|
||||||
},
|
bitstreamFormat1.mimetype = 'application/octet-stream';
|
||||||
{
|
bitstreamFormat1.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||||
shortDescription: 'License',
|
bitstreamFormat1.internal = false;
|
||||||
description: 'Item-specific license agreed upon to submission',
|
bitstreamFormat1.extensions = null;
|
||||||
mimetype: 'text/plain; charset=utf-8',
|
|
||||||
supportLevel: 1,
|
const bitstreamFormat2 = new BitstreamFormat();
|
||||||
internal: true,
|
bitstreamFormat2.uuid = 'test-uuid-2';
|
||||||
extensions: null
|
bitstreamFormat2.id = 'test-uuid-2';
|
||||||
},
|
bitstreamFormat2.shortDescription = 'License';
|
||||||
{
|
bitstreamFormat2.description = 'Item-specific license agreed upon to submission';
|
||||||
shortDescription: 'CC License',
|
bitstreamFormat2.mimetype = 'text/plain; charset=utf-8';
|
||||||
description: 'Item-specific Creative Commons license agreed upon to submission',
|
bitstreamFormat2.supportLevel = BitstreamFormatSupportLevel.Known;
|
||||||
mimetype: 'text/html; charset=utf-8',
|
bitstreamFormat2.internal = true;
|
||||||
supportLevel: 2,
|
bitstreamFormat2.extensions = null;
|
||||||
internal: true,
|
|
||||||
extensions: null
|
const bitstreamFormat3 = new BitstreamFormat();
|
||||||
},
|
bitstreamFormat3.uuid = 'test-uuid-3';
|
||||||
{
|
bitstreamFormat3.id = 'test-uuid-3';
|
||||||
shortDescription: 'Adobe PDF',
|
bitstreamFormat3.shortDescription = 'CC License';
|
||||||
description: 'Adobe Portable Document Format',
|
bitstreamFormat3.description = 'Item-specific Creative Commons license agreed upon to submission';
|
||||||
mimetype: 'application/pdf',
|
bitstreamFormat3.mimetype = 'text/html; charset=utf-8';
|
||||||
supportLevel: 0,
|
bitstreamFormat3.supportLevel = BitstreamFormatSupportLevel.Supported;
|
||||||
internal: false,
|
bitstreamFormat3.internal = true;
|
||||||
extensions: null
|
bitstreamFormat3.extensions = null;
|
||||||
}
|
|
||||||
];
|
const bitstreamFormat4 = new BitstreamFormat();
|
||||||
const mockFormats = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFormatsList));
|
bitstreamFormat4.uuid = 'test-uuid-4';
|
||||||
const registryServiceStub = {
|
bitstreamFormat4.id = 'test-uuid-4';
|
||||||
getBitstreamFormats: () => mockFormats
|
bitstreamFormat4.shortDescription = 'Adobe PDF';
|
||||||
};
|
bitstreamFormat4.description = 'Adobe Portable Document Format';
|
||||||
|
bitstreamFormat4.mimetype = 'application/pdf';
|
||||||
|
bitstreamFormat4.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||||
|
bitstreamFormat4.internal = false;
|
||||||
|
bitstreamFormat4.extensions = null;
|
||||||
|
|
||||||
|
const mockFormatsList: BitstreamFormat[] = [
|
||||||
|
bitstreamFormat1,
|
||||||
|
bitstreamFormat2,
|
||||||
|
bitstreamFormat3,
|
||||||
|
bitstreamFormat4
|
||||||
|
];
|
||||||
|
const mockFormatsRD = new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList));
|
||||||
|
|
||||||
|
const initAsync = () => {
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
|
findAll: observableOf(mockFormatsRD),
|
||||||
|
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||||
|
getSelectedBitstreamFormats: hot('a', {a: mockFormatsList}),
|
||||||
|
selectBitstreamFormat: {},
|
||||||
|
deselectBitstreamFormat: {},
|
||||||
|
deselectAllBitstreamFormats: {},
|
||||||
|
delete: observableOf(true),
|
||||||
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
const initBeforeEach = () => {
|
||||||
fixture = TestBed.createComponent(BitstreamFormatsComponent);
|
fixture = TestBed.createComponent(BitstreamFormatsComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
registryService = (comp as any).service;
|
};
|
||||||
});
|
|
||||||
|
describe('Bitstream format page content', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
|
||||||
it('should contain four formats', () => {
|
it('should contain four formats', () => {
|
||||||
const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
|
const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
|
||||||
@@ -82,17 +118,172 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the correct formats', () => {
|
it('should contain the correct formats', () => {
|
||||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(1)')).nativeElement;
|
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||||
expect(unknownName.textContent).toBe('Unknown');
|
expect(unknownName.textContent).toBe('Unknown');
|
||||||
|
|
||||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(1)')).nativeElement;
|
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||||
expect(licenseName.textContent).toBe('License');
|
expect(licenseName.textContent).toBe('License');
|
||||||
|
|
||||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(1)')).nativeElement;
|
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
|
||||||
expect(ccLicenseName.textContent).toBe('CC License');
|
expect(ccLicenseName.textContent).toBe('CC License');
|
||||||
|
|
||||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(1)')).nativeElement;
|
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
|
||||||
expect(adobeName.textContent).toBe('Adobe PDF');
|
expect(adobeName.textContent).toBe('Adobe PDF');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectBitStreamFormat', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should select a bitstreamFormat if it was selected in the event', () => {
|
||||||
|
const event = {target: {checked: true}};
|
||||||
|
|
||||||
|
comp.selectBitStreamFormat(bitstreamFormat1, event);
|
||||||
|
|
||||||
|
expect(bitstreamFormatService.selectBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat1);
|
||||||
|
});
|
||||||
|
it('should deselect a bitstreamFormat if it is deselected in the event', () => {
|
||||||
|
const event = {target: {checked: false}};
|
||||||
|
|
||||||
|
comp.selectBitStreamFormat(bitstreamFormat1, event);
|
||||||
|
|
||||||
|
expect(bitstreamFormatService.deselectBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat1);
|
||||||
|
});
|
||||||
|
it('should be called when a user clicks a checkbox', () => {
|
||||||
|
spyOn(comp, 'selectBitStreamFormat');
|
||||||
|
const unknownFormat = fixture.debugElement.query(By.css('#formats tr:nth-child(1) input'));
|
||||||
|
|
||||||
|
const event = {target: {checked: true}};
|
||||||
|
unknownFormat.triggerEventHandler('change', event);
|
||||||
|
|
||||||
|
expect(comp.selectBitStreamFormat).toHaveBeenCalledWith(bitstreamFormat1, event);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSelected', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
|
||||||
|
const result = comp.isSelected(bitstreamFormat1);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(cold('b', {b: true}));
|
||||||
|
});
|
||||||
|
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
|
||||||
|
const format = new BitstreamFormat();
|
||||||
|
format.uuid = 'new';
|
||||||
|
|
||||||
|
const result = comp.isSelected(format);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(cold('b', {b: false}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deselectAll', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should deselect all bitstreamFormats', () => {
|
||||||
|
comp.deselectAll();
|
||||||
|
expect(bitstreamFormatService.deselectAllBitstreamFormats).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be called when the deselect all button is clicked', () => {
|
||||||
|
spyOn(comp, 'deselectAll');
|
||||||
|
const deselectAllButton = fixture.debugElement.query(By.css('button.deselect'));
|
||||||
|
deselectAllButton.triggerEventHandler('click', null);
|
||||||
|
|
||||||
|
expect(comp.deselectAll).toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFormats success', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
|
findAll: observableOf(mockFormatsRD),
|
||||||
|
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||||
|
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||||
|
selectBitstreamFormat: {},
|
||||||
|
deselectBitstreamFormat: {},
|
||||||
|
deselectAllBitstreamFormats: {},
|
||||||
|
delete: observableOf(true),
|
||||||
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||||
|
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should clear bitstream formats ', () => {
|
||||||
|
comp.deleteFormats();
|
||||||
|
|
||||||
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
||||||
|
'admin.registries.bitstream-formats.delete.success.amount');
|
||||||
|
expect(notificationsServiceStub.error).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFormats error', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
|
||||||
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
|
findAll: observableOf(mockFormatsRD),
|
||||||
|
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||||
|
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||||
|
selectBitstreamFormat: {},
|
||||||
|
deselectBitstreamFormat: {},
|
||||||
|
deselectAllBitstreamFormats: {},
|
||||||
|
delete: observableOf(false),
|
||||||
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||||
|
providers: [
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||||
|
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||||
|
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should clear bitstream formats ', () => {
|
||||||
|
comp.deleteFormats();
|
||||||
|
|
||||||
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
|
||||||
|
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
||||||
|
'admin.registries.bitstream-formats.delete.failure.amount');
|
||||||
|
expect(notificationsServiceStub.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
||||||
import { Observable } 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 { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
|
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
|
import { FindAllOptions } from '../../../core/data/request.models';
|
||||||
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a list of bitstream formats
|
* This component renders a list of bitstream formats
|
||||||
@@ -13,24 +19,125 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
|||||||
selector: 'ds-bitstream-formats',
|
selector: 'ds-bitstream-formats',
|
||||||
templateUrl: './bitstream-formats.component.html'
|
templateUrl: './bitstream-formats.component.html'
|
||||||
})
|
})
|
||||||
export class BitstreamFormatsComponent {
|
export class BitstreamFormatsComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A paginated list of bitstream formats to be shown on the page
|
* A paginated list of bitstream formats to be shown on the page
|
||||||
*/
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats
|
||||||
|
*/
|
||||||
|
pageState: BehaviorSubject<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current pagination configuration for the page used by the FindAll method
|
||||||
|
* Currently simply renders all bitstream formats
|
||||||
|
*/
|
||||||
|
config: FindAllOptions = Object.assign(new FindAllOptions(), {
|
||||||
|
elementsPerPage: 20
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pagination configuration for the page
|
* The current pagination configuration for the page
|
||||||
* Currently simply renders all bitstream formats
|
* Currently simply renders all bitstream formats
|
||||||
*/
|
*/
|
||||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'registry-bitstreamformats-pagination',
|
id: 'registry-bitstreamformats-pagination',
|
||||||
pageSize: 10000
|
pageSize: 20
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private registryService: RegistryService) {
|
constructor(private notificationsService: NotificationsService,
|
||||||
this.updateFormats();
|
private router: Router,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private bitstreamFormatService: BitstreamFormatDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the currently selected formats from the registry and updates the presented list
|
||||||
|
*/
|
||||||
|
deleteFormats() {
|
||||||
|
this.bitstreamFormatService.clearBitStreamFormatRequests().subscribe();
|
||||||
|
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(take(1)).subscribe(
|
||||||
|
(formats) => {
|
||||||
|
const tasks$ = [];
|
||||||
|
for (const format of formats) {
|
||||||
|
if (hasValue(format.id)) {
|
||||||
|
tasks$.push(this.bitstreamFormatService.delete(format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||||
|
const successResponses = results.filter((result: boolean) => result);
|
||||||
|
const failedResponses = results.filter((result: boolean) => !result);
|
||||||
|
if (successResponses.length > 0) {
|
||||||
|
this.showNotification(true, successResponses.length);
|
||||||
|
}
|
||||||
|
if (failedResponses.length > 0) {
|
||||||
|
this.showNotification(false, failedResponses.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deselectAll();
|
||||||
|
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: Object.assign({}, { page: 1 }),
|
||||||
|
queryParamsHandling: 'merge'
|
||||||
|
}); });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deselects all selecetd bitstream formats
|
||||||
|
*/
|
||||||
|
deselectAll() {
|
||||||
|
this.bitstreamFormatService.deselectAllBitstreamFormats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a given bitstream format is selected in the list (checkbox)
|
||||||
|
* @param bitstreamFormat
|
||||||
|
*/
|
||||||
|
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
||||||
|
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
|
map((bitstreamFormats: BitstreamFormat[]) => {
|
||||||
|
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects or deselects a bitstream format based on the checkbox state
|
||||||
|
* @param bitstreamFormat
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
selectBitStreamFormat(bitstreamFormat: BitstreamFormat, event) {
|
||||||
|
event.target.checked ?
|
||||||
|
this.bitstreamFormatService.selectBitstreamFormat(bitstreamFormat) :
|
||||||
|
this.bitstreamFormatService.deselectBitstreamFormat(bitstreamFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notifications for an amount of deleted bitstream formats
|
||||||
|
* @param success Whether or not the notification should be a success message (error message when false)
|
||||||
|
* @param amount The amount of deleted bitstream formats
|
||||||
|
*/
|
||||||
|
private showNotification(success: boolean, amount: number) {
|
||||||
|
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||||
|
const suffix = success ? 'success' : 'failure';
|
||||||
|
|
||||||
|
const messages = observableCombineLatest(
|
||||||
|
this.translateService.get(`${prefix}.${suffix}.head`),
|
||||||
|
this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount})
|
||||||
|
);
|
||||||
|
messages.subscribe(([head, content]) => {
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.notificationsService.success(head, content);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(head, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,14 +145,26 @@ export class BitstreamFormatsComponent {
|
|||||||
* @param event The page change event
|
* @param event The page change event
|
||||||
*/
|
*/
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.config.currentPage = event;
|
this.config = Object.assign(new FindAllOptions(), this.config, {
|
||||||
this.updateFormats();
|
currentPage: event,
|
||||||
|
});
|
||||||
|
this.pageConfig.currentPage = event;
|
||||||
|
this.pageState.next('pageChange');
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.pageState = new BehaviorSubject('init');
|
||||||
|
this.bitstreamFormats = this.pageState.pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
return this.updateFormats()
|
||||||
|
;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to update the bitstream formats that are shown
|
* Finds all formats based on the current config
|
||||||
*/
|
*/
|
||||||
private updateFormats() {
|
private updateFormats() {
|
||||||
this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
|
return this.bitstreamFormatService.findAll(this.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||||
|
import { SharedModule } from '../../../shared/shared.module';
|
||||||
|
import { FormatFormComponent } from './format-form/format-form.component';
|
||||||
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||||
|
import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module';
|
||||||
|
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
RouterModule,
|
||||||
|
TranslateModule,
|
||||||
|
BitstreamFormatsRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
BitstreamFormatsComponent,
|
||||||
|
EditBitstreamFormatComponent,
|
||||||
|
AddBitstreamFormatComponent,
|
||||||
|
FormatFormComponent
|
||||||
|
],
|
||||||
|
entryComponents: []
|
||||||
|
})
|
||||||
|
export class BitstreamFormatsModule {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { find } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific bitstreamFormat before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BitstreamFormatsResolver implements Resolve<RemoteData<BitstreamFormat>> {
|
||||||
|
constructor(private bitstreamFormatDataService: BitstreamFormatDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving an bitstreamFormat based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<BitstreamFormat>> Emits the found bitstreamFormat based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<BitstreamFormat>> {
|
||||||
|
return this.bitstreamFormatDataService.findById(route.params.id)
|
||||||
|
.pipe(
|
||||||
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<h2 id="sub-header"
|
||||||
|
class="border-bottom mb-2">{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}</h2>
|
||||||
|
|
||||||
|
<ds-bitstream-format-form [bitstreamFormat]="(bitstreamFormatRD$ | async)?.payload" (updatedFormat)="updateFormat($event)"></ds-bitstream-format-form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,123 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format.component';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||||
|
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||||
|
import { ResourceType } from '../../../../core/shared/resource-type';
|
||||||
|
|
||||||
|
describe('EditBitstreamFormatComponent', () => {
|
||||||
|
let comp: EditBitstreamFormatComponent;
|
||||||
|
let fixture: ComponentFixture<EditBitstreamFormatComponent>;
|
||||||
|
|
||||||
|
const bitstreamFormat = new BitstreamFormat();
|
||||||
|
bitstreamFormat.uuid = 'test-uuid-1';
|
||||||
|
bitstreamFormat.id = 'test-uuid-1';
|
||||||
|
bitstreamFormat.shortDescription = 'Unknown';
|
||||||
|
bitstreamFormat.description = 'Unknown data format';
|
||||||
|
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||||
|
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||||
|
bitstreamFormat.internal = false;
|
||||||
|
bitstreamFormat.extensions = null;
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
bitstreamFormat: new RemoteData(false, false, true, null, bitstreamFormat)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let router;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let bitstreamFormatDataService: BitstreamFormatDataService;
|
||||||
|
|
||||||
|
const initAsync = () => {
|
||||||
|
router = new RouterStub();
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||||
|
updateBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success'))
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [EditBitstreamFormatComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: NotificationsService, useValue: notificationService},
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initBeforeEach = () => {
|
||||||
|
fixture = TestBed.createComponent(EditBitstreamFormatComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should initialise the bitstreamFormat based on the route', () => {
|
||||||
|
|
||||||
|
comp.bitstreamFormatRD$.subscribe((format: RemoteData<BitstreamFormat>) => {
|
||||||
|
expect(format).toEqual(new RemoteData(false, false, true, null, bitstreamFormat));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('updateFormat success', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||||
|
comp.updateFormat(bitstreamFormat);
|
||||||
|
|
||||||
|
expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('updateFormat error', () => {
|
||||||
|
beforeEach(async( () => {
|
||||||
|
router = new RouterStub();
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||||
|
updateBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request'))
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [EditBitstreamFormatComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
{provide: NotificationsService, useValue: notificationService},
|
||||||
|
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||||
|
comp.updateFormat(bitstreamFormat);
|
||||||
|
|
||||||
|
expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,62 @@
|
|||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||||
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the edit page of a bitstream format.
|
||||||
|
* The route parameter 'id' is used to request the bitstream format.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-bitstream-format',
|
||||||
|
templateUrl: './edit-bitstream-format.component.html',
|
||||||
|
})
|
||||||
|
export class EditBitstreamFormatComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream format wrapped in a remote-data object
|
||||||
|
*/
|
||||||
|
bitstreamFormatRD$: Observable<RemoteData<BitstreamFormat>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private notificationService: NotificationsService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.bitstreamFormatRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.bitstreamFormat as RemoteData<BitstreamFormat>)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the bitstream format based on the provided bitstream format emitted by the form.
|
||||||
|
* When successful, a success notification will be shown and the user will be navigated back to the overview page.
|
||||||
|
* When failed, an error notification will be shown.
|
||||||
|
*/
|
||||||
|
updateFormat(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.bitstreamFormatDataService.updateBitstreamFormat(bitstreamFormat).pipe(take(1)
|
||||||
|
).subscribe((response: RestResponse) => {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.edit.success.head'),
|
||||||
|
this.translateService.get('admin.registries.bitstream-formats.edit.success.content'));
|
||||||
|
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||||
|
} else {
|
||||||
|
this.notificationService.error('admin.registries.bitstream-formats.edit.failure.head',
|
||||||
|
'admin.registries.bitstream-formats.create.edit.content');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
<ds-form *ngIf="formModel"
|
||||||
|
[formId]="'comcol-form-id'"
|
||||||
|
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
|
@@ -0,0 +1,104 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { FormatFormComponent } from './format-form.component';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||||
|
import { DynamicCheckboxModel, DynamicFormArrayModel, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
|
import { isEmpty } from '../../../../shared/empty.util';
|
||||||
|
|
||||||
|
describe('FormatFormComponent', () => {
|
||||||
|
let comp: FormatFormComponent;
|
||||||
|
let fixture: ComponentFixture<FormatFormComponent>;
|
||||||
|
|
||||||
|
const router = new RouterStub();
|
||||||
|
|
||||||
|
const bitstreamFormat = new BitstreamFormat();
|
||||||
|
bitstreamFormat.uuid = 'test-uuid-1';
|
||||||
|
bitstreamFormat.id = 'test-uuid-1';
|
||||||
|
bitstreamFormat.shortDescription = 'Unknown';
|
||||||
|
bitstreamFormat.description = 'Unknown data format';
|
||||||
|
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||||
|
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||||
|
bitstreamFormat.internal = false;
|
||||||
|
bitstreamFormat.extensions = [];
|
||||||
|
|
||||||
|
const submittedBitstreamFormat = new BitstreamFormat();
|
||||||
|
submittedBitstreamFormat.id = bitstreamFormat.id;
|
||||||
|
submittedBitstreamFormat.shortDescription = bitstreamFormat.shortDescription;
|
||||||
|
submittedBitstreamFormat.mimetype = bitstreamFormat.mimetype;
|
||||||
|
submittedBitstreamFormat.description = bitstreamFormat.description;
|
||||||
|
submittedBitstreamFormat.supportLevel = bitstreamFormat.supportLevel;
|
||||||
|
submittedBitstreamFormat.internal = bitstreamFormat.internal;
|
||||||
|
submittedBitstreamFormat.extensions = bitstreamFormat.extensions;
|
||||||
|
|
||||||
|
const initAsync = () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), ReactiveFormsModule, FormsModule, TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [FormatFormComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: Router, useValue: router},
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initBeforeEach = () => {
|
||||||
|
fixture = TestBed.createComponent(FormatFormComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
comp.bitstreamFormat = bitstreamFormat;
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('initialise', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
it('should initialises the values in the form', () => {
|
||||||
|
|
||||||
|
expect((comp.formModel[0] as DynamicInputModel).value).toBe(bitstreamFormat.shortDescription);
|
||||||
|
expect((comp.formModel[1] as DynamicInputModel).value).toBe(bitstreamFormat.mimetype);
|
||||||
|
expect((comp.formModel[2] as DynamicInputModel).value).toBe(bitstreamFormat.description);
|
||||||
|
expect((comp.formModel[3] as DynamicInputModel).value).toBe(bitstreamFormat.supportLevel);
|
||||||
|
expect((comp.formModel[4] as DynamicCheckboxModel).value).toBe(bitstreamFormat.internal);
|
||||||
|
|
||||||
|
const formArray = (comp.formModel[5] as DynamicFormArrayModel);
|
||||||
|
const extensions = [];
|
||||||
|
for (let i = 0; i < formArray.groups.length; i++) {
|
||||||
|
const value = (formArray.get(i).get(0) as DynamicInputModel).value;
|
||||||
|
if (!isEmpty(value)) {
|
||||||
|
extensions.push((formArray.get(i).get(0) as DynamicInputModel).value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(extensions).toEqual(bitstreamFormat.extensions);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('onSubmit', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
|
||||||
|
it('should emit the bitstreamFormat currently present in the form', () => {
|
||||||
|
spyOn(comp.updatedFormat, 'emit');
|
||||||
|
comp.onSubmit();
|
||||||
|
|
||||||
|
expect(comp.updatedFormat.emit).toHaveBeenCalledWith(submittedBitstreamFormat);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('onCancel', () => {
|
||||||
|
beforeEach(async(initAsync));
|
||||||
|
beforeEach(initBeforeEach);
|
||||||
|
|
||||||
|
it('should navigate back to the bitstream overview', () => {
|
||||||
|
comp.onCancel();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,194 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||||
|
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||||
|
import {
|
||||||
|
DynamicCheckboxModel,
|
||||||
|
DynamicFormArrayModel,
|
||||||
|
DynamicFormControlLayout, DynamicFormControlLayoutConfig,
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicInputModel,
|
||||||
|
DynamicSelectModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||||
|
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component responsible for rendering the form to create/edit a bitstream format
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-bitstream-format-form',
|
||||||
|
templateUrl: './format-form.component.html'
|
||||||
|
})
|
||||||
|
export class FormatFormComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current bitstream format
|
||||||
|
* This can either be and existing one or a new one
|
||||||
|
*/
|
||||||
|
@Input() bitstreamFormat: BitstreamFormat = new BitstreamFormat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventEmitter that will emit the updated bitstream format
|
||||||
|
*/
|
||||||
|
@Output() updatedFormat: EventEmitter<BitstreamFormat> = new EventEmitter<BitstreamFormat>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The different supported support level of the bitstream format
|
||||||
|
*/
|
||||||
|
supportLevelOptions = [{label: BitstreamFormatSupportLevel.Known, value: BitstreamFormatSupportLevel.Known},
|
||||||
|
{label: BitstreamFormatSupportLevel.Unknown, value: BitstreamFormatSupportLevel.Unknown},
|
||||||
|
{label: BitstreamFormatSupportLevel.Supported, value: BitstreamFormatSupportLevel.Supported}];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styling element for repeatable field
|
||||||
|
*/
|
||||||
|
arrayElementLayout: DynamicFormControlLayout = {
|
||||||
|
grid: {
|
||||||
|
group: 'form-row',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Styling element for element of repeatable field
|
||||||
|
*/
|
||||||
|
arrayInputElementLayout: DynamicFormControlLayout = {
|
||||||
|
grid: {
|
||||||
|
host: 'col'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form model representing the bitstream format
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'shortDescription',
|
||||||
|
name: 'shortDescription',
|
||||||
|
label: 'admin.registries.bitstream-formats.edit.shortDescription.label',
|
||||||
|
hint: 'admin.registries.bitstream-formats.edit.shortDescription.hint',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'Please enter a name for this bitstream format'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'mimetype',
|
||||||
|
name: 'mimetype',
|
||||||
|
label: 'admin.registries.bitstream-formats.edit.mimetype.label',
|
||||||
|
hint: 'admin.registries.bitstream-formats.edit.mimetype.hint',
|
||||||
|
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'description',
|
||||||
|
label: 'admin.registries.bitstream-formats.edit.description.label',
|
||||||
|
hint: 'admin.registries.bitstream-formats.edit.description.hint',
|
||||||
|
|
||||||
|
}),
|
||||||
|
new DynamicSelectModel({
|
||||||
|
id: 'supportLevel',
|
||||||
|
name: 'supportLevel',
|
||||||
|
options: this.supportLevelOptions,
|
||||||
|
label: 'admin.registries.bitstream-formats.edit.supportLevel.label',
|
||||||
|
hint: 'admin.registries.bitstream-formats.edit.supportLevel.hint',
|
||||||
|
value: this.supportLevelOptions[0].value
|
||||||
|
|
||||||
|
}),
|
||||||
|
new DynamicCheckboxModel({
|
||||||
|
id: 'internal',
|
||||||
|
name: 'internal',
|
||||||
|
label: 'Internal',
|
||||||
|
hint: 'admin.registries.bitstream-formats.edit.internal.hint',
|
||||||
|
}),
|
||||||
|
new DynamicFormArrayModel({
|
||||||
|
id: 'extensions',
|
||||||
|
name: 'extensions',
|
||||||
|
label: 'admin.registries.bitstream-formats.edit.extensions.label',
|
||||||
|
groupFactory: () => [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'extension',
|
||||||
|
placeholder: 'admin.registries.bitstream-formats.edit.extensions.placeholder',
|
||||||
|
}, this.arrayInputElementLayout)
|
||||||
|
]
|
||||||
|
}, this.arrayElementLayout),
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private dynamicFormService: DynamicFormService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private router: Router) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
this.initValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the form based on the provided bitstream format
|
||||||
|
*/
|
||||||
|
initValues() {
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicFormControlModel) => {
|
||||||
|
if (fieldModel.name === 'extensions') {
|
||||||
|
if (hasValue(this.bitstreamFormat.extensions)) {
|
||||||
|
const extenstions = this.bitstreamFormat.extensions;
|
||||||
|
const formArray = (fieldModel as DynamicFormArrayModel);
|
||||||
|
for (let i = 0; i < extenstions.length; i++) {
|
||||||
|
formArray.insertGroup(i).group[0] = new DynamicInputModel({
|
||||||
|
id: `extension-${i}`,
|
||||||
|
value: extenstions[i]
|
||||||
|
}, this.arrayInputElementLayout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasValue(this.bitstreamFormat[fieldModel.name])) {
|
||||||
|
(fieldModel as DynamicInputModel).value = this.bitstreamFormat[fieldModel.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an updated bistream format based on the current values in the form
|
||||||
|
* Emits the updated bitstream format trouhg the updatedFormat emitter
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
const updatedBitstreamFormat = Object.assign(new BitstreamFormat(),
|
||||||
|
{
|
||||||
|
id: this.bitstreamFormat.id
|
||||||
|
});
|
||||||
|
|
||||||
|
this.formModel.forEach(
|
||||||
|
(fieldModel: DynamicFormControlModel) => {
|
||||||
|
if (fieldModel.name === 'extensions') {
|
||||||
|
const formArray = (fieldModel as DynamicFormArrayModel);
|
||||||
|
const extensions = [];
|
||||||
|
for (let i = 0; i < formArray.groups.length; i++) {
|
||||||
|
const value = (formArray.get(i).get(0) as DynamicInputModel).value;
|
||||||
|
if (!isEmpty(value)) {
|
||||||
|
extensions.push((formArray.get(i).get(0) as DynamicInputModel).value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedBitstreamFormat.extensions = extensions;
|
||||||
|
} else {
|
||||||
|
updatedBitstreamFormat[fieldModel.name] = (fieldModel as DynamicInputModel).value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updatedFormat.emit(updatedBitstreamFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the edit/create action of the bitstream format and navigates back to the bitstream format registry
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,19 @@
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { getAdminModulePath } from '../app-routing.module';
|
||||||
|
|
||||||
|
const REGISTRIES_MODULE_PATH = 'registries';
|
||||||
|
|
||||||
|
export function getRegistriesModulePath() {
|
||||||
|
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: 'registries',
|
path: REGISTRIES_MODULE_PATH,
|
||||||
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@@ -52,6 +52,9 @@
|
|||||||
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading"
|
||||||
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||||
|
<div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'collection.page.browse.recent.empty' | translate}}
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="collectionRD?.hasFailed"
|
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||||
|
@@ -0,0 +1,179 @@
|
|||||||
|
import { Component, Inject, Injectable, OnInit } from '@angular/core';
|
||||||
|
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* Abstract component for managing object updates of an item
|
||||||
|
*/
|
||||||
|
export abstract class AbstractItemUpdateComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The item to display the edit page for
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
/**
|
||||||
|
* The current values and updates for all this item's fields
|
||||||
|
* Should be initialized in the initializeUpdates method of the child component
|
||||||
|
*/
|
||||||
|
updates$: Observable<FieldUpdates>;
|
||||||
|
/**
|
||||||
|
* The current url of this page
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Prefix for this component's notification translate keys
|
||||||
|
* Should be initialized in the initializeNotificationsPrefix method of the child component
|
||||||
|
*/
|
||||||
|
notificationsPrefix;
|
||||||
|
/**
|
||||||
|
* The time span for being able to undo discarding changes
|
||||||
|
*/
|
||||||
|
discardTimeOut: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
protected router: Router,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize common properties between item-update components
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.parent.data.pipe(map((data) => data.item))
|
||||||
|
.pipe(
|
||||||
|
first(),
|
||||||
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.item = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout;
|
||||||
|
this.url = this.router.url;
|
||||||
|
if (this.url.indexOf('?') > 0) {
|
||||||
|
this.url = this.url.substr(0, this.url.indexOf('?'));
|
||||||
|
}
|
||||||
|
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
||||||
|
if (!hasChanges) {
|
||||||
|
this.initializeOriginalFields();
|
||||||
|
} else {
|
||||||
|
this.checkLastModified();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initializeNotificationsPrefix();
|
||||||
|
this.initializeUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the values and updates of the current item's fields
|
||||||
|
*/
|
||||||
|
abstract initializeUpdates(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the prefix for notification messages
|
||||||
|
*/
|
||||||
|
abstract initializeNotificationsPrefix(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends all initial values of this item to the object updates service
|
||||||
|
*/
|
||||||
|
abstract initializeOriginalFields(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
|
*/
|
||||||
|
trackUpdate(index, update: FieldUpdate) {
|
||||||
|
return update && update.field ? update.field.uuid : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not there are currently updates for this item
|
||||||
|
*/
|
||||||
|
hasChanges(): Observable<boolean> {
|
||||||
|
return this.objectUpdatesService.hasUpdates(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current page is entirely valid
|
||||||
|
*/
|
||||||
|
protected isValid() {
|
||||||
|
return this.objectUpdatesService.isValidPage(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current item is still in sync with the version in the store
|
||||||
|
* If it's not, a notification is shown and the changes are removed
|
||||||
|
*/
|
||||||
|
private checkLastModified() {
|
||||||
|
const currentVersion = this.item.lastModified;
|
||||||
|
this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe(
|
||||||
|
(updateVersion: Date) => {
|
||||||
|
if (updateVersion.getDate() !== currentVersion.getDate()) {
|
||||||
|
this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated'));
|
||||||
|
this.initializeOriginalFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the current changes
|
||||||
|
*/
|
||||||
|
abstract submit(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the object updates service to discard all current changes to this item
|
||||||
|
* Shows a notification to remind the user that they can undo this
|
||||||
|
*/
|
||||||
|
discard() {
|
||||||
|
const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut });
|
||||||
|
this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the object updates service to undo discarding all changes to this item
|
||||||
|
*/
|
||||||
|
reinstate() {
|
||||||
|
this.objectUpdatesService.reinstateFieldUpdates(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether or not the item is currently reinstatable
|
||||||
|
*/
|
||||||
|
isReinstatable(): Observable<boolean> {
|
||||||
|
return this.objectUpdatesService.isReinstatable(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification title
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
protected getNotificationTitle(key: string) {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification content
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
protected getNotificationContent(key: string) {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||||
import { EditItemPageComponent } from './edit-item-page.component';
|
import { EditItemPageComponent } from './edit-item-page.component';
|
||||||
import { ItemStatusComponent } from './item-status/item-status.component';
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
import { ItemOperationComponent } from './item-operation/item-operation.component';
|
||||||
@@ -14,8 +15,10 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
|||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
|
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
||||||
|
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module that contains all components related to the Edit Item page administrator functionality
|
* Module that contains all components related to the Edit Item page administrator functionality
|
||||||
@@ -38,8 +41,11 @@ import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
|||||||
ItemDeleteComponent,
|
ItemDeleteComponent,
|
||||||
ItemStatusComponent,
|
ItemStatusComponent,
|
||||||
ItemMetadataComponent,
|
ItemMetadataComponent,
|
||||||
|
ItemRelationshipsComponent,
|
||||||
ItemBitstreamsComponent,
|
ItemBitstreamsComponent,
|
||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
|
EditRelationshipComponent,
|
||||||
|
EditRelationshipListComponent,
|
||||||
ItemMoveComponent,
|
ItemMoveComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -11,15 +11,13 @@ import { ItemStatusComponent } from './item-status/item-status.component';
|
|||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
import { getItemEditPath } from '../item-page-routing.module';
|
|
||||||
|
|
||||||
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
|
||||||
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
|
||||||
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
const ITEM_EDIT_PRIVATE_PATH = 'private';
|
||||||
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
const ITEM_EDIT_PUBLIC_PATH = 'public';
|
||||||
const ITEM_EDIT_DELETE_PATH = 'delete';
|
const ITEM_EDIT_DELETE_PATH = 'delete';
|
||||||
|
|
||||||
const ITEM_EDIT_MOVE_PATH = 'move';
|
const ITEM_EDIT_MOVE_PATH = 'move';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,29 +41,34 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
component: ItemStatusComponent,
|
component: ItemStatusComponent,
|
||||||
data: {title: 'item.edit.tabs.status.title'}
|
data: { title: 'item.edit.tabs.status.title' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bitstreams',
|
path: 'bitstreams',
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: {title: 'item.edit.tabs.bitstreams.title'}
|
data: { title: 'item.edit.tabs.bitstreams.title' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'metadata',
|
path: 'metadata',
|
||||||
component: ItemMetadataComponent,
|
component: ItemMetadataComponent,
|
||||||
data: {title: 'item.edit.tabs.metadata.title'}
|
data: { title: 'item.edit.tabs.metadata.title' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'relationships',
|
||||||
|
component: ItemRelationshipsComponent,
|
||||||
|
data: { title: 'item.edit.tabs.relationships.title' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'view',
|
path: 'view',
|
||||||
/* TODO - change when view page exists */
|
/* TODO - change when view page exists */
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: {title: 'item.edit.tabs.view.title'}
|
data: { title: 'item.edit.tabs.view.title' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'curate',
|
path: 'curate',
|
||||||
/* TODO - change when curate page exists */
|
/* TODO - change when curate page exists */
|
||||||
component: ItemBitstreamsComponent,
|
component: ItemBitstreamsComponent,
|
||||||
data: {title: 'item.edit.tabs.curate.title'}
|
data: { title: 'item.edit.tabs.curate.title' }
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -107,7 +110,7 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_MOVE_PATH,
|
path: ITEM_EDIT_MOVE_PATH,
|
||||||
component: ItemMoveComponent,
|
component: ItemMoveComponent,
|
||||||
data: {title: 'item.edit.move.title'},
|
data: { title: 'item.edit.move.title' },
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver
|
item: ItemPageResolver
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ import {
|
|||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../../shared/testing/utils';
|
} from '../../../shared/testing/utils';
|
||||||
|
|
||||||
let comp: ItemMetadataComponent;
|
let comp: any;
|
||||||
let fixture: ComponentFixture<ItemMetadataComponent>;
|
let fixture: ComponentFixture<ItemMetadataComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
let el: HTMLElement;
|
let el: HTMLElement;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
@@ -6,8 +6,6 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
FieldUpdate,
|
|
||||||
FieldUpdates,
|
|
||||||
Identifiable
|
Identifiable
|
||||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
@@ -19,6 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||||
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -29,28 +28,7 @@ import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
|||||||
/**
|
/**
|
||||||
* Component for displaying an item's metadata edit page
|
* Component for displaying an item's metadata edit page
|
||||||
*/
|
*/
|
||||||
export class ItemMetadataComponent implements OnInit {
|
export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||||
|
|
||||||
/**
|
|
||||||
* The item to display the edit page for
|
|
||||||
*/
|
|
||||||
item: Item;
|
|
||||||
/**
|
|
||||||
* The current values and updates for all this item's metadata fields
|
|
||||||
*/
|
|
||||||
updates$: Observable<FieldUpdates>;
|
|
||||||
/**
|
|
||||||
* The current url of this page
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* The time span for being able to undo discarding changes
|
|
||||||
*/
|
|
||||||
private discardTimeOut: number;
|
|
||||||
/**
|
|
||||||
* Prefix for this component's notification translate keys
|
|
||||||
*/
|
|
||||||
private notificationsPrefix = 'item.edit.metadata.notifications.';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable with a list of strings with all existing metadata field keys
|
* Observable with a list of strings with all existing metadata field keys
|
||||||
@@ -58,44 +36,38 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
metadataFields$: Observable<string[]>;
|
metadataFields$: Observable<string[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private itemService: ItemDataService,
|
protected itemService: ItemDataService,
|
||||||
private objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
private router: Router,
|
protected router: Router,
|
||||||
private notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
protected translateService: TranslateService,
|
||||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
private route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private metadataFieldService: RegistryService,
|
protected metadataFieldService: RegistryService,
|
||||||
) {
|
) {
|
||||||
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up and initialize all fields
|
* Set up and initialize all fields
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
this.metadataFields$ = this.findMetadataFields();
|
this.metadataFields$ = this.findMetadataFields();
|
||||||
this.route.parent.data.pipe(map((data) => data.item))
|
}
|
||||||
.pipe(
|
|
||||||
first(),
|
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
|
||||||
).subscribe((item: Item) => {
|
|
||||||
this.item = item;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.discardTimeOut = this.EnvConfig.item.edit.undoTimeout;
|
/**
|
||||||
this.url = this.router.url;
|
* Initialize the values and updates of the current item's metadata fields
|
||||||
if (this.url.indexOf('?') > 0) {
|
*/
|
||||||
this.url = this.url.substr(0, this.url.indexOf('?'));
|
public initializeUpdates(): void {
|
||||||
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
|
||||||
}
|
}
|
||||||
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
|
||||||
if (!hasChanges) {
|
/**
|
||||||
this.initializeOriginalFields();
|
* Initialize the prefix for notification messages
|
||||||
} else {
|
*/
|
||||||
this.checkLastModified();
|
public initializeNotificationsPrefix(): void {
|
||||||
}
|
this.notificationsPrefix = 'item.edit.metadata.notifications.';
|
||||||
});
|
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,47 +76,23 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
|
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
|
||||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
|
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the object updates service to discard all current changes to this item
|
|
||||||
* Shows a notification to remind the user that they can undo this
|
|
||||||
*/
|
|
||||||
discard() {
|
|
||||||
const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), { timeOut: this.discardTimeOut });
|
|
||||||
this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request the object updates service to undo discarding all changes to this item
|
|
||||||
*/
|
|
||||||
reinstate() {
|
|
||||||
this.objectUpdatesService.reinstateFieldUpdates(this.url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
private initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified);
|
this.objectUpdatesService.initialize(this.url, this.getMetadataAsListExcludingRelationships(), this.item.lastModified);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prevent unnecessary rerendering so fields don't lose focus
|
|
||||||
*/
|
|
||||||
trackUpdate(index, update: FieldUpdate) {
|
|
||||||
return update && update.field ? update.field.uuid : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests all current metadata for this item and requests the item service to update the item
|
* Requests all current metadata for this item and requests the item service to update the item
|
||||||
* Makes sure the new version of the item is rendered on the page
|
* Makes sure the new version of the item is rendered on the page
|
||||||
*/
|
*/
|
||||||
submit() {
|
public submit() {
|
||||||
this.isValid().pipe(first()).subscribe((isValid) => {
|
this.isValid().pipe(first()).subscribe((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable<MetadatumViewModel[]>;
|
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.getMetadataAsListExcludingRelationships()) as Observable<MetadatumViewModel[]>;
|
||||||
metadata$.pipe(
|
metadata$.pipe(
|
||||||
first(),
|
first(),
|
||||||
switchMap((metadata: MetadatumViewModel[]) => {
|
switchMap((metadata: MetadatumViewModel[]) => {
|
||||||
@@ -157,7 +105,7 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
(rd: RemoteData<Item>) => {
|
(rd: RemoteData<Item>) => {
|
||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
|
||||||
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -167,60 +115,6 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether or not there are currently updates for this item
|
|
||||||
*/
|
|
||||||
hasChanges(): Observable<boolean> {
|
|
||||||
return this.objectUpdatesService.hasUpdates(this.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether or not the item is currently reinstatable
|
|
||||||
*/
|
|
||||||
isReinstatable(): Observable<boolean> {
|
|
||||||
return this.objectUpdatesService.isReinstatable(this.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the current item is still in sync with the version in the store
|
|
||||||
* If it's not, a notification is shown and the changes are removed
|
|
||||||
*/
|
|
||||||
private checkLastModified() {
|
|
||||||
const currentVersion = this.item.lastModified;
|
|
||||||
this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe(
|
|
||||||
(updateVersion: Date) => {
|
|
||||||
if (updateVersion.getDate() !== currentVersion.getDate()) {
|
|
||||||
this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated'));
|
|
||||||
this.initializeOriginalFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current page is entirely valid
|
|
||||||
*/
|
|
||||||
private isValid() {
|
|
||||||
return this.objectUpdatesService.isValidPage(this.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get translated notification title
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
private getNotificationTitle(key: string) {
|
|
||||||
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get translated notification content
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
private getNotificationContent(key: string) {
|
|
||||||
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to request all metadata fields and convert them to a list of strings
|
* Method to request all metadata fields and convert them to a list of strings
|
||||||
*/
|
*/
|
||||||
@@ -230,4 +124,8 @@ export class ItemMetadataComponent implements OnInit {
|
|||||||
take(1),
|
take(1),
|
||||||
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMetadataAsListExcludingRelationships(): MetadatumViewModel[] {
|
||||||
|
return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
<ng-container *ngVar="(updates$ | async) as updates">
|
||||||
|
<div *ngIf="updates">
|
||||||
|
<h5>{{getRelationshipMessageKey(relationshipLabel) | translate}}</h5>
|
||||||
|
<ng-container *ngVar="(updates | dsObjectValues) as updateValues">
|
||||||
|
<div *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
|
ds-edit-relationship
|
||||||
|
class="relationship-row d-block"
|
||||||
|
[fieldUpdate]="updateValue || {}"
|
||||||
|
[url]="url"
|
||||||
|
[ngClass]="{'alert alert-danger': updateValue.changeType === 2}">
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@@ -0,0 +1,12 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.relationship-row:not(.alert-danger) {
|
||||||
|
padding: $alert-padding-y 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-row.alert-danger {
|
||||||
|
margin-left: -$alert-padding-x;
|
||||||
|
margin-right: -$alert-padding-x;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
@@ -0,0 +1,136 @@
|
|||||||
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { ResourceType } from '../../../../core/shared/resource-type';
|
||||||
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { SharedModule } from '../../../../shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
let comp: EditRelationshipListComponent;
|
||||||
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let objectUpdatesService;
|
||||||
|
let relationshipService;
|
||||||
|
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
|
let item;
|
||||||
|
let author1;
|
||||||
|
let author2;
|
||||||
|
let fieldUpdate1;
|
||||||
|
let fieldUpdate2;
|
||||||
|
let relationships;
|
||||||
|
let relationshipType;
|
||||||
|
|
||||||
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftLabel: 'isAuthorOfPublication',
|
||||||
|
rightLabel: 'isPublicationOfAuthor'
|
||||||
|
});
|
||||||
|
|
||||||
|
relationships = [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/2',
|
||||||
|
id: '2',
|
||||||
|
uuid: '2',
|
||||||
|
leftId: 'author1',
|
||||||
|
rightId: 'publication',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
}),
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/3',
|
||||||
|
id: '3',
|
||||||
|
uuid: '3',
|
||||||
|
leftId: 'author2',
|
||||||
|
rightId: 'publication',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/publication',
|
||||||
|
id: 'publication',
|
||||||
|
uuid: 'publication',
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
||||||
|
});
|
||||||
|
|
||||||
|
author1 = Object.assign(new Item(), {
|
||||||
|
id: 'author1',
|
||||||
|
uuid: 'author1'
|
||||||
|
});
|
||||||
|
author2 = Object.assign(new Item(), {
|
||||||
|
id: 'author2',
|
||||||
|
uuid: 'author2'
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldUpdate1 = {
|
||||||
|
field: author1,
|
||||||
|
changeType: undefined
|
||||||
|
};
|
||||||
|
fieldUpdate2 = {
|
||||||
|
field: author2,
|
||||||
|
changeType: FieldChangeType.REMOVE
|
||||||
|
};
|
||||||
|
|
||||||
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
|
{
|
||||||
|
getFieldUpdatesExclusive: observableOf({
|
||||||
|
[author1.uuid]: fieldUpdate1,
|
||||||
|
[author2.uuid]: fieldUpdate2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
|
{
|
||||||
|
getRelatedItemsByLabel: observableOf([author1, author2]),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [EditRelationshipListComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: RelationshipService, useValue: relationshipService }
|
||||||
|
], schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
comp.item = item;
|
||||||
|
comp.url = url;
|
||||||
|
comp.relationshipLabel = relationshipType.leftLabel;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('changeType is REMOVE', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should have class alert-danger', () => {
|
||||||
|
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
||||||
|
expect(element.classList).toContain('alert-danger');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,99 @@
|
|||||||
|
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-relationship-list',
|
||||||
|
styleUrls: ['./edit-relationship-list.component.scss'],
|
||||||
|
templateUrl: './edit-relationship-list.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component creating a list of editable relationships of a certain type
|
||||||
|
* The relationships are rendered as a list of related items
|
||||||
|
*/
|
||||||
|
export class EditRelationshipListComponent implements OnInit, OnChanges {
|
||||||
|
/**
|
||||||
|
* The item to display related items for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to the current page
|
||||||
|
* Used to fetch updates for the current item from the store
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label of the relationship-type we're rendering a list for
|
||||||
|
*/
|
||||||
|
@Input() relationshipLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FieldUpdates for the relationships in question
|
||||||
|
*/
|
||||||
|
updates$: Observable<FieldUpdates>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
protected relationshipService: RelationshipService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
this.initUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the FieldUpdates using the related items
|
||||||
|
*/
|
||||||
|
initUpdates() {
|
||||||
|
this.updates$ = this.getUpdatesByLabel(this.relationshipLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the item's relationships of a specific type into related items
|
||||||
|
* @param label The relationship type's label
|
||||||
|
*/
|
||||||
|
public getRelatedItemsByLabel(label: string): Observable<Item[]> {
|
||||||
|
return this.relationshipService.getRelatedItemsByLabel(this.item, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FieldUpdates for the relationships of a specific type
|
||||||
|
* @param label The relationship type's label
|
||||||
|
*/
|
||||||
|
public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
|
||||||
|
return this.getRelatedItemsByLabel(label).pipe(
|
||||||
|
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the i18n message key for a relationship
|
||||||
|
* @param label The relationship type's label
|
||||||
|
*/
|
||||||
|
public getRelationshipMessageKey(label: string): string {
|
||||||
|
if (hasValue(label) && label.indexOf('Of') > -1) {
|
||||||
|
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
|
*/
|
||||||
|
trackUpdate(index, update: FieldUpdate) {
|
||||||
|
return update && update.field ? update.field.uuid : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="row" *ngIf="item">
|
||||||
|
<div class="col-10 relationship">
|
||||||
|
<ds-item-type-switcher [object]="item" [viewMode]="viewMode"></ds-item-type-switcher>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<div class="btn-group relationship-action-buttons">
|
||||||
|
<button [disabled]="!canRemove()" (click)="remove()"
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
|
||||||
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button [disabled]="!canUndo()" (click)="undo()"
|
||||||
|
class="btn btn-outline-warning btn-sm"
|
||||||
|
title="{{'item.edit.metadata.edit.buttons.undo' | translate}}">
|
||||||
|
<i class="fas fa-undo-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,15 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.btn[disabled] {
|
||||||
|
color: $gray-600;
|
||||||
|
border-color: $gray-600;
|
||||||
|
z-index: 0; // prevent border colors jumping on hover
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-action-buttons {
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
@@ -0,0 +1,179 @@
|
|||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EditRelationshipComponent } from './edit-relationship.component';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { ResourceType } from '../../../../core/shared/resource-type';
|
||||||
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
|
|
||||||
|
let objectUpdatesService: ObjectUpdatesService;
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
|
let item;
|
||||||
|
let author1;
|
||||||
|
let author2;
|
||||||
|
let fieldUpdate1;
|
||||||
|
let fieldUpdate2;
|
||||||
|
let relationships;
|
||||||
|
let relationshipType;
|
||||||
|
|
||||||
|
let fixture;
|
||||||
|
let comp: EditRelationshipComponent;
|
||||||
|
let de;
|
||||||
|
let el;
|
||||||
|
|
||||||
|
describe('EditRelationshipComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftLabel: 'isAuthorOfPublication',
|
||||||
|
rightLabel: 'isPublicationOfAuthor'
|
||||||
|
});
|
||||||
|
|
||||||
|
relationships = [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/2',
|
||||||
|
id: '2',
|
||||||
|
uuid: '2',
|
||||||
|
leftId: 'author1',
|
||||||
|
rightId: 'publication',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
}),
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/3',
|
||||||
|
id: '3',
|
||||||
|
uuid: '3',
|
||||||
|
leftId: 'author2',
|
||||||
|
rightId: 'publication',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/publication',
|
||||||
|
id: 'publication',
|
||||||
|
uuid: 'publication',
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
||||||
|
});
|
||||||
|
|
||||||
|
author1 = Object.assign(new Item(), {
|
||||||
|
id: 'author1',
|
||||||
|
uuid: 'author1'
|
||||||
|
});
|
||||||
|
author2 = Object.assign(new Item(), {
|
||||||
|
id: 'author2',
|
||||||
|
uuid: 'author2'
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldUpdate1 = {
|
||||||
|
field: author1,
|
||||||
|
changeType: undefined
|
||||||
|
};
|
||||||
|
fieldUpdate2 = {
|
||||||
|
field: author2,
|
||||||
|
changeType: FieldChangeType.REMOVE
|
||||||
|
};
|
||||||
|
|
||||||
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
|
{
|
||||||
|
saveChangeFieldUpdate: {},
|
||||||
|
saveRemoveFieldUpdate: {},
|
||||||
|
setEditableFieldUpdate: {},
|
||||||
|
setValidFieldUpdate: {},
|
||||||
|
removeSingleFieldUpdate: {},
|
||||||
|
isEditable: observableOf(false), // should always return something --> its in ngOnInit
|
||||||
|
isValid: observableOf(true) // should always return something --> its in ngOnInit
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [EditRelationshipComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }
|
||||||
|
], schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditRelationshipComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
|
||||||
|
comp.url = url;
|
||||||
|
comp.fieldUpdate = fieldUpdate1;
|
||||||
|
comp.item = item;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when fieldUpdate has no changeType', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.fieldUpdate = fieldUpdate1;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canRemove', () => {
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(comp.canRemove()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canUndo', () => {
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(comp.canUndo()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when fieldUpdate has DELETE as changeType', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.fieldUpdate = fieldUpdate2;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canRemove', () => {
|
||||||
|
it('should return false', () => {
|
||||||
|
expect(comp.canRemove()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canUndo', () => {
|
||||||
|
it('should return true', () => {
|
||||||
|
expect(comp.canUndo()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call saveRemoveFieldUpdate with the correct arguments', () => {
|
||||||
|
expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('undo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call removeSingleFieldUpdate with the correct arguments', () => {
|
||||||
|
expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, item.uuid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,74 @@
|
|||||||
|
import { Component, Input, OnChanges } from '@angular/core';
|
||||||
|
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { ItemViewMode } from '../../../../shared/items/item-type-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
// tslint:disable-next-line:component-selector
|
||||||
|
selector: '[ds-edit-relationship]',
|
||||||
|
styleUrls: ['./edit-relationship.component.scss'],
|
||||||
|
templateUrl: './edit-relationship.component.html',
|
||||||
|
})
|
||||||
|
export class EditRelationshipComponent implements OnChanges {
|
||||||
|
/**
|
||||||
|
* The current field, value and state of the relationship
|
||||||
|
*/
|
||||||
|
@Input() fieldUpdate: FieldUpdate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current url of this page
|
||||||
|
*/
|
||||||
|
@Input() url: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The related item of this relationship
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view-mode we're currently on
|
||||||
|
*/
|
||||||
|
viewMode = ItemViewMode.Element;
|
||||||
|
|
||||||
|
constructor(private objectUpdatesService: ObjectUpdatesService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current relationship based on the fieldUpdate input field
|
||||||
|
*/
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.item = cloneDeep(this.fieldUpdate.field) as Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a new remove update for this field to the object updates service
|
||||||
|
*/
|
||||||
|
remove(): void {
|
||||||
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the current update for this field in the object updates service
|
||||||
|
*/
|
||||||
|
undo(): void {
|
||||||
|
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.item.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user should be allowed to remove this field
|
||||||
|
*/
|
||||||
|
canRemove(): boolean {
|
||||||
|
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user should be allowed to cancel the update to this field
|
||||||
|
*/
|
||||||
|
canUndo(): boolean {
|
||||||
|
return this.fieldUpdate.changeType >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="item-relationships">
|
||||||
|
<div class="button-row top d-flex">
|
||||||
|
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)"
|
||||||
|
[disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="discard()"><i
|
||||||
|
class="fas fa-times"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async"
|
||||||
|
(click)="reinstate()"><i
|
||||||
|
class="fas fa-undo-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="submit()"><i
|
||||||
|
class="fas fa-save"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div *ngFor="let label of relationLabels$ | async" class="mb-4">
|
||||||
|
<ds-edit-relationship-list [item]="item" [url]="url" [relationshipLabel]="label" ></ds-edit-relationship-list>
|
||||||
|
</div>
|
||||||
|
<div class="button-row bottom">
|
||||||
|
<div class="float-right">
|
||||||
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
|
[disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="discard()"><i
|
||||||
|
class="fas fa-times"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||||
|
(click)="reinstate()"><i
|
||||||
|
class="fas fa-undo-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||||
|
(click)="submit()"><i
|
||||||
|
class="fas fa-save"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,22 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
.btn {
|
||||||
|
margin-right: 0.5 * $spacer;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
|
||||||
|
min-width: $edit-item-button-min-width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top .btn {
|
||||||
|
margin-top: $spacer/2;
|
||||||
|
margin-bottom: $spacer/2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,233 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ItemRelationshipsComponent } from './item-relationships.component';
|
||||||
|
import { ChangeDetectorRef, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { INotification, Notification } from '../../../shared/notifications/models/notification.model';
|
||||||
|
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { SharedModule } from '../../../shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../../config';
|
||||||
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
|
||||||
|
let comp: any;
|
||||||
|
let fixture: ComponentFixture<ItemRelationshipsComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
let el: HTMLElement;
|
||||||
|
let objectUpdatesService;
|
||||||
|
let relationshipService;
|
||||||
|
let requestService;
|
||||||
|
let objectCache;
|
||||||
|
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
||||||
|
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||||
|
const successNotification: INotification = new Notification('id', NotificationType.Success, 'success');
|
||||||
|
const notificationsService = jasmine.createSpyObj('notificationsService',
|
||||||
|
{
|
||||||
|
info: infoNotification,
|
||||||
|
warning: warningNotification,
|
||||||
|
success: successNotification
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const router = new RouterStub();
|
||||||
|
let routeStub;
|
||||||
|
let itemService;
|
||||||
|
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
router.url = url;
|
||||||
|
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let item;
|
||||||
|
let author1;
|
||||||
|
let author2;
|
||||||
|
let fieldUpdate1;
|
||||||
|
let fieldUpdate2;
|
||||||
|
let relationships;
|
||||||
|
let relationshipType;
|
||||||
|
|
||||||
|
describe('ItemRelationshipsComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftLabel: 'isAuthorOfPublication',
|
||||||
|
rightLabel: 'isPublicationOfAuthor'
|
||||||
|
});
|
||||||
|
|
||||||
|
relationships = [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/2',
|
||||||
|
id: '2',
|
||||||
|
uuid: '2',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
}),
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/3',
|
||||||
|
id: '3',
|
||||||
|
uuid: '3',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
item = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/publication',
|
||||||
|
id: 'publication',
|
||||||
|
uuid: 'publication',
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))),
|
||||||
|
lastModified: date
|
||||||
|
});
|
||||||
|
|
||||||
|
author1 = Object.assign(new Item(), {
|
||||||
|
id: 'author1',
|
||||||
|
uuid: 'author1'
|
||||||
|
});
|
||||||
|
author2 = Object.assign(new Item(), {
|
||||||
|
id: 'author2',
|
||||||
|
uuid: 'author2'
|
||||||
|
});
|
||||||
|
|
||||||
|
relationships[0].leftItem = observableOf(new RemoteData(false, false, true, undefined, author1));
|
||||||
|
relationships[0].rightItem = observableOf(new RemoteData(false, false, true, undefined, item));
|
||||||
|
relationships[1].leftItem = observableOf(new RemoteData(false, false, true, undefined, author2));
|
||||||
|
relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item));
|
||||||
|
|
||||||
|
fieldUpdate1 = {
|
||||||
|
field: author1,
|
||||||
|
changeType: undefined
|
||||||
|
};
|
||||||
|
fieldUpdate2 = {
|
||||||
|
field: author2,
|
||||||
|
changeType: FieldChangeType.REMOVE
|
||||||
|
};
|
||||||
|
|
||||||
|
itemService = jasmine.createSpyObj('itemService', {
|
||||||
|
findById: observableOf(new RemoteData(false, false, true, undefined, item))
|
||||||
|
});
|
||||||
|
routeStub = {
|
||||||
|
parent: {
|
||||||
|
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
|
{
|
||||||
|
getFieldUpdates: observableOf({
|
||||||
|
[author1.uuid]: fieldUpdate1,
|
||||||
|
[author2.uuid]: fieldUpdate2
|
||||||
|
}),
|
||||||
|
getFieldUpdatesExclusive: observableOf({
|
||||||
|
[author1.uuid]: fieldUpdate1,
|
||||||
|
[author2.uuid]: fieldUpdate2
|
||||||
|
}),
|
||||||
|
saveAddFieldUpdate: {},
|
||||||
|
discardFieldUpdates: {},
|
||||||
|
reinstateFieldUpdates: observableOf(true),
|
||||||
|
initialize: {},
|
||||||
|
getUpdatedFields: observableOf([author1, author2]),
|
||||||
|
getLastModified: observableOf(date),
|
||||||
|
hasUpdates: observableOf(true),
|
||||||
|
isReinstatable: observableOf(false), // should always return something --> its in ngOnInit
|
||||||
|
isValidPage: observableOf(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
|
{
|
||||||
|
getItemRelationshipLabels: observableOf(['isAuthorOfPublication']),
|
||||||
|
getRelatedItems: observableOf([author1, author2]),
|
||||||
|
getRelatedItemsByLabel: observableOf([author1, author2]),
|
||||||
|
getItemRelationshipsArray: observableOf(relationships),
|
||||||
|
deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
|
getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService',
|
||||||
|
{
|
||||||
|
removeByHrefSubstring: {},
|
||||||
|
hasByHrefObservable: observableOf(false)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
|
remove: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [ItemRelationshipsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any },
|
||||||
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCache },
|
||||||
|
{ provide: RequestService, useValue: requestService },
|
||||||
|
ChangeDetectorRef
|
||||||
|
], schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemRelationshipsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
de = fixture.debugElement;
|
||||||
|
el = de.nativeElement;
|
||||||
|
comp.url = url;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('discard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.discard();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should call discardFieldUpdates on the objectUpdatesService with the correct url and notification', () => {
|
||||||
|
expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reinstate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.reinstate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should call reinstateFieldUpdates on the objectUpdatesService with the correct url', () => {
|
||||||
|
expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should delete the correct relationship', () => {
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,172 @@
|
|||||||
|
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
|
||||||
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||||
|
import { RelationshipService } from '../../../core/data/relationship.service';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
|
import { getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-relationships',
|
||||||
|
styleUrls: ['./item-relationships.component.scss'],
|
||||||
|
templateUrl: './item-relationships.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for displaying an item's relationships edit page
|
||||||
|
*/
|
||||||
|
export class ItemRelationshipsComponent extends AbstractItemUpdateComponent implements OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The labels of all different relations within this item
|
||||||
|
*/
|
||||||
|
relationLabels$: Observable<string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
||||||
|
* This is used to update the item in cache after relationships are deleted
|
||||||
|
*/
|
||||||
|
itemUpdateSubscription: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
|
protected router: Router,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected relationshipService: RelationshipService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected cdRef: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up and initialize all fields
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item);
|
||||||
|
this.initializeItemUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the item (and view) when it's removed in the request cache
|
||||||
|
*/
|
||||||
|
public initializeItemUpdate(): void {
|
||||||
|
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
||||||
|
filter((exists: boolean) => !exists),
|
||||||
|
switchMap(() => this.itemService.findById(this.item.uuid)),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
).subscribe((itemRD: RemoteData<Item>) => {
|
||||||
|
this.item = itemRD.payload;
|
||||||
|
this.cdRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the values and updates of the current item's relationship fields
|
||||||
|
*/
|
||||||
|
public initializeUpdates(): void {
|
||||||
|
this.updates$ = this.relationshipService.getRelatedItems(this.item).pipe(
|
||||||
|
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the prefix for notification messages
|
||||||
|
*/
|
||||||
|
public initializeNotificationsPrefix(): void {
|
||||||
|
this.notificationsPrefix = 'item.edit.relationships.notifications.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the currently selected related items back to relationships and send a delete request for each of the relationships found
|
||||||
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
|
*/
|
||||||
|
public submit(): void {
|
||||||
|
// Get all IDs of related items of which their relationship with the current item is about to be removed
|
||||||
|
const removedItemIds$ = this.relationshipService.getRelatedItems(this.item).pipe(
|
||||||
|
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items) as Observable<FieldUpdates>),
|
||||||
|
map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)),
|
||||||
|
map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field.uuid) as string[]),
|
||||||
|
isNotEmptyOperator()
|
||||||
|
);
|
||||||
|
// Get all the relationships that should be removed
|
||||||
|
const removedRelationships$ = removedItemIds$.pipe(
|
||||||
|
getRelationsByRelatedItemIds(this.item, this.relationshipService)
|
||||||
|
);
|
||||||
|
// Request a delete for every relationship found in the observable created above
|
||||||
|
removedRelationships$.pipe(
|
||||||
|
take(1),
|
||||||
|
map((removedRelationships: Relationship[]) => removedRelationships.map((rel: Relationship) => rel.id)),
|
||||||
|
switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid))))
|
||||||
|
).subscribe((responses: RestResponse[]) => {
|
||||||
|
this.displayNotifications(responses);
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display notifications
|
||||||
|
* - Error notification for each failed response with their message
|
||||||
|
* - Success notification in case there's at least one successful response
|
||||||
|
* @param responses
|
||||||
|
*/
|
||||||
|
displayNotifications(responses: RestResponse[]) {
|
||||||
|
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||||
|
const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||||
|
|
||||||
|
failedResponses.forEach((response: ErrorResponse) => {
|
||||||
|
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
|
||||||
|
});
|
||||||
|
if (successfulResponses.length > 0) {
|
||||||
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-initialize fields and subscriptions
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.initializeOriginalFields();
|
||||||
|
this.initializeUpdates();
|
||||||
|
this.initializeItemUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends all initial values of this item to the object updates service
|
||||||
|
*/
|
||||||
|
public initializeOriginalFields() {
|
||||||
|
this.relationshipService.getRelatedItems(this.item).pipe(take(1)).subscribe((items: Item[]) => {
|
||||||
|
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the item update when the component is destroyed
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.itemUpdateSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -62,7 +62,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
|||||||
GenericItemPageFieldComponent,
|
GenericItemPageFieldComponent,
|
||||||
RelatedEntitiesSearchComponent,
|
RelatedEntitiesSearchComponent,
|
||||||
RelatedItemsComponent,
|
RelatedItemsComponent,
|
||||||
MetadataRepresentationListComponent
|
MetadataRepresentationListComponent,
|
||||||
|
ItemPageTitleFieldComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
PublicationComponent
|
PublicationComponent
|
||||||
|
@@ -7,10 +7,12 @@ import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
|||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, flatMap, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { of as observableOf, zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
import { of as observableOf, zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator for comparing arrays using a mapping function
|
* Operator for comparing arrays using a mapping function
|
||||||
@@ -147,3 +149,17 @@ export const relationsToRepresentations = (parentId: string, itemType: string, m
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for fetching an item's relationships, but filtered by related item IDs (essentially performing a reverse lookup)
|
||||||
|
* Only relationships where leftItem or rightItem's ID is present in the list provided will be returned
|
||||||
|
* @param item
|
||||||
|
* @param relationshipService
|
||||||
|
*/
|
||||||
|
export const getRelationsByRelatedItemIds = (item: Item, relationshipService: RelationshipService) =>
|
||||||
|
(source: Observable<string[]>): Observable<Relationship[]> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((relatedItemIds: string[]) => relationshipService.getItemResolvedRelatedItemsAndRelationships(item).pipe(
|
||||||
|
map(([leftItems, rightItems, rels]) => rels.filter((rel: Relationship, index: number) => relatedItemIds.indexOf(leftItems[index].uuid) > -1 || relatedItemIds.indexOf(rightItems[index].uuid) > -1))
|
||||||
|
))
|
||||||
|
);
|
||||||
|
@@ -109,7 +109,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.searchOptions$ = this.getSearchOptions();
|
this.searchOptions$ = this.getSearchOptions();
|
||||||
this.sub = this.searchOptions$.pipe(
|
this.sub = this.searchOptions$.pipe(
|
||||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(observableOf(undefined)))))
|
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||||
.subscribe((results) => {
|
.subscribe((results) => {
|
||||||
this.resultsRD$.next(results);
|
this.resultsRD$.next(results);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { NavigationExtras, Router } from '@angular/router';
|
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
||||||
import { first, map, switchMap } from 'rxjs/operators';
|
import { first, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
@@ -103,11 +103,18 @@ export class SearchService implements OnDestroy {
|
|||||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||||
*/
|
*/
|
||||||
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||||
map((url: string) => {
|
map((url: string) => {
|
||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestObs = hrefObs.pipe(
|
||||||
|
map((url: string) => {
|
||||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||||
|
|
||||||
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||||
@@ -136,10 +143,11 @@ export class SearchService implements OnDestroy {
|
|||||||
map((sqr: SearchQueryResponse) => {
|
map((sqr: SearchQueryResponse) => {
|
||||||
return sqr.objects
|
return sqr.objects
|
||||||
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject))
|
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject))
|
||||||
.map((nsr: NormalizedSearchResult) => {
|
.map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), nsr.indexableObject))
|
||||||
return this.rdb.buildSingle(nsr.indexableObject);
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
|
// Send a request for each item to ensure fresh cache
|
||||||
|
tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))),
|
||||||
|
map((reqs: RestRequest[]) => reqs.map((req: RestRequest) => this.rdb.buildSingle(req.href))),
|
||||||
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -168,12 +176,21 @@ export class SearchService implements OnDestroy {
|
|||||||
|
|
||||||
const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
|
const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
|
||||||
map(([tDomainList, pageInfo]) => {
|
map(([tDomainList, pageInfo]) => {
|
||||||
return new PaginatedList(pageInfo, tDomainList);
|
return new PaginatedList(pageInfo, tDomainList.filter((obj) => hasValue(obj)));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return observableCombineLatest(hrefObs, tDomainListObs, requestEntryObs).pipe(
|
||||||
|
switchMap(([href, tDomainList, requestEntry]) => {
|
||||||
|
if (tDomainList.indexOf(undefined) > -1 && requestEntry && requestEntry.completed) {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
return this.search(searchOptions)
|
||||||
|
} else {
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the filter configuration for a given scope or the whole repository
|
* Request the filter configuration for a given scope or the whole repository
|
||||||
|
@@ -16,6 +16,12 @@ const COMMUNITY_MODULE_PATH = 'communities';
|
|||||||
export function getCommunityModulePath() {
|
export function getCommunityModulePath() {
|
||||||
return `/${COMMUNITY_MODULE_PATH}`;
|
return `/${COMMUNITY_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ADMIN_MODULE_PATH = 'admin';
|
||||||
|
export function getAdminModulePath() {
|
||||||
|
return `/${ADMIN_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
@@ -27,7 +33,7 @@ export function getCommunityModulePath() {
|
|||||||
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
|
||||||
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||||
|
@@ -44,6 +44,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { RouteService } from './shared/services/route.service';
|
import { RouteService } from './shared/services/route.service';
|
||||||
import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
||||||
import { MockRouter } from './shared/mocks/mock-router';
|
import { MockRouter } from './shared/mocks/mock-router';
|
||||||
|
import { CookieService } from './shared/services/cookie.service';
|
||||||
|
import { MockCookieService } from './shared/mocks/mock-cookie.service';
|
||||||
|
|
||||||
let comp: AppComponent;
|
let comp: AppComponent;
|
||||||
let fixture: ComponentFixture<AppComponent>;
|
let fixture: ComponentFixture<AppComponent>;
|
||||||
@@ -78,6 +80,7 @@ describe('App component', () => {
|
|||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
|
{ provide: CookieService, useValue: new MockCookieService()},
|
||||||
AppComponent,
|
AppComponent,
|
||||||
RouteService
|
RouteService
|
||||||
],
|
],
|
||||||
|
@@ -32,6 +32,11 @@ import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
|||||||
import { slideSidebarPadding } from './shared/animations/slide';
|
import { slideSidebarPadding } from './shared/animations/slide';
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
import { Theme } from '../config/theme.inferface';
|
import { Theme } from '../config/theme.inferface';
|
||||||
|
import { ClientCookieService } from './shared/services/client-cookie.service';
|
||||||
|
import { isNotEmpty } from './shared/empty.util';
|
||||||
|
import { CookieService } from './shared/services/cookie.service';
|
||||||
|
|
||||||
|
export const LANG_COOKIE = 'language_cookie';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -61,6 +66,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
|
private cookie: CookieService
|
||||||
) {
|
) {
|
||||||
// Load all the languages that are defined as active from the config file
|
// Load all the languages that are defined as active from the config file
|
||||||
translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
translate.addLangs(config.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
@@ -68,12 +74,21 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
// Load the default language from the config file
|
// Load the default language from the config file
|
||||||
translate.setDefaultLang(config.defaultLanguage);
|
translate.setDefaultLang(config.defaultLanguage);
|
||||||
|
|
||||||
|
// Attempt to get the language from a cookie
|
||||||
|
const lang = cookie.get(LANG_COOKIE);
|
||||||
|
if (isNotEmpty(lang)) {
|
||||||
|
// Cookie found
|
||||||
|
// Use the language from the cookie
|
||||||
|
translate.use(lang);
|
||||||
|
} else {
|
||||||
|
// Cookie not found
|
||||||
// Attempt to get the browser language from the user
|
// Attempt to get the browser language from the user
|
||||||
if (translate.getLangs().includes(translate.getBrowserLang())) {
|
if (translate.getLangs().includes(translate.getBrowserLang())) {
|
||||||
translate.use(translate.getBrowserLang());
|
translate.use(translate.getBrowserLang());
|
||||||
} else {
|
} else {
|
||||||
translate.use(config.defaultLanguage);
|
translate.use(config.defaultLanguage);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
metadata.listenForRouteChange();
|
metadata.listenForRouteChange();
|
||||||
|
|
||||||
|
@@ -39,6 +39,7 @@ import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/e
|
|||||||
import { NavbarModule } from './navbar/navbar.module';
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
|
||||||
|
import { ClientCookieService } from './shared/services/client-cookie.service';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return ENV_CONFIG;
|
return ENV_CONFIG;
|
||||||
@@ -97,7 +98,8 @@ const PROVIDERS = [
|
|||||||
{
|
{
|
||||||
provide: RouterStateSerializer,
|
provide: RouterStateSerializer,
|
||||||
useClass: DSpaceRouterStateSerializer
|
useClass: DSpaceRouterStateSerializer
|
||||||
}
|
},
|
||||||
|
ClientCookieService
|
||||||
];
|
];
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
|
@@ -23,6 +23,10 @@ import { hasValue } from './shared/empty.util';
|
|||||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||||
import { historyReducer, HistoryState } from './shared/history/history.reducer';
|
import { historyReducer, HistoryState } from './shared/history/history.reducer';
|
||||||
|
import {
|
||||||
|
bitstreamFormatReducer,
|
||||||
|
BitstreamFormatRegistryState
|
||||||
|
} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
@@ -30,6 +34,7 @@ export interface AppState {
|
|||||||
hostWindow: HostWindowState;
|
hostWindow: HostWindowState;
|
||||||
forms: FormState;
|
forms: FormState;
|
||||||
metadataRegistry: MetadataRegistryState;
|
metadataRegistry: MetadataRegistryState;
|
||||||
|
bitstreamFormats: BitstreamFormatRegistryState;
|
||||||
notifications: NotificationsState;
|
notifications: NotificationsState;
|
||||||
searchSidebar: SearchSidebarState;
|
searchSidebar: SearchSidebarState;
|
||||||
searchFilter: SearchFiltersState;
|
searchFilter: SearchFiltersState;
|
||||||
@@ -44,6 +49,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
hostWindow: hostWindowReducer,
|
hostWindow: hostWindowReducer,
|
||||||
forms: formReducer,
|
forms: formReducer,
|
||||||
metadataRegistry: metadataRegistryReducer,
|
metadataRegistry: metadataRegistryReducer,
|
||||||
|
bitstreamFormats: bitstreamFormatReducer,
|
||||||
notifications: notificationsReducer,
|
notifications: notificationsReducer,
|
||||||
searchSidebar: sidebarReducer,
|
searchSidebar: sidebarReducer,
|
||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
|
@@ -4,7 +4,7 @@ import { BitstreamFormat } from '../../shared/bitstream-format.model';
|
|||||||
import { mapsTo } from '../builders/build-decorators';
|
import { mapsTo } from '../builders/build-decorators';
|
||||||
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
|
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
|
||||||
import { NormalizedObject } from './normalized-object.model';
|
import { NormalizedObject } from './normalized-object.model';
|
||||||
import { SupportLevel } from './support-level.model';
|
import { BitstreamFormatSupportLevel } from '../../shared/bitstream-format-support-level';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalized model class for a Bitstream Format
|
* Normalized model class for a Bitstream Format
|
||||||
@@ -34,7 +34,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject<BitstreamFormat>
|
|||||||
* The level of support the system offers for this Bitstream Format
|
* The level of support the system offers for this Bitstream Format
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
supportLevel: SupportLevel;
|
supportLevel: BitstreamFormatSupportLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the Bitstream Format is used to store system information, rather than the content of items in the system
|
* True if the Bitstream Format is used to store system information, rather than the content of items in the system
|
||||||
@@ -46,7 +46,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject<BitstreamFormat>
|
|||||||
* String representing this Bitstream Format's file extension
|
* String representing this Bitstream Format's file extension
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
extensions: string;
|
extensions: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifier for this Bitstream Format
|
* Identifier for this Bitstream Format
|
||||||
|
18
src/app/core/cache/object-cache.service.ts
vendored
18
src/app/core/cache/object-cache.service.ts
vendored
@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
@@ -68,8 +68,8 @@ export class ObjectCacheService {
|
|||||||
* @param href
|
* @param href
|
||||||
* The unique href of the object to be removed
|
* The unique href of the object to be removed
|
||||||
*/
|
*/
|
||||||
remove(uuid: string): void {
|
remove(href: string): void {
|
||||||
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
|
this.store.dispatch(new RemoveFromObjectCacheAction(href));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -224,6 +224,18 @@ export class ObjectCacheService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable that emits a new value whenever the availability of the cached object changes.
|
||||||
|
* The value it emits is a boolean stating if the object exists in cache or not.
|
||||||
|
* @param selfLink The self link of the object to observe
|
||||||
|
*/
|
||||||
|
hasBySelfLinkObservable(selfLink: string): Observable<boolean> {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(entryFromSelfLinkSelector(selfLink)),
|
||||||
|
map((entry: ObjectCacheEntry) => this.isValid(entry))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether an ObjectCacheEntry should still be cached
|
* Check whether an ObjectCacheEntry should still be cached
|
||||||
*
|
*
|
||||||
|
@@ -101,12 +101,14 @@ import { NormalizedSubmissionFormsModel } from './config/models/normalized-confi
|
|||||||
import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model';
|
import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model';
|
||||||
import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model';
|
import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model';
|
||||||
import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model';
|
import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model';
|
||||||
|
import { RelationshipService } from './data/relationship.service';
|
||||||
import { RoleService } from './roles/role.service';
|
import { RoleService } from './roles/role.service';
|
||||||
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
|
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
|
||||||
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
|
||||||
import { ClaimedTaskDataService } from './tasks/claimed-task-data.service';
|
import { ClaimedTaskDataService } from './tasks/claimed-task-data.service';
|
||||||
import { PoolTaskDataService } from './tasks/pool-task-data.service';
|
import { PoolTaskDataService } from './tasks/pool-task-data.service';
|
||||||
import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
|
||||||
|
import { BitstreamFormatDataService } from './data/bitstream-format-data.service';
|
||||||
import { NormalizedClaimedTask } from './tasks/models/normalized-claimed-task-object.model';
|
import { NormalizedClaimedTask } from './tasks/models/normalized-claimed-task-object.model';
|
||||||
import { NormalizedTaskObject } from './tasks/models/normalized-task-object.model';
|
import { NormalizedTaskObject } from './tasks/models/normalized-task-object.model';
|
||||||
import { NormalizedPoolTask } from './tasks/models/normalized-pool-task-object.model';
|
import { NormalizedPoolTask } from './tasks/models/normalized-pool-task-object.model';
|
||||||
@@ -153,6 +155,7 @@ const PROVIDERS = [
|
|||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
ResourcePolicyService,
|
ResourcePolicyService,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
|
BitstreamFormatDataService,
|
||||||
NormalizedObjectBuildService,
|
NormalizedObjectBuildService,
|
||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
RequestService,
|
RequestService,
|
||||||
@@ -198,6 +201,7 @@ const PROVIDERS = [
|
|||||||
MenuService,
|
MenuService,
|
||||||
ObjectUpdatesService,
|
ObjectUpdatesService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
RelationshipService,
|
||||||
MyDSpaceGuard,
|
MyDSpaceGuard,
|
||||||
RoleService,
|
RoleService,
|
||||||
TaskResponseParsingService,
|
TaskResponseParsingService,
|
||||||
|
293
src/app/core/data/bitstream-format-data.service.spec.ts
Normal file
293
src/app/core/data/bitstream-format-data.service.spec.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { Action, Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
|
import { async } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BitstreamFormatsRegistryDeselectAction,
|
||||||
|
BitstreamFormatsRegistryDeselectAllAction,
|
||||||
|
BitstreamFormatsRegistrySelectAction
|
||||||
|
} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
describe('BitstreamFormatDataService', () => {
|
||||||
|
let service: BitstreamFormatDataService;
|
||||||
|
let requestService;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
|
||||||
|
const bitstreamFormatsEndpoint = 'https://rest.api/core/bitstream-formats';
|
||||||
|
const bitstreamFormatsIdEndpoint = 'https://rest.api/core/bitstream-formats/format-id';
|
||||||
|
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||||
|
responseCacheEntry.completed = true;
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
dispatch(action: Action) {
|
||||||
|
// Do Nothing
|
||||||
|
}
|
||||||
|
} as Store<CoreState>;
|
||||||
|
|
||||||
|
const objectCache = {} as ObjectCacheService;
|
||||||
|
const halEndpointService = {
|
||||||
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
|
return cold('a', {a: bitstreamFormatsEndpoint});
|
||||||
|
}
|
||||||
|
} as HALEndpointService;
|
||||||
|
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {} as any;
|
||||||
|
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||||
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
|
|
||||||
|
function initTestService(halService) {
|
||||||
|
return new BitstreamFormatDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
dataBuildService,
|
||||||
|
store,
|
||||||
|
objectCache,
|
||||||
|
halService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getBrowseEndpoint', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
}));
|
||||||
|
it('should get the browse endpoint', () => {
|
||||||
|
const result = service.getBrowseEndpoint();
|
||||||
|
const expected = cold('b', {b: bitstreamFormatsEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUpdateEndpoint', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
}));
|
||||||
|
it('should get the update endpoint', () => {
|
||||||
|
const formatId = 'format-id';
|
||||||
|
|
||||||
|
const result = service.getUpdateEndpoint(formatId);
|
||||||
|
const expected = cold('b', {b: bitstreamFormatsIdEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCreateEndpoint', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
}));
|
||||||
|
it('should get the create endpoint ', () => {
|
||||||
|
|
||||||
|
const result = service.getCreateEndpoint();
|
||||||
|
const expected = cold('b', {b: bitstreamFormatsEndpoint});
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateBitstreamFormat', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
}));
|
||||||
|
it('should update the bitstream format', () => {
|
||||||
|
const updatedBistreamFormat = new BitstreamFormat();
|
||||||
|
updatedBistreamFormat.uuid = 'updated-uuid';
|
||||||
|
|
||||||
|
const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')});
|
||||||
|
const result = service.updateBitstreamFormat(updatedBistreamFormat);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createBitstreamFormat', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
}));
|
||||||
|
it('should create a new bitstream format', () => {
|
||||||
|
const newFormat = new BitstreamFormat();
|
||||||
|
newFormat.uuid = 'new-uuid';
|
||||||
|
|
||||||
|
const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')});
|
||||||
|
const result = service.createBitstreamFormat(newFormat);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearBitStreamFormatRequests', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
const halService = {
|
||||||
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
|
return observableOf(bitstreamFormatsEndpoint);
|
||||||
|
}
|
||||||
|
} as HALEndpointService;
|
||||||
|
service = initTestService(halService);
|
||||||
|
service.clearBitStreamFormatRequests().subscribe();
|
||||||
|
}));
|
||||||
|
it('should remove the bitstream format hrefs in the request service', () => {
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(bitstreamFormatsEndpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectBitstreamFormat', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
}));
|
||||||
|
it('should add a selected bitstream to the store', () => {
|
||||||
|
const format = new BitstreamFormat();
|
||||||
|
format.uuid = 'uuid';
|
||||||
|
|
||||||
|
service.selectBitstreamFormat(format);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistrySelectAction(format));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deselectBitstreamFormat', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
}));
|
||||||
|
it('should remove a bitstream from the store', () => {
|
||||||
|
const format = new BitstreamFormat();
|
||||||
|
format.uuid = 'uuid';
|
||||||
|
|
||||||
|
service.deselectBitstreamFormat(format);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAction(format));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deselectAllBitstreamFormats', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: cold('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
service = initTestService(halEndpointService);
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
|
||||||
|
}));
|
||||||
|
it('should remove all bitstreamFormats from the store', () => {
|
||||||
|
service.deselectAllBitstreamFormats();
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAllAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
configure: {},
|
||||||
|
getByHref: observableOf(responseCacheEntry),
|
||||||
|
getByUUID: hot('a', {a: responseCacheEntry}),
|
||||||
|
generateRequestId: 'request-id',
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
const halService = {
|
||||||
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
|
return observableOf(bitstreamFormatsEndpoint);
|
||||||
|
}
|
||||||
|
} as HALEndpointService;
|
||||||
|
service = initTestService(halService);
|
||||||
|
}));
|
||||||
|
it('should delete a bitstream format', () => {
|
||||||
|
const format = new BitstreamFormat();
|
||||||
|
format.uuid = 'format-uuid';
|
||||||
|
format.id = 'format-id';
|
||||||
|
|
||||||
|
const expected = cold('(b|)', {b: true});
|
||||||
|
const result = service.delete(format);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
183
src/app/core/data/bitstream-format-data.service.ts
Normal file
183
src/app/core/data/bitstream-format-data.service.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { DeleteByIDRequest, FindAllOptions, PostRequest, PutRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { find, map, tap } from 'rxjs/operators';
|
||||||
|
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||||
|
import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
|
import {
|
||||||
|
BitstreamFormatsRegistryDeselectAction,
|
||||||
|
BitstreamFormatsRegistryDeselectAllAction,
|
||||||
|
BitstreamFormatsRegistrySelectAction
|
||||||
|
} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
|
||||||
|
const bitstreamFormatsStateSelector = (state: AppState) => state.bitstreamFormats;
|
||||||
|
const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector,
|
||||||
|
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
||||||
|
|
||||||
|
protected linkPath = 'bitstreamformats';
|
||||||
|
protected forceBypassCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<BitstreamFormat>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for browsing bitstream formats
|
||||||
|
* @param {FindAllOptions} options
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to update an existing bitstream format
|
||||||
|
* @param formatId
|
||||||
|
*/
|
||||||
|
public getUpdateEndpoint(formatId: string): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, formatId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint to create a new bitstream format
|
||||||
|
*/
|
||||||
|
public getCreateEndpoint(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing bitstreamFormat
|
||||||
|
* @param bitstreamFormat
|
||||||
|
*/
|
||||||
|
updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
this.getUpdateEndpoint(bitstreamFormat.id).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) =>
|
||||||
|
new PutRequest(requestId, endpointURL, bitstreamFormat)),
|
||||||
|
configureRequest(this.requestService)).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new BitstreamFormat
|
||||||
|
* @param BitstreamFormat
|
||||||
|
*/
|
||||||
|
public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
this.getCreateEndpoint().pipe(
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
return new PostRequest(requestId, endpointURL, bitstreamFormat);
|
||||||
|
}),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the cache of the list of BitstreamFormats
|
||||||
|
*/
|
||||||
|
public clearBitStreamFormatRequests(): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the selected BitstreamFormats from the store
|
||||||
|
*/
|
||||||
|
public getSelectedBitstreamFormats(): Observable<BitstreamFormat[]> {
|
||||||
|
return this.store.pipe(select(selectedBitstreamFormatSelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a BistreamFormat to the selected BitstreamFormats in the store
|
||||||
|
* @param bitstreamFormat
|
||||||
|
*/
|
||||||
|
public selectBitstreamFormat(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.store.dispatch(new BitstreamFormatsRegistrySelectAction(bitstreamFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a BistreamFormat from the list of selected BitstreamFormats in the store
|
||||||
|
* @param bitstreamFormat
|
||||||
|
*/
|
||||||
|
public deselectBitstreamFormat(bitstreamFormat: BitstreamFormat) {
|
||||||
|
this.store.dispatch(new BitstreamFormatsRegistryDeselectAction(bitstreamFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all BitstreamFormats from the list of selected BitstreamFormats in the store
|
||||||
|
*/
|
||||||
|
public deselectAllBitstreamFormats() {
|
||||||
|
this.store.dispatch(new BitstreamFormatsRegistryDeselectAllAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing DSpace Object on the server
|
||||||
|
* @param format The DSpace Object to be removed
|
||||||
|
* Return an observable that emits true when the deletion was successful, false when it failed
|
||||||
|
*/
|
||||||
|
delete(format: BitstreamFormat): Observable<boolean> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, format.id)));
|
||||||
|
|
||||||
|
hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new DeleteByIDRequest(requestId, href, format.id);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.requestService.getByUUID(requestId).pipe(
|
||||||
|
find((request: RequestEntry) => request.completed),
|
||||||
|
map((request: RequestEntry) => request.response.isSuccessful)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -105,6 +105,27 @@ export class ObjectUpdatesService {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that combines the state's updates (excluding updates that aren't part of the initialFields) with
|
||||||
|
* the initial values (when there's no update) to create a FieldUpdates object
|
||||||
|
* @param url The URL of the page for which the FieldUpdates should be requested
|
||||||
|
* @param initialFields The initial values of the fields
|
||||||
|
*/
|
||||||
|
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||||
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
|
return objectUpdates.pipe(map((objectEntry) => {
|
||||||
|
const fieldUpdates: FieldUpdates = {};
|
||||||
|
for (const object of initialFields) {
|
||||||
|
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
|
||||||
|
if (isEmpty(fieldUpdate)) {
|
||||||
|
fieldUpdate = { field: object, changeType: undefined };
|
||||||
|
}
|
||||||
|
fieldUpdates[object.uuid] = fieldUpdate;
|
||||||
|
}
|
||||||
|
return fieldUpdates;
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to check if a specific field is currently editable in the store
|
* Method to check if a specific field is currently editable in the store
|
||||||
* @param url The URL of the page on which the field resides
|
* @param url The URL of the page on which the field resides
|
||||||
|
157
src/app/core/data/relationship.service.spec.ts
Normal file
157
src/app/core/data/relationship.service.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { RelationshipService } from './relationship.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
|
import { ResourceType } from '../shared/resource-type';
|
||||||
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { DeleteRequest } from './request.models';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
|
describe('RelationshipService', () => {
|
||||||
|
let service: RelationshipService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
const restEndpointURL = 'https://rest.api/';
|
||||||
|
const relationshipsEndpointURL = `${restEndpointURL}/relationships`;
|
||||||
|
const halService: any = new HALEndpointServiceStub(restEndpointURL);
|
||||||
|
const rdbService = getMockRemoteDataBuildService();
|
||||||
|
const objectCache = Object.assign({
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
remove: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
}) as ObjectCacheService;
|
||||||
|
|
||||||
|
const relationshipType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
leftLabel: 'isAuthorOfPublication',
|
||||||
|
rightLabel: 'isPublicationOfAuthor'
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationship1 = Object.assign(new Relationship(), {
|
||||||
|
self: relationshipsEndpointURL + '/2',
|
||||||
|
id: '2',
|
||||||
|
uuid: '2',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
});
|
||||||
|
const relationship2 = Object.assign(new Relationship(), {
|
||||||
|
self: relationshipsEndpointURL + '/3',
|
||||||
|
id: '3',
|
||||||
|
uuid: '3',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationships = [ relationship1, relationship2 ];
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), {
|
||||||
|
self: 'fake-item-url/publication',
|
||||||
|
id: 'publication',
|
||||||
|
uuid: 'publication',
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
||||||
|
});
|
||||||
|
|
||||||
|
const relatedItem1 = Object.assign(new Item(), {
|
||||||
|
id: 'author1',
|
||||||
|
uuid: 'author1'
|
||||||
|
});
|
||||||
|
const relatedItem2 = Object.assign(new Item(), {
|
||||||
|
id: 'author2',
|
||||||
|
uuid: 'author2'
|
||||||
|
});
|
||||||
|
relationship1.leftItem = getRemotedataObservable(relatedItem1);
|
||||||
|
relationship1.rightItem = getRemotedataObservable(item);
|
||||||
|
relationship2.leftItem = getRemotedataObservable(relatedItem2);
|
||||||
|
relationship2.rightItem = getRemotedataObservable(item);
|
||||||
|
const relatedItems = [relatedItem1, relatedItem2];
|
||||||
|
|
||||||
|
const itemService = jasmine.createSpyObj('itemService', {
|
||||||
|
findById: (uuid) => new RemoteData(false, false, true, undefined, relatedItems.filter((relatedItem) => relatedItem.id === uuid)[0])
|
||||||
|
});
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new RelationshipService(
|
||||||
|
requestService,
|
||||||
|
halService,
|
||||||
|
rdbService,
|
||||||
|
itemService,
|
||||||
|
objectCache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
|
return observableOf({
|
||||||
|
response: { isSuccessful: successful, payload: relationships } as any
|
||||||
|
} as RequestEntry)
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteRelationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1));
|
||||||
|
spyOn(objectCache, 'remove');
|
||||||
|
service.deleteRelationship(relationships[0].uuid).subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a DeleteRequest', () => {
|
||||||
|
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid);
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear the related items their cache', () => {
|
||||||
|
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
|
||||||
|
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getItemRelationshipsArray', () => {
|
||||||
|
it('should return the item\'s relationships in the form of an array', () => {
|
||||||
|
service.getItemRelationshipsArray(item).subscribe((result) => {
|
||||||
|
expect(result).toEqual(relationships);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getItemRelationshipLabels', () => {
|
||||||
|
it('should return the correct labels', () => {
|
||||||
|
service.getItemRelationshipLabels(item).subscribe((result) => {
|
||||||
|
expect(result).toEqual([relationshipType.rightLabel]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRelatedItems', () => {
|
||||||
|
it('should return the related items', () => {
|
||||||
|
service.getRelatedItems(item).subscribe((result) => {
|
||||||
|
expect(result).toEqual(relatedItems);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRelatedItemsByLabel', () => {
|
||||||
|
it('should return the related items by label', () => {
|
||||||
|
service.getRelatedItemsByLabel(item, relationshipType.rightLabel).subscribe((result) => {
|
||||||
|
expect(result).toEqual(relatedItems);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||||
|
return observableOf(new RemoteData(false, false, true, undefined, obj));
|
||||||
|
}
|
235
src/app/core/data/relationship.service.ts
Normal file
235
src/app/core/data/relationship.service.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
|
import { distinctUntilChanged, filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses,
|
||||||
|
getRemoteDataPayload, getResponseFromEntry,
|
||||||
|
getSucceededRemoteData
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { DeleteRequest, RestRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||||
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
import { zip as observableZip } from 'rxjs';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { ItemDataService } from './item-data.service';
|
||||||
|
import {
|
||||||
|
compareArraysUsingIds, filterRelationsByTypeLabel,
|
||||||
|
relationsToItems
|
||||||
|
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service handling all relationship requests
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RelationshipService {
|
||||||
|
protected linkPath = 'relationships';
|
||||||
|
|
||||||
|
constructor(protected requestService: RequestService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected objectCache: ObjectCacheService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for a relationship by ID
|
||||||
|
* @param uuid
|
||||||
|
*/
|
||||||
|
getRelationshipEndpoint(uuid: string) {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((href: string) => `${href}/${uuid}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a relationship by its UUID
|
||||||
|
* @param uuid
|
||||||
|
*/
|
||||||
|
findById(uuid: string): Observable<RemoteData<Relationship>> {
|
||||||
|
const href$ = this.getRelationshipEndpoint(uuid);
|
||||||
|
return this.rdbService.buildSingle<Relationship>(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a delete request for a relationship by ID
|
||||||
|
* @param uuid
|
||||||
|
*/
|
||||||
|
deleteRelationship(uuid: string): Observable<RestResponse> {
|
||||||
|
return this.getRelationshipEndpoint(uuid).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||||
|
getResponseFromEntry(),
|
||||||
|
tap(() => this.clearRelatedCache(uuid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types
|
||||||
|
* This is used for easier access of a relationship's type because they exist as observables
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.getItemRelationshipsArray(item),
|
||||||
|
this.getItemRelationshipTypesArray(item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships their types
|
||||||
|
* This is used for easier access of a relationship's type and left and right items because they exist as observables
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemResolvedRelatedItemsAndTypes(item: Item): Observable<[Item[], Item[], RelationshipType[]]> {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.getItemLeftRelatedItemArray(item),
|
||||||
|
this.getItemRightRelatedItemArray(item),
|
||||||
|
this.getItemRelationshipTypesArray(item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a combined observable containing an array of all the item's relationship's left- and right-side items, as well as an array of the relationships themselves
|
||||||
|
* This is used for easier access of the relationship and their left and right items because they exist as observables
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemResolvedRelatedItemsAndRelationships(item: Item): Observable<[Item[], Item[], Relationship[]]> {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.getItemLeftRelatedItemArray(item),
|
||||||
|
this.getItemRightRelatedItemArray(item),
|
||||||
|
this.getItemRelationshipsArray(item)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an item their relationships in the form of an array
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemRelationshipsArray(item: Item): Observable<Relationship[]> {
|
||||||
|
return item.relationships.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((rels: PaginatedList<Relationship>) => rels.page),
|
||||||
|
hasValueOperator(),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an item their relationship types in the form of an array
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemRelationshipTypesArray(item: Item): Observable<RelationshipType[]> {
|
||||||
|
return this.getItemRelationshipsArray(item).pipe(
|
||||||
|
flatMap((rels: Relationship[]) =>
|
||||||
|
observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe(
|
||||||
|
map(([...arr]: Array<RemoteData<RelationshipType>>) => arr.map((d: RemoteData<RelationshipType>) => d.payload).filter((type) => hasValue(type))),
|
||||||
|
filter((arr) => arr.length === rels.length)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an item his relationship's left-side related items in the form of an array
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemLeftRelatedItemArray(item: Item): Observable<Item[]> {
|
||||||
|
return this.getItemRelationshipsArray(item).pipe(
|
||||||
|
flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.leftItem)).pipe(
|
||||||
|
map(([...arr]: Array<RemoteData<Item>>) => arr.map((rd: RemoteData<Item>) => rd.payload).filter((i) => hasValue(i))),
|
||||||
|
filter((arr) => arr.length === rels.length)
|
||||||
|
)),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an item his relationship's right-side related items in the form of an array
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemRightRelatedItemArray(item: Item): Observable<Item[]> {
|
||||||
|
return this.getItemRelationshipsArray(item).pipe(
|
||||||
|
flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.rightItem)).pipe(
|
||||||
|
map(([...arr]: Array<RemoteData<Item>>) => arr.map((rd: RemoteData<Item>) => rd.payload).filter((i) => hasValue(i))),
|
||||||
|
filter((arr) => arr.length === rels.length)
|
||||||
|
)),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of an item their unique relationship type's labels
|
||||||
|
* The array doesn't contain any duplicate labels
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getItemRelationshipLabels(item: Item): Observable<string[]> {
|
||||||
|
return this.getItemResolvedRelatedItemsAndTypes(item).pipe(
|
||||||
|
map(([leftItems, rightItems, relTypesCurrentPage]) => {
|
||||||
|
return relTypesCurrentPage.map((type, index) => {
|
||||||
|
if (leftItems[index].uuid === item.uuid) {
|
||||||
|
return type.leftLabel;
|
||||||
|
} else {
|
||||||
|
return type.rightLabel;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
map((labels: string[]) => Array.from(new Set(labels)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a given item's relationships into related items and return the items as an array
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getRelatedItems(item: Item): Observable<Item[]> {
|
||||||
|
return this.getItemRelationshipsArray(item).pipe(
|
||||||
|
relationsToItems(item.uuid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a given item's relationships into related items, filtered by a relationship label
|
||||||
|
* and return the items as an array
|
||||||
|
* @param item
|
||||||
|
* @param label
|
||||||
|
*/
|
||||||
|
getRelatedItemsByLabel(item: Item, label: string): Observable<Item[]> {
|
||||||
|
return this.getItemResolvedRelsAndTypes(item).pipe(
|
||||||
|
filterRelationsByTypeLabel(label),
|
||||||
|
relationsToItems(item.uuid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear object and request caches of the items related to a relationship (left and right items)
|
||||||
|
* @param uuid
|
||||||
|
*/
|
||||||
|
clearRelatedCache(uuid: string) {
|
||||||
|
this.findById(uuid).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
flatMap((rd: RemoteData<Relationship>) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))),
|
||||||
|
take(1)
|
||||||
|
).subscribe(([leftItem, rightItem]) => {
|
||||||
|
this.objectCache.remove(leftItem.payload.self);
|
||||||
|
this.objectCache.remove(rightItem.payload.self);
|
||||||
|
this.requestService.removeByHrefSubstring(leftItem.payload.self);
|
||||||
|
this.requestService.removeByHrefSubstring(rightItem.payload.self);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -65,8 +65,7 @@ const uuidsFromHrefSubstringSelector =
|
|||||||
const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
|
const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
|
||||||
let result = [];
|
let result = [];
|
||||||
if (isNotEmpty(state)) {
|
if (isNotEmpty(state)) {
|
||||||
result = Object.values(state)
|
result = Object.keys(state).filter((key) => key.startsWith(href)).map((key) => state[key]);
|
||||||
.filter((value: string) => value.startsWith(href));
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -315,4 +314,15 @@ export class RequestService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable that emits a new value whenever the availability of the cached request changes.
|
||||||
|
* The value it emits is a boolean stating if the request exists in cache or not.
|
||||||
|
* @param href The href of the request to observe
|
||||||
|
*/
|
||||||
|
hasByHrefObservable(href: string): Observable<boolean> {
|
||||||
|
return this.getByHref(href).pipe(
|
||||||
|
map((requestEntry: RequestEntry) => this.isValid(requestEntry))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
export class BitstreamFormat {
|
|
||||||
shortDescription: string;
|
|
||||||
description: string;
|
|
||||||
mimetype: string;
|
|
||||||
supportLevel: number;
|
|
||||||
internal: boolean;
|
|
||||||
extensions: string;
|
|
||||||
}
|
|
@@ -12,7 +12,6 @@ import { PageInfo } from '../shared/page-info.model';
|
|||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RegistryBitstreamformatsSuccessResponse,
|
|
||||||
RegistryMetadatafieldsSuccessResponse,
|
RegistryMetadatafieldsSuccessResponse,
|
||||||
RegistryMetadataschemasSuccessResponse,
|
RegistryMetadataschemasSuccessResponse,
|
||||||
RestResponse
|
RestResponse
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { MockStore } from '../../shared/testing/mock-store';
|
import { MockStore } from '../../shared/testing/mock-store';
|
||||||
@@ -44,7 +42,7 @@ import { MetadataSchema } from '../metadata/metadata-schema.model';
|
|||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({template: ''})
|
||||||
class DummyComponent {
|
class DummyComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +125,7 @@ describe('RegistryService', () => {
|
|||||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
||||||
return observableCombineLatest(requestEntryObs,
|
return observableCombineLatest(requestEntryObs,
|
||||||
payloadObs).pipe(map(([req, pay]) => {
|
payloadObs).pipe(map(([req, pay]) => {
|
||||||
return { req, pay };
|
return {req, pay};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -143,11 +141,11 @@ describe('RegistryService', () => {
|
|||||||
DummyComponent
|
DummyComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
{provide: RequestService, useValue: getMockRequestService()},
|
||||||
{ provide: RemoteDataBuildService, useValue: rdbStub },
|
{provide: RemoteDataBuildService, useValue: rdbStub},
|
||||||
{ provide: HALEndpointService, useValue: halServiceStub },
|
{provide: HALEndpointService, useValue: halServiceStub},
|
||||||
{ provide: Store, useClass: MockStore },
|
{provide: Store, useClass: MockStore},
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{provide: NotificationsService, useValue: new NotificationsServiceStub()},
|
||||||
RegistryService
|
RegistryService
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -162,7 +160,7 @@ describe('RegistryService', () => {
|
|||||||
page: pageInfo
|
page: pageInfo
|
||||||
});
|
});
|
||||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
const responseEntry = Object.assign(new RequestEntry(), {response: response});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||||
@@ -191,7 +189,7 @@ describe('RegistryService', () => {
|
|||||||
page: pageInfo
|
page: pageInfo
|
||||||
});
|
});
|
||||||
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
const responseEntry = Object.assign(new RequestEntry(), {response: response});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||||
@@ -220,7 +218,7 @@ describe('RegistryService', () => {
|
|||||||
page: pageInfo
|
page: pageInfo
|
||||||
});
|
});
|
||||||
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
const responseEntry = Object.assign(new RequestEntry(), {response: response});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
||||||
@@ -243,35 +241,6 @@ describe('RegistryService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when requesting bitstreamformats', () => {
|
|
||||||
const queryResponse = Object.assign(new RegistryBitstreamformatsResponse(), {
|
|
||||||
bitstreamformats: mockFieldsList,
|
|
||||||
page: pageInfo
|
|
||||||
});
|
|
||||||
const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, 200, 'OK', pageInfo);
|
|
||||||
const responseEntry = Object.assign(new RequestEntry(), { response: response });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
|
|
||||||
/* tslint:disable:no-empty */
|
|
||||||
registryService.getBitstreamFormats(pagination).subscribe((value) => {
|
|
||||||
});
|
|
||||||
/* 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 dispatching to the store', () => {
|
describe('when dispatching to the store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(mockStore, 'dispatch');
|
spyOn(mockStore, 'dispatch');
|
||||||
@@ -284,7 +253,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => {
|
it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling cancelEditMetadataSchema', () => {
|
describe('when calling cancelEditMetadataSchema', () => {
|
||||||
@@ -294,7 +263,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryCancelSchemaAction', () => {
|
it('should dispatch a MetadataRegistryCancelSchemaAction', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction());
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction());
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling selectMetadataSchema', () => {
|
describe('when calling selectMetadataSchema', () => {
|
||||||
@@ -304,7 +273,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => {
|
it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling deselectMetadataSchema', () => {
|
describe('when calling deselectMetadataSchema', () => {
|
||||||
@@ -314,7 +283,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => {
|
it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling deselectAllMetadataSchema', () => {
|
describe('when calling deselectAllMetadataSchema', () => {
|
||||||
@@ -324,7 +293,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => {
|
it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction());
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction());
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling editMetadataField', () => {
|
describe('when calling editMetadataField', () => {
|
||||||
@@ -334,7 +303,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => {
|
it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling cancelEditMetadataField', () => {
|
describe('when calling cancelEditMetadataField', () => {
|
||||||
@@ -344,7 +313,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryCancelFieldAction', () => {
|
it('should dispatch a MetadataRegistryCancelFieldAction', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction());
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction());
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling selectMetadataField', () => {
|
describe('when calling selectMetadataField', () => {
|
||||||
@@ -354,7 +323,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => {
|
it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling deselectMetadataField', () => {
|
describe('when calling deselectMetadataField', () => {
|
||||||
@@ -364,7 +333,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => {
|
it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0]));
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0]));
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when calling deselectAllMetadataField', () => {
|
describe('when calling deselectAllMetadataField', () => {
|
||||||
@@ -374,7 +343,7 @@ describe('RegistryService', () => {
|
|||||||
|
|
||||||
it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => {
|
it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => {
|
||||||
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction());
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction());
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -417,7 +386,7 @@ describe('RegistryService', () => {
|
|||||||
result.subscribe((response: RestResponse) => {
|
result.subscribe((response: RestResponse) => {
|
||||||
expect(response.isSuccessful).toBe(true);
|
expect(response.isSuccessful).toBe(true);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when deleteMetadataField is called', () => {
|
describe('when deleteMetadataField is called', () => {
|
||||||
@@ -431,7 +400,7 @@ describe('RegistryService', () => {
|
|||||||
result.subscribe((response: RestResponse) => {
|
result.subscribe((response: RestResponse) => {
|
||||||
expect(response.isSuccessful).toBe(true);
|
expect(response.isSuccessful).toBe(true);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when clearMetadataSchemaRequests is called', () => {
|
describe('when clearMetadataSchemaRequests is called', () => {
|
||||||
|
@@ -3,13 +3,13 @@ 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 { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { BitstreamFormat } from './mock-bitstream-format.model';
|
|
||||||
import {
|
import {
|
||||||
CreateMetadataFieldRequest,
|
CreateMetadataFieldRequest,
|
||||||
CreateMetadataSchemaRequest,
|
CreateMetadataSchemaRequest,
|
||||||
DeleteRequest,
|
DeleteRequest,
|
||||||
GetRequest,
|
GetRequest,
|
||||||
RestRequest, UpdateMetadataFieldRequest,
|
RestRequest,
|
||||||
|
UpdateMetadataFieldRequest,
|
||||||
UpdateMetadataSchemaRequest
|
UpdateMetadataSchemaRequest
|
||||||
} from '../data/request.models';
|
} from '../data/request.models';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
@@ -19,24 +19,19 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
|
||||||
import {
|
import {
|
||||||
ErrorResponse, MetadatafieldSuccessResponse, MetadataschemaSuccessResponse,
|
MetadatafieldSuccessResponse,
|
||||||
RegistryBitstreamformatsSuccessResponse,
|
MetadataschemaSuccessResponse,
|
||||||
RegistryMetadatafieldsSuccessResponse,
|
RegistryMetadatafieldsSuccessResponse,
|
||||||
RegistryMetadataschemasSuccessResponse, RestResponse
|
RegistryMetadataschemasSuccessResponse,
|
||||||
|
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 { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
|
||||||
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
|
||||||
import { hasValue, hasNoValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
|
import { configureRequest, getResponseFromEntry } from '../shared/operators';
|
||||||
import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
|
|
||||||
import {
|
|
||||||
configureRequest,
|
|
||||||
getResponseFromEntry,
|
|
||||||
getSucceededRemoteData
|
|
||||||
} 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';
|
||||||
@@ -52,9 +47,8 @@ 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, switchMap, take, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
|
||||||
import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model';
|
import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||||
@@ -79,7 +73,8 @@ export class RegistryService {
|
|||||||
|
|
||||||
private metadataSchemasPath = 'metadataschemas';
|
private metadataSchemasPath = 'metadataschemas';
|
||||||
private metadataFieldsPath = 'metadatafields';
|
private metadataFieldsPath = 'metadatafields';
|
||||||
private bitstreamFormatsPath = 'bitstreamformats';
|
|
||||||
|
// private bitstreamFormatsPath = 'bitstreamformats';
|
||||||
|
|
||||||
constructor(protected requestService: RequestService,
|
constructor(protected requestService: RequestService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
@@ -197,7 +192,7 @@ export class RegistryService {
|
|||||||
*/
|
*/
|
||||||
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
if (hasNoValue(pagination)) {
|
if (hasNoValue(pagination)) {
|
||||||
pagination = { currentPage: 1, pageSize: 10000 } as any;
|
pagination = {currentPage: 1, pageSize: 10000} as any;
|
||||||
}
|
}
|
||||||
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
const requestObs = this.getMetadataFieldsRequestObs(pagination);
|
||||||
|
|
||||||
@@ -231,41 +226,7 @@ export class RegistryService {
|
|||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
||||||
* Retrieves all bitstream formats
|
|
||||||
* @param pagination The pagination info used to retrieve the bitstream formats
|
|
||||||
*/
|
|
||||||
public getBitstreamFormats(pagination: PaginationComponentOptions): Observable<RemoteData<PaginatedList<BitstreamFormat>>> {
|
|
||||||
const requestObs = this.getBitstreamFormatsRequestObs(pagination);
|
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
|
||||||
);
|
|
||||||
|
|
||||||
const rbrObs: Observable<RegistryBitstreamformatsResponse> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse)
|
|
||||||
);
|
|
||||||
|
|
||||||
const bitstreamformatsObs: Observable<BitstreamFormat[]> = rbrObs.pipe(
|
|
||||||
map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
|
||||||
getResponseFromEntry(),
|
|
||||||
map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo)
|
|
||||||
);
|
|
||||||
|
|
||||||
const payloadObs = observableCombineLatest(bitstreamformatsObs, pageInfoObs).pipe(
|
|
||||||
map(([bitstreamformats, pageInfo]) => {
|
|
||||||
return new PaginatedList(pageInfo, bitstreamformats);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
|
||||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||||
map((url: string) => {
|
map((url: string) => {
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
@@ -327,30 +288,6 @@ export class RegistryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBitstreamFormatsRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> {
|
|
||||||
return this.halService.getEndpoint(this.bitstreamFormatsPath).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 RegistryBitstreamformatsResponseParsingService;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to start editing a metadata schema, dispatches an edit schema action
|
|
||||||
* @param schema The schema that's being edited
|
|
||||||
*/
|
|
||||||
public editMetadataSchema(schema: MetadataSchema) {
|
public editMetadataSchema(schema: MetadataSchema) {
|
||||||
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
|
||||||
}
|
}
|
||||||
@@ -374,7 +311,7 @@ export class RegistryService {
|
|||||||
* @param schema The schema that's being selected
|
* @param schema The schema that's being selected
|
||||||
*/
|
*/
|
||||||
public selectMetadataSchema(schema: MetadataSchema) {
|
public selectMetadataSchema(schema: MetadataSchema) {
|
||||||
this.store.dispatch(new MetadataRegistrySelectSchemaAction(schema))
|
this.store.dispatch(new MetadataRegistrySelectSchemaAction(schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,14 +319,14 @@ export class RegistryService {
|
|||||||
* @param schema The schema that's it being deselected
|
* @param schema The schema that's it being deselected
|
||||||
*/
|
*/
|
||||||
public deselectMetadataSchema(schema: MetadataSchema) {
|
public deselectMetadataSchema(schema: MetadataSchema) {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectSchemaAction(schema))
|
this.store.dispatch(new MetadataRegistryDeselectSchemaAction(schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to deselect all currently selected metadata schema, dispatches a deselect all schema action
|
* Method to deselect all currently selected metadata schema, dispatches a deselect all schema action
|
||||||
*/
|
*/
|
||||||
public deselectAllMetadataSchema() {
|
public deselectAllMetadataSchema() {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectAllSchemaAction())
|
this.store.dispatch(new MetadataRegistryDeselectAllSchemaAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -423,20 +360,20 @@ export class RegistryService {
|
|||||||
* @param field The field that's being selected
|
* @param field The field that's being selected
|
||||||
*/
|
*/
|
||||||
public selectMetadataField(field: MetadataField) {
|
public selectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistrySelectFieldAction(field))
|
this.store.dispatch(new MetadataRegistrySelectFieldAction(field));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Method to deselect a metadata field, dispatches a deselect field action
|
* Method to deselect a metadata field, dispatches a deselect field action
|
||||||
* @param field The field that's it being deselected
|
* @param field The field that's it being deselected
|
||||||
*/
|
*/
|
||||||
public deselectMetadataField(field: MetadataField) {
|
public deselectMetadataField(field: MetadataField) {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field))
|
this.store.dispatch(new MetadataRegistryDeselectFieldAction(field));
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
* Method to deselect all currently selected metadata fields, dispatches a deselect all field action
|
||||||
*/
|
*/
|
||||||
public deselectAllMetadataField() {
|
public deselectAllMetadataField() {
|
||||||
this.store.dispatch(new MetadataRegistryDeselectAllFieldAction())
|
this.store.dispatch(new MetadataRegistryDeselectAllFieldAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -494,7 +431,7 @@ export class RegistryService {
|
|||||||
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showNotifications(true, isUpdate, false, { prefix: schema.prefix });
|
this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -521,7 +458,7 @@ export class RegistryService {
|
|||||||
public clearMetadataSchemaRequests(): Observable<string> {
|
public clearMetadataSchemaRequests(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
|
||||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -571,7 +508,7 @@ export class RegistryService {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
||||||
this.showNotifications(true, isUpdate, true, { field: fieldString });
|
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -597,7 +534,7 @@ export class RegistryService {
|
|||||||
public clearMetadataFieldRequests(): Observable<string> {
|
public clearMetadataFieldRequests(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
|
||||||
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
tap((href: string) => this.requestService.removeByHrefSubstring(href))
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private delete(path: string, id: number): Observable<RestResponse> {
|
private delete(path: string, id: number): Observable<RestResponse> {
|
||||||
@@ -633,9 +570,9 @@ export class RegistryService {
|
|||||||
);
|
);
|
||||||
messages.subscribe(([head, content]) => {
|
messages.subscribe(([head, content]) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content)
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content)
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
5
src/app/core/shared/bitstream-format-support-level.ts
Normal file
5
src/app/core/shared/bitstream-format-support-level.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum BitstreamFormatSupportLevel {
|
||||||
|
Known = 'KNOWN',
|
||||||
|
Unknown = 'UNKNOWN',
|
||||||
|
Supported = 'SUPPORTED'
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { CacheableObject, TypedObject } from '../cache/object-cache.reducer';
|
import { CacheableObject, TypedObject } from '../cache/object-cache.reducer';
|
||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
|
import { BitstreamFormatSupportLevel } from './bitstream-format-support-level';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model class for a Bitstream Format
|
* Model class for a Bitstream Format
|
||||||
@@ -27,7 +27,7 @@ export class BitstreamFormat implements CacheableObject {
|
|||||||
/**
|
/**
|
||||||
* The level of support the system offers for this Bitstream Format
|
* The level of support the system offers for this Bitstream Format
|
||||||
*/
|
*/
|
||||||
supportLevel: number;
|
supportLevel: BitstreamFormatSupportLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if the Bitstream Format is used to store system information, rather than the content of items in the system
|
* True if the Bitstream Format is used to store system information, rather than the content of items in the system
|
||||||
@@ -37,7 +37,7 @@ export class BitstreamFormat implements CacheableObject {
|
|||||||
/**
|
/**
|
||||||
* String representing this Bitstream Format's file extension
|
* String representing this Bitstream Format's file extension
|
||||||
*/
|
*/
|
||||||
extensions: string;
|
extensions: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The link to the rest endpoint where this Bitstream Format can be found
|
* The link to the rest endpoint where this Bitstream Format can be found
|
||||||
@@ -49,4 +49,11 @@ export class BitstreamFormat implements CacheableObject {
|
|||||||
*/
|
*/
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier for this Bitstream Format
|
||||||
|
* Note that this ID is unique for bitstream formats,
|
||||||
|
* but might not be unique across different object types
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,10 +5,11 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Collection } from './collection.model';
|
import { Collection } from './collection.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { Relationship } from './item-relationships/relationship.model';
|
import { Relationship } from './item-relationships/relationship.model';
|
||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
|
import { getSucceededRemoteData } from './operators';
|
||||||
|
|
||||||
export class Item extends DSpaceObject {
|
export class Item extends DSpaceObject {
|
||||||
static type = new ResourceType('item');
|
static type = new ResourceType('item');
|
||||||
@@ -97,7 +98,7 @@ export class Item extends DSpaceObject {
|
|||||||
*/
|
*/
|
||||||
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||||
return this.bitstreams.pipe(
|
return this.bitstreams.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<Bitstream>>) => !rd.isResponsePending && isNotUndefined(rd.payload)),
|
getSucceededRemoteData(),
|
||||||
map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page),
|
map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page),
|
||||||
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
|
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
|
||||||
take(1),
|
take(1),
|
||||||
|
@@ -91,7 +91,7 @@ export const toDSpaceObjectListRD = () =>
|
|||||||
source.pipe(
|
source.pipe(
|
||||||
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
|
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
|
||||||
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
|
||||||
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.indexableObject);
|
const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult<T>) => searchResult.indexableObject);
|
||||||
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
|
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
|
||||||
return Object.assign(rd, { payload: payload });
|
return Object.assign(rd, { payload: payload });
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('creativework.datePublished')" class="item-date card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
|
<span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="dso.hasMetadata('journal.title')" class="item-journal-title card-text">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span [innerHTML]="firstMetadataValue('journal.title')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { JournalIssueGridElementComponent } from './journal-issue-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'creativework.datePublished': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'journal.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'The journal title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalIssueGridElementComponent', getEntityGridElementTestComponent(JournalIssueGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'journal-title']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
|
||||||
|
@rendersItemType('JournalIssue', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal-issue-grid-element',
|
||||||
|
styleUrls: ['./journal-issue-grid-element.component.scss'],
|
||||||
|
templateUrl: './journal-issue-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Journal Issue
|
||||||
|
*/
|
||||||
|
export class JournalIssueGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('creativework.datePublished')" class="item-date card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
|
<span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span [innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { JournalVolumeGridElementComponent } from './journal-volume-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'creativework.datePublished': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'A description for the journal volume'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalVolumeGridElementComponent', getEntityGridElementTestComponent(JournalVolumeGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'description']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
|
||||||
|
@rendersItemType('JournalVolume', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal-volume-grid-element',
|
||||||
|
styleUrls: ['./journal-volume-grid-element.component.scss'],
|
||||||
|
templateUrl: './journal-volume-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Journal Volume
|
||||||
|
*/
|
||||||
|
export class JournalVolumeGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('creativework.editor')"
|
||||||
|
class="item-publisher card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
|
<span class="item-editor">{{dso.firstMetadataValue('creativework.editor')}}</span>
|
||||||
|
<span *ngIf="dso.hasMetadata('creativework.publisher')" class="item-publisher">
|
||||||
|
<span>, </span>
|
||||||
|
{{dso.firstMetadataValue('creativework.publisher')}}
|
||||||
|
</span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span [innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { JournalGridElementComponent } from './journal-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'creativework.editor': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith, Donald'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'creativework.publisher': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'A company'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is the description'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalGridElementComponent', getEntityGridElementTestComponent(JournalGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['editor', 'publisher', 'description']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
|
||||||
|
@rendersItemType('Journal', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal-grid-element',
|
||||||
|
styleUrls: ['./journal-grid-element.component.scss'],
|
||||||
|
templateUrl: './journal-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Journal
|
||||||
|
*/
|
||||||
|
export class JournalGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -9,6 +9,9 @@ import { JournalListElementComponent } from './item-list-elements/journal/journa
|
|||||||
import { JournalIssueListElementComponent } from './item-list-elements/journal-issue/journal-issue-list-element.component';
|
import { JournalIssueListElementComponent } from './item-list-elements/journal-issue/journal-issue-list-element.component';
|
||||||
import { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component';
|
import { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component';
|
||||||
import { TooltipModule } from 'ngx-bootstrap';
|
import { TooltipModule } from 'ngx-bootstrap';
|
||||||
|
import { JournalIssueGridElementComponent } from './item-grid-elements/journal-issue/journal-issue-grid-element.component';
|
||||||
|
import { JournalVolumeGridElementComponent } from './item-grid-elements/journal-volume/journal-volume-grid-element.component';
|
||||||
|
import { JournalGridElementComponent } from './item-grid-elements/journal/journal-grid-element.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
JournalComponent,
|
JournalComponent,
|
||||||
@@ -16,7 +19,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
JournalVolumeComponent,
|
JournalVolumeComponent,
|
||||||
JournalListElementComponent,
|
JournalListElementComponent,
|
||||||
JournalIssueListElementComponent,
|
JournalIssueListElementComponent,
|
||||||
JournalVolumeListElementComponent
|
JournalVolumeListElementComponent,
|
||||||
|
JournalIssueGridElementComponent,
|
||||||
|
JournalVolumeGridElementComponent,
|
||||||
|
JournalGridElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('organization.legalName')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('organization.foundingDate')" class="item-date card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
|
<span [innerHTML]="firstMetadataValue('organization.foundingDate')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="dso.hasMetadata('organization.address.addressCountry')"
|
||||||
|
class="item-location card-text">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span class="item-country">{{dso.firstMetadataValue('organization.address.addressCountry')}}</span>
|
||||||
|
<span *ngIf="dso.hasMetadata('organization.address.addressLocality')" class="item-city">
|
||||||
|
<span>, </span>
|
||||||
|
{{dso.firstMetadataValue('organization.address.addressLocality')}}
|
||||||
|
</span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { OrgunitGridElementComponent } from './orgunit-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'organization.foundingDate': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: '2015-06-26'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'organization.address.addressCountry': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Belgium'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'organization.address.addressLocality': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Brussels'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OrgunitGridElementComponent', getEntityGridElementTestComponent(OrgunitGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'country', 'city']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
|
||||||
|
@rendersItemType('OrgUnit', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orgunit-grid-element',
|
||||||
|
styleUrls: ['./orgunit-grid-element.component.scss'],
|
||||||
|
templateUrl: './orgunit-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Organisation Unit
|
||||||
|
*/
|
||||||
|
export class OrgunitGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('person.familyName') + ', ' + dso.firstMetadataValue('person.givenName')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('person.email')" class="item-email card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||||
|
<span [innerHTML]="firstMetadataValue('person.email')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<p *ngIf="dso.hasMetadata('person.jobTitle')" class="item-jobtitle card-text">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span [innerHTML]="firstMetadataValue('person.jobTitle')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { PersonGridElementComponent } from './person-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'person.email': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Smith-Donald@gmail.com'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'person.jobTitle': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Web Developer'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PersonGridElementComponent', getEntityGridElementTestComponent(PersonGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['email', 'jobtitle']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
|
||||||
|
@rendersItemType('Person', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-person-grid-element',
|
||||||
|
styleUrls: ['./person-grid-element.component.scss'],
|
||||||
|
templateUrl: './person-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Person
|
||||||
|
*/
|
||||||
|
export class PersonGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
|
<div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width">
|
||||||
|
<div>
|
||||||
|
<ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async">
|
||||||
|
</ds-grid-thumbnail>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<ds-item-type-badge [object]="object"></ds-item-type-badge>
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
|
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
<p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text text-muted">
|
||||||
|
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
||||||
|
<span [innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</p>
|
||||||
|
<div class="text-center">
|
||||||
|
<a [routerLink]="['/items/' + dso.id]"
|
||||||
|
class="lead btn btn-primary viewButton">View</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ds-truncatable>
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec';
|
||||||
|
import { ProjectGridElementComponent } from './project-grid-element.component';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
|
||||||
|
const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithMetadata.hitHighlights = {};
|
||||||
|
mockItemWithMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'The project description'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult();
|
||||||
|
mockItemWithoutMetadata.hitHighlights = {};
|
||||||
|
mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), {
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProjectGridElementComponent', getEntityGridElementTestComponent(ProjectGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['description']));
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { focusShadow } from '../../../../shared/animations/focus';
|
||||||
|
import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
|
|
||||||
|
@rendersItemType('Project', ItemViewMode.Card)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-project-grid-element',
|
||||||
|
styleUrls: ['./project-grid-element.component.scss'],
|
||||||
|
templateUrl: './project-grid-element.component.html',
|
||||||
|
animations: [focusShadow]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an item of the type Project
|
||||||
|
*/
|
||||||
|
export class ProjectGridElementComponent extends TypedItemSearchResultGridElementComponent {
|
||||||
|
}
|
@@ -11,6 +11,9 @@ import { PersonMetadataListElementComponent } from './item-list-elements/person/
|
|||||||
import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component';
|
import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component';
|
||||||
import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component';
|
import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component';
|
||||||
import { TooltipModule } from 'ngx-bootstrap';
|
import { TooltipModule } from 'ngx-bootstrap';
|
||||||
|
import { PersonGridElementComponent } from './item-grid-elements/person/person-grid-element.component';
|
||||||
|
import { OrgunitGridElementComponent } from './item-grid-elements/orgunit/orgunit-grid-element.component';
|
||||||
|
import { ProjectGridElementComponent } from './item-grid-elements/project/project-grid-element.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
OrgunitComponent,
|
OrgunitComponent,
|
||||||
@@ -20,7 +23,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
OrgUnitMetadataListElementComponent,
|
OrgUnitMetadataListElementComponent,
|
||||||
PersonListElementComponent,
|
PersonListElementComponent,
|
||||||
PersonMetadataListElementComponent,
|
PersonMetadataListElementComponent,
|
||||||
ProjectListElementComponent
|
ProjectListElementComponent,
|
||||||
|
PersonGridElementComponent,
|
||||||
|
OrgunitGridElementComponent,
|
||||||
|
ProjectGridElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -9,6 +9,7 @@ import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynami
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
|
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
|
||||||
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
import { isNotEmpty } from '../../empty.util';
|
import { isNotEmpty } from '../../empty.util';
|
||||||
import { Community } from '../../../core/shared/community.model';
|
import { Community } from '../../../core/shared/community.model';
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Type of DSpaceObject that the form represents
|
* Type of DSpaceObject that the form represents
|
||||||
*/
|
*/
|
||||||
protected type;
|
protected type: ResourceType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string} Key prefix used to generate form labels
|
* @type {string} Key prefix used to generate form labels
|
||||||
@@ -110,11 +111,11 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
|
|||||||
private updateFieldTranslations() {
|
private updateFieldTranslations() {
|
||||||
this.formModel.forEach(
|
this.formModel.forEach(
|
||||||
(fieldModel: DynamicInputModel) => {
|
(fieldModel: DynamicInputModel) => {
|
||||||
fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id);
|
fieldModel.label = this.translate.instant(this.type.value + this.LABEL_KEY_PREFIX + fieldModel.id);
|
||||||
if (isNotEmpty(fieldModel.validators)) {
|
if (isNotEmpty(fieldModel.validators)) {
|
||||||
fieldModel.errorMessages = {};
|
fieldModel.errorMessages = {};
|
||||||
Object.keys(fieldModel.validators).forEach((key) => {
|
Object.keys(fieldModel.validators).forEach((key) => {
|
||||||
fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key);
|
fieldModel.errorMessages[key] = this.translate.instant(this.type.value + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<ng-container #componentViewContainer></ng-container>
|
<ng-container #componentViewContainer></ng-container>
|
||||||
|
|
||||||
<small *ngIf="hasHint" class="text-muted" [innerHTML]="model.hint" [ngClass]="getClass('element', 'hint')"></small>
|
<small *ngIf="hasHint" class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||||
|
|
||||||
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>
|
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>
|
||||||
|
@@ -3,6 +3,7 @@ import { MetadataRepresentationType } from '../../core/shared/metadata-represent
|
|||||||
|
|
||||||
export enum ItemViewMode {
|
export enum ItemViewMode {
|
||||||
Element = 'element',
|
Element = 'element',
|
||||||
|
Card = 'card',
|
||||||
Full = 'full',
|
Full = 'full',
|
||||||
Metadata = 'metadata'
|
Metadata = 'metadata'
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul ngbDropdownMenu class="dropdown-menu" aria-labelledby="dropdownLang">
|
<ul ngbDropdownMenu class="dropdown-menu" aria-labelledby="dropdownLang">
|
||||||
<li class="dropdown-item" #langSelect *ngFor="let lang of translate.getLangs()"
|
<li class="dropdown-item" #langSelect *ngFor="let lang of translate.getLangs()"
|
||||||
(click)="translate.use(lang)"
|
(click)="useLang(lang)"
|
||||||
[class.active]="lang === translate.currentLang">
|
[class.active]="lang === translate.currentLang">
|
||||||
{{ langLabel(lang) }}
|
{{ langLabel(lang) }}
|
||||||
</li>
|
</li>
|
||||||
|
@@ -6,6 +6,9 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
|
|||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import {LangConfig} from '../../../config/lang-config.interface';
|
import {LangConfig} from '../../../config/lang-config.interface';
|
||||||
import {Observable, of} from 'rxjs';
|
import {Observable, of} from 'rxjs';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
import { MockCookieService } from '../mocks/mock-cookie.service';
|
||||||
|
|
||||||
// This test is completely independent from any message catalogs or keys in the codebase
|
// This test is completely independent from any message catalogs or keys in the codebase
|
||||||
// The translation module is instantiated with these bogus messages that we aren't using anyway.
|
// The translation module is instantiated with these bogus messages that we aren't using anyway.
|
||||||
@@ -28,8 +31,14 @@ class CustomLoader implements TranslateLoader {
|
|||||||
/* tslint:enable:quotemark */
|
/* tslint:enable:quotemark */
|
||||||
/* tslint:enable:object-literal-key-quotes */
|
/* tslint:enable:object-literal-key-quotes */
|
||||||
|
|
||||||
|
let cookie: CookieService;
|
||||||
|
|
||||||
describe('LangSwitchComponent', () => {
|
describe('LangSwitchComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cookie = Object.assign(new MockCookieService());
|
||||||
|
});
|
||||||
|
|
||||||
describe('with English and Deutsch activated, English as default', () => {
|
describe('with English and Deutsch activated, English as default', () => {
|
||||||
let component: LangSwitchComponent;
|
let component: LangSwitchComponent;
|
||||||
let fixture: ComponentFixture<LangSwitchComponent>;
|
let fixture: ComponentFixture<LangSwitchComponent>;
|
||||||
@@ -61,7 +70,11 @@ describe('LangSwitchComponent', () => {
|
|||||||
)],
|
)],
|
||||||
declarations: [LangSwitchComponent],
|
declarations: [LangSwitchComponent],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
providers: [TranslateService, {provide: GLOBAL_CONFIG, useValue: mockConfig}]
|
providers: [
|
||||||
|
TranslateService,
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: mockConfig },
|
||||||
|
{ provide: CookieService, useValue: cookie }
|
||||||
|
]
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
translate = TestBed.get(TranslateService);
|
translate = TestBed.get(TranslateService);
|
||||||
@@ -73,6 +86,7 @@ describe('LangSwitchComponent', () => {
|
|||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
langSwitchElement = de.nativeElement;
|
langSwitchElement = de.nativeElement;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -93,6 +107,24 @@ describe('LangSwitchComponent', () => {
|
|||||||
it('should define the main A HREF in the UI', (() => {
|
it('should define the main A HREF in the UI', (() => {
|
||||||
expect(langSwitchElement.querySelector('a')).toBeDefined();
|
expect(langSwitchElement.querySelector('a')).toBeDefined();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
describe('when selecting a language', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(translate, 'use');
|
||||||
|
spyOn(cookie, 'set');
|
||||||
|
const langItem = fixture.debugElement.query(By.css('.dropdown-item')).nativeElement;
|
||||||
|
langItem.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should translate the app', () => {
|
||||||
|
expect(translate.use).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the client\'s language cookie', () => {
|
||||||
|
expect(cookie.set).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with English as the only active and also default language', () => {
|
describe('with English as the only active and also default language', () => {
|
||||||
@@ -127,7 +159,11 @@ describe('LangSwitchComponent', () => {
|
|||||||
)],
|
)],
|
||||||
declarations: [LangSwitchComponent],
|
declarations: [LangSwitchComponent],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
providers: [TranslateService, {provide: GLOBAL_CONFIG, useValue: mockConfig}]
|
providers: [
|
||||||
|
TranslateService,
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: mockConfig },
|
||||||
|
{ provide: CookieService, useValue: cookie }
|
||||||
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
translate = TestBed.get(TranslateService);
|
translate = TestBed.get(TranslateService);
|
||||||
translate.addLangs(mockConfig.languages.filter((MyLangConfig) => MyLangConfig.active === true).map((a) => a.code));
|
translate.addLangs(mockConfig.languages.filter((MyLangConfig) => MyLangConfig.active === true).map((a) => a.code));
|
||||||
|
@@ -2,6 +2,9 @@ import {Component, Inject, OnInit} from '@angular/core';
|
|||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {LangConfig} from '../../../config/lang-config.interface';
|
import {LangConfig} from '../../../config/lang-config.interface';
|
||||||
|
import { ClientCookieService } from '../services/client-cookie.service';
|
||||||
|
import { LANG_COOKIE } from '../../app.component';
|
||||||
|
import { CookieService } from '../services/cookie.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-lang-switch',
|
selector: 'ds-lang-switch',
|
||||||
@@ -23,7 +26,8 @@ export class LangSwitchComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
public translate: TranslateService
|
public translate: TranslateService,
|
||||||
|
public cookie: CookieService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,4 +50,13 @@ export class LangSwitchComponent implements OnInit {
|
|||||||
return this.activeLangs.find((MyLangConfig) => MyLangConfig.code === langcode).label;
|
return this.activeLangs.find((MyLangConfig) => MyLangConfig.code === langcode).label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a language and store it in a cookie
|
||||||
|
* @param lang The language to switch to
|
||||||
|
*/
|
||||||
|
useLang(lang: string) {
|
||||||
|
this.translate.use(lang);
|
||||||
|
this.cookie.set(LANG_COOKIE, lang);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
26
src/app/shared/mocks/mock-cookie.service.ts
Normal file
26
src/app/shared/mocks/mock-cookie.service.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Mock for [[CookieService]]
|
||||||
|
*/
|
||||||
|
export class MockCookieService {
|
||||||
|
cookies: Map<string, string>;
|
||||||
|
|
||||||
|
constructor(cookies: Map<string, string> = new Map()) {
|
||||||
|
this.cookies = cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name, value) {
|
||||||
|
this.cookies.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name) {
|
||||||
|
return this.cookies.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
return jasmine.createSpy('remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
return jasmine.createSpy('getAll');
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,9 @@
|
|||||||
import {of as observableOf, Observable } from 'rxjs';
|
import {of as observableOf, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
|
||||||
/* tslint:disable:no-shadowed-variable */
|
/* tslint:disable:no-shadowed-variable */
|
||||||
export const MockItem: Item = Object.assign(new Item(), {
|
export const MockItem: Item = Object.assign(new Item(), {
|
||||||
@@ -9,12 +12,19 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
isArchived: true,
|
isArchived: true,
|
||||||
isDiscoverable: true,
|
isDiscoverable: true,
|
||||||
isWithdrawn: false,
|
isWithdrawn: false,
|
||||||
bitstreams: observableOf({
|
bitstreams: observableOf(Object.assign({
|
||||||
self: 'dspace-angular://aggregated/object/1507836003548',
|
self: 'dspace-angular://aggregated/object/1507836003548',
|
||||||
requestPending: false,
|
requestPending: false,
|
||||||
responsePending: false,
|
responsePending: false,
|
||||||
isSuccessful: true,
|
isSuccessful: true,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
|
state: '',
|
||||||
|
error: undefined,
|
||||||
|
isRequestPending: false,
|
||||||
|
isResponsePending: false,
|
||||||
|
isLoading: false,
|
||||||
|
hasFailed: false,
|
||||||
|
hasSucceeded: true,
|
||||||
statusCode: '202',
|
statusCode: '202',
|
||||||
pageInfo: {},
|
pageInfo: {},
|
||||||
payload: {
|
payload: {
|
||||||
@@ -97,7 +107,7 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}),
|
}) as RemoteData<PaginatedList<Bitstream>>),
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user