diff --git a/config/environment.test.js b/config/environment.test.js
index f4d625303f..0652755bc7 100644
--- a/config/environment.test.js
+++ b/config/environment.test.js
@@ -1,3 +1,5 @@
module.exports = {
-
+ theme: {
+ name: 'default',
+ }
};
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index f1a9120df8..bbe77f3e2f 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -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.link.home-page": "Take me to the home page",
"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.formats.no-items": "No bitstream formats to show.",
- "admin.registries.bitstream-formats.formats.table.internal": "internal",
- "admin.registries.bitstream-formats.formats.table.mimetype": "MIME Type",
- "admin.registries.bitstream-formats.formats.table.name": "Name",
- "admin.registries.bitstream-formats.formats.table.supportLevel.0": "Unknown",
- "admin.registries.bitstream-formats.formats.table.supportLevel.1": "Known",
- "admin.registries.bitstream-formats.formats.table.supportLevel.2": "Support",
- "admin.registries.bitstream-formats.formats.table.supportLevel.head": "Support Level",
+ "admin.registries.bitstream-formats.edit.description.hint": "",
+ "admin.registries.bitstream-formats.edit.description.label": "Description",
+ "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.edit.extensions.label": "File extensions",
+ "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot",
+ "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.",
+ "admin.registries.bitstream-formats.edit.failure.head": "Failure",
+ "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.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.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",
@@ -101,6 +132,7 @@
"collection.form.tableofcontents": "News (HTML)",
"collection.form.title": "Name",
"collection.page.browse.recent.head": "Recent Submissions",
+ "collection.page.browse.recent.empty": "No items to show",
"collection.page.license": "License",
"collection.page.news": "News",
"community.create.head": "Create a Community",
@@ -235,12 +267,26 @@
"item.edit.reinstate.error": "An error occurred while reinstating the item",
"item.edit.reinstate.header": "Reinstate item: {{ id }}",
"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.title": "Item Edit - Bitstreams",
"item.edit.tabs.curate.head": "Curate",
"item.edit.tabs.curate.title": "Item Edit - Curate",
"item.edit.tabs.metadata.head": "Item 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.label": "Edit item's authorization policies",
"item.edit.tabs.status.buttons.delete.button": "Permanently delete",
@@ -670,4 +716,4 @@
"uploader.or": ", or",
"uploader.processing": "Processing",
"uploader.queue-lenght": "Queue length"
-}
+}
\ No newline at end of file
diff --git a/src/app/+admin/admin-registries/admin-registries-routing.module.ts b/src/app/+admin/admin-registries/admin-registries-routing.module.ts
index 8e3c322bc8..afdc46bf17 100644
--- a/src/app/+admin/admin-registries/admin-registries-routing.module.ts
+++ b/src/app/+admin/admin-registries/admin-registries-routing.module.ts
@@ -2,14 +2,29 @@ import { MetadataRegistryComponent } from './metadata-registry/metadata-registry
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
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({
imports: [
RouterModule.forChild([
- { 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', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.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'}
+ },
])
]
})
diff --git a/src/app/+admin/admin-registries/admin-registries.module.ts b/src/app/+admin/admin-registries/admin-registries.module.ts
index c7890e6697..bbeb59f0ab 100644
--- a/src/app/+admin/admin-registries/admin-registries.module.ts
+++ b/src/app/+admin/admin-registries/admin-registries.module.ts
@@ -5,10 +5,10 @@ import { CommonModule } from '@angular/common';
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
-import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
import { SharedModule } from '../../shared/shared.module';
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
-import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/metadata-field-form.component';
+import { MetadataFieldFormComponent } from './metadata-schema/metadata-field-form/metadata-field-form.component';
+import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module';
@NgModule({
imports: [
@@ -16,12 +16,12 @@ import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/
SharedModule,
RouterModule,
TranslateModule,
+ BitstreamFormatsModule,
AdminRegistriesRoutingModule
],
declarations: [
MetadataRegistryComponent,
MetadataSchemaComponent,
- BitstreamFormatsComponent,
MetadataSchemaFormComponent,
MetadataFieldFormComponent
],
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html
new file mode 100644
index 0000000000..2b65b369b2
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts
new file mode 100644
index 0000000000..0a10633956
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.spec.ts
@@ -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;
+
+ 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();
+
+ });
+ });
+});
diff --git a/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts
new file mode 100644
index 0000000000..9712be70ca
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.ts
@@ -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'));
+ }
+ }
+ );
+ }
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.actions.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.actions.ts
new file mode 100644
index 0000000000..58b0686dfd
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.actions.ts
@@ -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
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts
new file mode 100644
index 0000000000..76576afc7a
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.spec.ts
@@ -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
+ }
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts
new file mode 100644
index 0000000000..41880bf16c
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-format.reducers.ts
@@ -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;
+ }
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts
new file mode 100644
index 0000000000..67f6aa373e
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats-routing.module.ts
@@ -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 {
+
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
index 1ac547653f..e5cf7cf5ec 100644
--- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
@@ -2,13 +2,15 @@
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
index 3a680c906b..e672dc82ea 100644
--- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
@@ -1,6 +1,5 @@
import { BitstreamFormatsComponent } from './bitstream-formats.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { RegistryService } from '../../../core/registry/registry.service';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
@@ -13,86 +12,278 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
import { HostWindowService } from '../../../shared/host-window.service';
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', () => {
let comp: BitstreamFormatsComponent;
let fixture: ComponentFixture;
- let registryService: RegistryService;
- const mockFormatsList = [
- {
- shortDescription: 'Unknown',
- description: 'Unknown data format',
- mimetype: 'application/octet-stream',
- supportLevel: 0,
- internal: false,
- extensions: null
- },
- {
- shortDescription: 'License',
- description: 'Item-specific license agreed upon to submission',
- mimetype: 'text/plain; charset=utf-8',
- supportLevel: 1,
- internal: true,
- extensions: null
- },
- {
- shortDescription: 'CC License',
- description: 'Item-specific Creative Commons license agreed upon to submission',
- mimetype: 'text/html; charset=utf-8',
- supportLevel: 2,
- internal: true,
- extensions: null
- },
- {
- shortDescription: 'Adobe PDF',
- description: 'Adobe Portable Document Format',
- mimetype: 'application/pdf',
- supportLevel: 0,
- internal: false,
- extensions: null
- }
- ];
- const mockFormats = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFormatsList));
- const registryServiceStub = {
- getBitstreamFormats: () => mockFormats
- };
+ let bitstreamFormatService;
+ let scheduler: TestScheduler;
+ let notificationsServiceStub;
+
+ const bitstreamFormat1 = new BitstreamFormat();
+ bitstreamFormat1.uuid = 'test-uuid-1';
+ bitstreamFormat1.id = 'test-uuid-1';
+ bitstreamFormat1.shortDescription = 'Unknown';
+ bitstreamFormat1.description = 'Unknown data format';
+ bitstreamFormat1.mimetype = 'application/octet-stream';
+ bitstreamFormat1.supportLevel = BitstreamFormatSupportLevel.Unknown;
+ bitstreamFormat1.internal = false;
+ bitstreamFormat1.extensions = null;
+
+ const bitstreamFormat2 = new BitstreamFormat();
+ bitstreamFormat2.uuid = 'test-uuid-2';
+ bitstreamFormat2.id = 'test-uuid-2';
+ bitstreamFormat2.shortDescription = 'License';
+ bitstreamFormat2.description = 'Item-specific license agreed upon to submission';
+ bitstreamFormat2.mimetype = 'text/plain; charset=utf-8';
+ bitstreamFormat2.supportLevel = BitstreamFormatSupportLevel.Known;
+ bitstreamFormat2.internal = true;
+ bitstreamFormat2.extensions = null;
+
+ const bitstreamFormat3 = new BitstreamFormat();
+ bitstreamFormat3.uuid = 'test-uuid-3';
+ bitstreamFormat3.id = 'test-uuid-3';
+ bitstreamFormat3.shortDescription = 'CC License';
+ bitstreamFormat3.description = 'Item-specific Creative Commons license agreed upon to submission';
+ bitstreamFormat3.mimetype = 'text/html; charset=utf-8';
+ bitstreamFormat3.supportLevel = BitstreamFormatSupportLevel.Supported;
+ bitstreamFormat3.internal = true;
+ bitstreamFormat3.extensions = null;
+
+ const bitstreamFormat4 = new BitstreamFormat();
+ bitstreamFormat4.uuid = 'test-uuid-4';
+ bitstreamFormat4.id = 'test-uuid-4';
+ 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({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
providers: [
- { provide: RegistryService, useValue: registryServiceStub },
- { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
+ {provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
+ {provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
+ {provide: NotificationsService, useValue: notificationsServiceStub}
]
}).compileComponents();
- }));
+ };
- beforeEach(() => {
+ const initBeforeEach = () => {
fixture = TestBed.createComponent(BitstreamFormatsComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
- registryService = (comp as any).service;
+ };
+
+ describe('Bitstream format page content', () => {
+ beforeEach(async(initAsync));
+ beforeEach(initBeforeEach);
+
+ it('should contain four formats', () => {
+ const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
+ expect(tbody.children.length).toBe(4);
+ });
+
+ it('should contain the correct formats', () => {
+ const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
+ expect(unknownName.textContent).toBe('Unknown');
+
+ const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
+ expect(licenseName.textContent).toBe('License');
+
+ const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
+ expect(ccLicenseName.textContent).toBe('CC License');
+
+ const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
+ expect(adobeName.textContent).toBe('Adobe PDF');
+ });
});
- it('should contain four formats', () => {
- const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
- expect(tbody.children.length).toBe(4);
+ 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);
+ });
});
- it('should contain the correct formats', () => {
- const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(1)')).nativeElement;
- expect(unknownName.textContent).toBe('Unknown');
+ 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);
- const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(1)')).nativeElement;
- expect(licenseName.textContent).toBe('License');
+ 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 ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(1)')).nativeElement;
- expect(ccLicenseName.textContent).toBe('CC License');
+ const result = comp.isSelected(format);
- const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(1)')).nativeElement;
- expect(adobeName.textContent).toBe('Adobe PDF');
+ 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();
+ });
+ });
});
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
index bc0cbb8da6..cb7aa1ef91 100644
--- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
@@ -1,10 +1,16 @@
-import { Component } from '@angular/core';
-import { RegistryService } from '../../../core/registry/registry.service';
-import { Observable } from 'rxjs';
+import { Component, OnInit } from '@angular/core';
+import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
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 { 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
@@ -13,24 +19,125 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
selector: 'ds-bitstream-formats',
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
*/
bitstreamFormats: Observable>>;
+ /**
+ * A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats
+ */
+ pageState: BehaviorSubject;
+
+ /**
+ * 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
* Currently simply renders all bitstream formats
*/
- config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'registry-bitstreamformats-pagination',
- pageSize: 10000
+ pageSize: 20
});
- constructor(private registryService: RegistryService) {
- this.updateFormats();
+ constructor(private notificationsService: NotificationsService,
+ 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 {
+ 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
*/
onPageChange(event) {
- this.config.currentPage = event;
- this.updateFormats();
+ this.config = Object.assign(new FindAllOptions(), this.config, {
+ 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() {
- this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
+ return this.bitstreamFormatService.findAll(this.config);
}
}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.module.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.module.ts
new file mode 100644
index 0000000000..0800c50169
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.module.ts
@@ -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 {
+
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts
new file mode 100644
index 0000000000..f6eef741fd
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts
@@ -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> {
+ 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<> 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> {
+ return this.bitstreamFormatDataService.findById(route.params.id)
+ .pipe(
+ find((RD) => hasValue(RD.error) || RD.hasSucceeded),
+ );
+ }
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html
new file mode 100644
index 0000000000..f57ec9cd38
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts
new file mode 100644
index 0000000000..cfa93a15a8
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts
@@ -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;
+
+ 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) => {
+ 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();
+
+ });
+ });
+});
diff --git a/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts
new file mode 100644
index 0000000000..0fdcc75689
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts
@@ -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>;
+
+ 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)
+ );
+ }
+
+ /**
+ * 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');
+ }
+ }
+ );
+ }
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html
new file mode 100644
index 0000000000..be6ebf2599
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts
new file mode 100644
index 0000000000..2870705fc8
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts
@@ -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;
+
+ 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']);
+ });
+ });
+});
diff --git a/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.ts b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
new file mode 100644
index 0000000000..505ccccd91
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
@@ -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 = new EventEmitter();
+
+ /**
+ * 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()]);
+ }
+}
diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts
index 71af51c683..2003ecf124 100644
--- a/src/app/+admin/admin-routing.module.ts
+++ b/src/app/+admin/admin-routing.module.ts
@@ -1,11 +1,19 @@
import { RouterModule } from '@angular/router';
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({
imports: [
RouterModule.forChild([
{
- path: 'registries',
+ path: REGISTRIES_MODULE_PATH,
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
}
])
diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html
index 91239de17c..2b16bc1ca6 100644
--- a/src/app/+collection-page/collection-page.component.html
+++ b/src/app/+collection-page/collection-page.component.html
@@ -52,6 +52,9 @@
message="{{'error.recent-submissions' | translate}}">
+
+ {{'collection.page.browse.recent.empty' | translate}}
+
;
+ /**
+ * 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- ) => 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 {
+ 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 {
+ 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');
+
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
index a82c1976c8..236388109e 100644
--- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts
+++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
+import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
import { EditItemPageComponent } from './edit-item-page.component';
import { ItemStatusComponent } from './item-status/item-status.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 { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.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 { EditItemPageRoutingModule } from './edit-item-page.routing.module';
/**
* 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,
ItemStatusComponent,
ItemMetadataComponent,
+ ItemRelationshipsComponent,
ItemBitstreamsComponent,
EditInPlaceFieldComponent,
+ EditRelationshipComponent,
+ EditRelationshipListComponent,
ItemMoveComponent,
]
})
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
index 0c8dd407f9..65e2a36fd1 100644
--- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
+++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
@@ -11,15 +11,13 @@ import { ItemStatusComponent } from './item-status/item-status.component';
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemMoveComponent } from './item-move/item-move.component';
-import { URLCombiner } from '../../core/url-combiner/url-combiner';
-import { getItemEditPath } from '../item-page-routing.module';
+import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
const ITEM_EDIT_PRIVATE_PATH = 'private';
const ITEM_EDIT_PUBLIC_PATH = 'public';
const ITEM_EDIT_DELETE_PATH = 'delete';
-
const ITEM_EDIT_MOVE_PATH = 'move';
/**
@@ -43,29 +41,34 @@ const ITEM_EDIT_MOVE_PATH = 'move';
{
path: 'status',
component: ItemStatusComponent,
- data: {title: 'item.edit.tabs.status.title'}
+ data: { title: 'item.edit.tabs.status.title' }
},
{
path: 'bitstreams',
component: ItemBitstreamsComponent,
- data: {title: 'item.edit.tabs.bitstreams.title'}
+ data: { title: 'item.edit.tabs.bitstreams.title' }
},
{
path: 'metadata',
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',
/* TODO - change when view page exists */
component: ItemBitstreamsComponent,
- data: {title: 'item.edit.tabs.view.title'}
+ data: { title: 'item.edit.tabs.view.title' }
},
{
path: 'curate',
/* TODO - change when curate page exists */
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,
component: ItemMoveComponent,
- data: {title: 'item.edit.move.title'},
+ data: { title: 'item.edit.move.title' },
resolve: {
item: ItemPageResolver
}
diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts
index e51182e7b3..0c164b7bc2 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts
+++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts
@@ -31,7 +31,7 @@ import {
createSuccessfulRemoteDataObject$
} from '../../../shared/testing/utils';
-let comp: ItemMetadataComponent;
+let comp: any;
let fixture: ComponentFixture;
let de: DebugElement;
let el: HTMLElement;
diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts
index 8148b89bd4..be657d71dc 100644
--- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts
+++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts
@@ -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 { ItemDataService } from '../../../core/data/item-data.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 { Observable } from 'rxjs';
import {
- FieldUpdate,
- FieldUpdates,
Identifiable
} from '../../../core/data/object-updates/object-updates.reducer';
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 { MetadatumViewModel } from '../../../core/shared/metadata.models';
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';
@Component({
@@ -29,28 +28,7 @@ import { MetadataField } from '../../../core/metadata/metadata-field.model';
/**
* Component for displaying an item's metadata edit page
*/
-export class ItemMetadataComponent implements OnInit {
-
- /**
- * The item to display the edit page for
- */
- item: Item;
- /**
- * The current values and updates for all this item's metadata fields
- */
- updates$: Observable;
- /**
- * 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.';
+export class ItemMetadataComponent extends AbstractItemUpdateComponent {
/**
* Observable with a list of strings with all existing metadata field keys
@@ -58,44 +36,38 @@ export class ItemMetadataComponent implements OnInit {
metadataFields$: Observable;
constructor(
- private itemService: ItemDataService,
- private objectUpdatesService: ObjectUpdatesService,
- private router: Router,
- private notificationsService: NotificationsService,
- private translateService: TranslateService,
+ protected itemService: ItemDataService,
+ protected objectUpdatesService: ObjectUpdatesService,
+ protected router: Router,
+ protected notificationsService: NotificationsService,
+ protected translateService: TranslateService,
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
- private route: ActivatedRoute,
- private metadataFieldService: RegistryService,
+ protected route: ActivatedRoute,
+ protected metadataFieldService: RegistryService,
) {
-
+ super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
}
/**
* Set up and initialize all fields
*/
ngOnInit(): void {
+ super.ngOnInit();
this.metadataFields$ = this.findMetadataFields();
- this.route.parent.data.pipe(map((data) => data.item))
- .pipe(
- first(),
- map((data: RemoteData
- ) => 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.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
+ /**
+ * Initialize the values and updates of the current item's metadata fields
+ */
+ public initializeUpdates(): void {
+ this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
+ }
+
+ /**
+ * Initialize the prefix for notification messages
+ */
+ public initializeNotificationsPrefix(): void {
+ this.notificationsPrefix = 'item.edit.metadata.notifications.';
}
/**
@@ -104,47 +76,23 @@ export class ItemMetadataComponent implements OnInit {
*/
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
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
*/
- private initializeOriginalFields() {
- this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified);
- }
-
- /**
- * Prevent unnecessary rerendering so fields don't lose focus
- */
- trackUpdate(index, update: FieldUpdate) {
- return update && update.field ? update.field.uuid : undefined;
+ public initializeOriginalFields() {
+ this.objectUpdatesService.initialize(this.url, this.getMetadataAsListExcludingRelationships(), this.item.lastModified);
}
/**
* 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
*/
- submit() {
+ public submit() {
this.isValid().pipe(first()).subscribe((isValid) => {
if (isValid) {
- const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable;
+ const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.getMetadataAsListExcludingRelationships()) as Observable;
metadata$.pipe(
first(),
switchMap((metadata: MetadatumViewModel[]) => {
@@ -157,7 +105,7 @@ export class ItemMetadataComponent implements OnInit {
(rd: RemoteData
- ) => {
this.item = rd.payload;
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'));
}
)
@@ -167,60 +115,6 @@ export class ItemMetadataComponent implements OnInit {
});
}
- /**
- * Checks whether or not there are currently updates for this item
- */
- hasChanges(): Observable {
- return this.objectUpdatesService.hasUpdates(this.url);
- }
-
- /**
- * Checks whether or not the item is currently reinstatable
- */
- isReinstatable(): Observable {
- 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
*/
@@ -230,4 +124,8 @@ export class ItemMetadataComponent implements OnInit {
take(1),
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.'));
+ }
}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html
new file mode 100644
index 0000000000..ba5164e81a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html
@@ -0,0 +1,15 @@
+
+
+
{{getRelationshipMessageKey(relationshipLabel) | translate}}
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss
new file mode 100644
index 0000000000..ec6cd2ba78
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.scss
@@ -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;
+}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts
new file mode 100644
index 0000000000..3748ebca9d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts
@@ -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;
+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');
+ });
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts
new file mode 100644
index 0000000000..765d6484d4
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts
@@ -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;
+
+ 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- {
+ 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 {
+ 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;
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html
new file mode 100644
index 0000000000..e25c3c204f
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html
@@ -0,0 +1,19 @@
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss
new file mode 100644
index 0000000000..808a8344ba
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.scss
@@ -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%);
+}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts
new file mode 100644
index 0000000000..3306d8eb01
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts
@@ -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);
+ });
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts
new file mode 100644
index 0000000000..497b06775a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts
@@ -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;
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html
new file mode 100644
index 0000000000..4bd0b3df2c
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss
new file mode 100644
index 0000000000..898533a9f0
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.scss
@@ -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;
+ }
+
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts
new file mode 100644
index 0000000000..b1a4e11371
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts
@@ -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;
+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);
+ });
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts
new file mode 100644
index 0000000000..e8f34bc70e
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts
@@ -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;
+
+ /**
+ * 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- ) => {
+ 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),
+ 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();
+ }
+
+}
diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts
index 6743028b6c..f510ccf19b 100644
--- a/src/app/+item-page/item-page.module.ts
+++ b/src/app/+item-page/item-page.module.ts
@@ -62,7 +62,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
GenericItemPageFieldComponent,
RelatedEntitiesSearchComponent,
RelatedItemsComponent,
- MetadataRepresentationListComponent
+ MetadataRepresentationListComponent,
+ ItemPageTitleFieldComponent
],
entryComponents: [
PublicationComponent
diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts
index 65ad743245..b4eda2abfb 100644
--- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts
+++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts
@@ -7,10 +7,12 @@ import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.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 { ItemDataService } from '../../../../core/data/item-data.service';
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
@@ -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): Observable =>
+ 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))
+ ))
+ );
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts
index 03433d1da1..c268c5b7f6 100644
--- a/src/app/+search-page/search-page.component.ts
+++ b/src/app/+search-page/search-page.component.ts
@@ -109,7 +109,7 @@ export class SearchPageComponent implements OnInit {
ngOnInit(): void {
this.searchOptions$ = this.getSearchOptions();
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) => {
this.resultsRD$.next(results);
});
diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts
index 86611cc87b..be95ed096e 100644
--- a/src/app/+search-page/search-service/search.service.ts
+++ b/src/app/+search-page/search-service/search.service.ts
@@ -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 { NavigationExtras, Router } from '@angular/router';
-import { first, map, switchMap } from 'rxjs/operators';
+import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
+import { first, map, switchMap, tap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import {
FacetConfigSuccessResponse,
@@ -23,7 +23,7 @@ import {
getSucceededRemoteData
} from '../../core/shared/operators';
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 { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model';
@@ -103,11 +103,18 @@ export class SearchService implements OnDestroy {
* @returns {Observable>>>} Emits a paginated list with all search results found
*/
search(searchOptions?: PaginatedSearchOptions): Observable>>> {
- const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
+ const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url: string) => {
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 getResponseParserFn: () => GenericConstructor = () => {
@@ -136,10 +143,11 @@ export class SearchService implements OnDestroy {
map((sqr: SearchQueryResponse) => {
return sqr.objects
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject))
- .map((nsr: NormalizedSearchResult) => {
- return this.rdb.buildSingle(nsr.indexableObject);
- })
+ .map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), 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>>) => this.rdb.aggregate(input)),
);
@@ -168,11 +176,20 @@ export class SearchService implements OnDestroy {
const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
map(([tDomainList, pageInfo]) => {
- return new PaginatedList(pageInfo, tDomainList);
+ return new PaginatedList(pageInfo, tDomainList.filter((obj) => hasValue(obj)));
})
);
- return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
+ 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);
+ }
+ })
+ );
}
/**
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 86364aca89..e1ddc2b889 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -16,6 +16,12 @@ const COMMUNITY_MODULE_PATH = 'communities';
export function getCommunityModulePath() {
return `/${COMMUNITY_MODULE_PATH}`;
}
+
+const ADMIN_MODULE_PATH = 'admin';
+export function getAdminModulePath() {
+ return `/${ADMIN_MODULE_PATH}`;
+}
+
@NgModule({
imports: [
RouterModule.forRoot([
@@ -27,7 +33,7 @@ export function getCommunityModulePath() {
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
- { path: 'admin', loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
+ { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index bd2d832c67..bc3d0d1504 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -44,6 +44,8 @@ import { ActivatedRoute, Router } from '@angular/router';
import { RouteService } from './shared/services/route.service';
import { MockActivatedRoute } from './shared/mocks/mock-active-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 fixture: ComponentFixture;
@@ -78,6 +80,7 @@ describe('App component', () => {
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
+ { provide: CookieService, useValue: new MockCookieService()},
AppComponent,
RouteService
],
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 52c169e7bc..632b812cb5 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -32,6 +32,11 @@ import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
import { slideSidebarPadding } from './shared/animations/slide';
import { HostWindowService } from './shared/host-window.service';
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({
selector: 'ds-app',
@@ -61,6 +66,7 @@ export class AppComponent implements OnInit, AfterViewInit {
private cssService: CSSVariableService,
private menuService: MenuService,
private windowService: HostWindowService,
+ private cookie: CookieService
) {
// 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));
@@ -68,11 +74,20 @@ export class AppComponent implements OnInit, AfterViewInit {
// Load the default language from the config file
translate.setDefaultLang(config.defaultLanguage);
- // Attempt to get the browser language from the user
- if (translate.getLangs().includes(translate.getBrowserLang())) {
- translate.use(translate.getBrowserLang());
+ // 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 {
- translate.use(config.defaultLanguage);
+ // Cookie not found
+ // Attempt to get the browser language from the user
+ if (translate.getLangs().includes(translate.getBrowserLang())) {
+ translate.use(translate.getBrowserLang());
+ } else {
+ translate.use(config.defaultLanguage);
+ }
}
metadata.listenForRouteChange();
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index ce5a2d78a2..3781edf532 100755
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -39,6 +39,7 @@ import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/e
import { NavbarModule } from './navbar/navbar.module';
import { JournalEntitiesModule } from './entity-groups/journal-entities/journal-entities.module';
import { ResearchEntitiesModule } from './entity-groups/research-entities/research-entities.module';
+import { ClientCookieService } from './shared/services/client-cookie.service';
export function getConfig() {
return ENV_CONFIG;
@@ -97,7 +98,8 @@ const PROVIDERS = [
{
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
- }
+ },
+ ClientCookieService
];
const DECLARATIONS = [
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index ea2512a974..e3333fb34a 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -23,6 +23,10 @@ import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { historyReducer, HistoryState } from './shared/history/history.reducer';
+import {
+ bitstreamFormatReducer,
+ BitstreamFormatRegistryState
+} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
export interface AppState {
router: fromRouter.RouterReducerState;
@@ -30,6 +34,7 @@ export interface AppState {
hostWindow: HostWindowState;
forms: FormState;
metadataRegistry: MetadataRegistryState;
+ bitstreamFormats: BitstreamFormatRegistryState;
notifications: NotificationsState;
searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState;
@@ -44,6 +49,7 @@ export const appReducers: ActionReducerMap = {
hostWindow: hostWindowReducer,
forms: formReducer,
metadataRegistry: metadataRegistryReducer,
+ bitstreamFormats: bitstreamFormatReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer,
searchFilter: filterReducer,
diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts
index 5ee135b530..2283ecb368 100644
--- a/src/app/core/cache/models/normalized-bitstream-format.model.ts
+++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts
@@ -4,7 +4,7 @@ import { BitstreamFormat } from '../../shared/bitstream-format.model';
import { mapsTo } from '../builders/build-decorators';
import { IDToUUIDSerializer } from '../id-to-uuid-serializer';
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
@@ -34,7 +34,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject
* The level of support the system offers for this Bitstream Format
*/
@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
@@ -46,7 +46,7 @@ export class NormalizedBitstreamFormat extends NormalizedObject
* String representing this Bitstream Format's file extension
*/
@autoserialize
- extensions: string;
+ extensions: string[];
/**
* Identifier for this Bitstream Format
diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts
index e18ff3b3a4..11f3a6ce3e 100644
--- a/src/app/core/cache/object-cache.service.ts
+++ b/src/app/core/cache/object-cache.service.ts
@@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
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 { coreSelector } from '../core.selectors';
import { RestRequestMethod } from '../data/rest-request-method';
@@ -68,8 +68,8 @@ export class ObjectCacheService {
* @param href
* The unique href of the object to be removed
*/
- remove(uuid: string): void {
- this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
+ remove(href: string): void {
+ this.store.dispatch(new RemoveFromObjectCacheAction(href));
}
/**
@@ -224,6 +224,18 @@ export class ObjectCacheService {
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 {
+ return this.store.pipe(
+ select(entryFromSelfLinkSelector(selfLink)),
+ map((entry: ObjectCacheEntry) => this.isValid(entry))
+ );
+ }
+
/**
* Check whether an ObjectCacheEntry should still be cached
*
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 1d22b5fefe..6054752edb 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -101,12 +101,14 @@ import { NormalizedSubmissionFormsModel } from './config/models/normalized-confi
import { NormalizedSubmissionSectionModel } from './config/models/normalized-config-submission-section.model';
import { NormalizedAuthStatus } from './auth/models/normalized-auth-status.model';
import { NormalizedAuthorityValue } from './integration/models/normalized-authority-value.model';
+import { RelationshipService } from './data/relationship.service';
import { RoleService } from './roles/role.service';
import { MyDSpaceGuard } from '../+my-dspace-page/my-dspace.guard';
import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service';
import { ClaimedTaskDataService } from './tasks/claimed-task-data.service';
import { PoolTaskDataService } from './tasks/pool-task-data.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 { NormalizedTaskObject } from './tasks/models/normalized-task-object.model';
import { NormalizedPoolTask } from './tasks/models/normalized-pool-task-object.model';
@@ -153,6 +155,7 @@ const PROVIDERS = [
PaginationComponentOptions,
ResourcePolicyService,
RegistryService,
+ BitstreamFormatDataService,
NormalizedObjectBuildService,
RemoteDataBuildService,
RequestService,
@@ -198,6 +201,7 @@ const PROVIDERS = [
MenuService,
ObjectUpdatesService,
SearchService,
+ RelationshipService,
MyDSpaceGuard,
RoleService,
TaskResponseParsingService,
diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts
new file mode 100644
index 0000000000..f3ce478236
--- /dev/null
+++ b/src/app/core/data/bitstream-format-data.service.spec.ts
@@ -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;
+
+ const objectCache = {} as ObjectCacheService;
+ const halEndpointService = {
+ getEndpoint(linkPath: string): Observable {
+ 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 {
+ 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 {
+ 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);
+ });
+ });
+});
diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts
new file mode 100644
index 0000000000..a5638183c0
--- /dev/null
+++ b/src/app/core/data/bitstream-format-data.service.ts
@@ -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 {
+
+ protected linkPath = 'bitstreamformats';
+ protected forceBypassCache = false;
+
+ constructor(
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected dataBuildService: NormalizedObjectBuildService,
+ protected store: Store,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService,
+ protected notificationsService: NotificationsService,
+ protected http: HttpClient,
+ protected comparator: DefaultChangeAnalyzer) {
+ super();
+ }
+
+ /**
+ * Get the endpoint for browsing bitstream formats
+ * @param {FindAllOptions} options
+ * @returns {Observable}
+ */
+ getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable {
+ return this.halService.getEndpoint(this.linkPath);
+ }
+
+ /**
+ * Get the endpoint to update an existing bitstream format
+ * @param formatId
+ */
+ public getUpdateEndpoint(formatId: string): Observable {
+ return this.getBrowseEndpoint().pipe(
+ map((endpoint: string) => this.getIDHref(endpoint, formatId))
+ );
+ }
+
+ /**
+ * Get the endpoint to create a new bitstream format
+ */
+ public getCreateEndpoint(): Observable {
+ return this.getBrowseEndpoint();
+ }
+
+ /**
+ * Update an existing bitstreamFormat
+ * @param bitstreamFormat
+ */
+ updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable {
+ 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 {
+ 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 {
+ return this.getBrowseEndpoint().pipe(
+ tap((href: string) => this.requestService.removeByHrefSubstring(href))
+ );
+ }
+
+ /**
+ * Gets all the selected BitstreamFormats from the store
+ */
+ public getSelectedBitstreamFormats(): Observable {
+ 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 {
+ 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)
+ );
+ }
+}
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 22d5fd3e77..08745f9223 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -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 {
+ 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
* @param url The URL of the page on which the field resides
diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts
new file mode 100644
index 0000000000..0ced517d74
--- /dev/null
+++ b/src/app/core/data/relationship.service.spec.ts
@@ -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> {
+ return observableOf(new RemoteData(false, false, true, undefined, obj));
+}
diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts
new file mode 100644
index 0000000000..1699b6a27d
--- /dev/null
+++ b/src/app/core/data/relationship.service.ts
@@ -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> {
+ const href$ = this.getRelationshipEndpoint(uuid);
+ return this.rdbService.buildSingle(href$);
+ }
+
+ /**
+ * Send a delete request for a relationship by ID
+ * @param uuid
+ */
+ deleteRelationship(uuid: string): Observable {
+ 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 {
+ return item.relationships.pipe(
+ getSucceededRemoteData(),
+ getRemoteDataPayload(),
+ map((rels: PaginatedList) => rels.page),
+ hasValueOperator(),
+ distinctUntilChanged(compareArraysUsingIds())
+ );
+ }
+
+ /**
+ * Get an item their relationship types in the form of an array
+ * @param item
+ */
+ getItemRelationshipTypesArray(item: Item): Observable {
+ return this.getItemRelationshipsArray(item).pipe(
+ flatMap((rels: Relationship[]) =>
+ observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe(
+ map(([...arr]: Array>) => arr.map((d: RemoteData) => 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
- {
+ return this.getItemRelationshipsArray(item).pipe(
+ flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.leftItem)).pipe(
+ map(([...arr]: Array>) => arr.map((rd: RemoteData
- ) => 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
- {
+ return this.getItemRelationshipsArray(item).pipe(
+ flatMap((rels: Relationship[]) => observableZip(...rels.map((rel: Relationship) => rel.rightItem)).pipe(
+ map(([...arr]: Array>) => arr.map((rd: RemoteData
- ) => 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 {
+ 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
- {
+ 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
- {
+ 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) => 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);
+ });
+ }
+
+}
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index 83071382ed..775118dbc0 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -65,8 +65,7 @@ const uuidsFromHrefSubstringSelector =
const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
let result = [];
if (isNotEmpty(state)) {
- result = Object.values(state)
- .filter((value: string) => value.startsWith(href));
+ result = Object.keys(state).filter((key) => key.startsWith(href)).map((key) => state[key]);
}
return result;
};
@@ -315,4 +314,15 @@ export class RequestService {
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 {
+ return this.getByHref(href).pipe(
+ map((requestEntry: RequestEntry) => this.isValid(requestEntry))
+ );
+ }
+
}
diff --git a/src/app/core/registry/mock-bitstream-format.model.ts b/src/app/core/registry/mock-bitstream-format.model.ts
deleted file mode 100644
index f5811e367c..0000000000
--- a/src/app/core/registry/mock-bitstream-format.model.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export class BitstreamFormat {
- shortDescription: string;
- description: string;
- mimetype: string;
- supportLevel: number;
- internal: boolean;
- extensions: string;
-}
diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts
index 47e306d624..455a8043da 100644
--- a/src/app/core/registry/registry.service.spec.ts
+++ b/src/app/core/registry/registry.service.spec.ts
@@ -12,7 +12,6 @@ import { PageInfo } from '../shared/page-info.model';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import {
- RegistryBitstreamformatsSuccessResponse,
RegistryMetadatafieldsSuccessResponse,
RegistryMetadataschemasSuccessResponse,
RestResponse
@@ -20,7 +19,6 @@ import {
import { Component } from '@angular/core';
import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
-import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
import { map } from 'rxjs/operators';
import { Store, StoreModule } from '@ngrx/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 { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
-@Component({ template: '' })
+@Component({template: ''})
class DummyComponent {
}
@@ -127,7 +125,7 @@ describe('RegistryService', () => {
toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => {
return observableCombineLatest(requestEntryObs,
payloadObs).pipe(map(([req, pay]) => {
- return { req, pay };
+ return {req, pay};
})
);
},
@@ -143,11 +141,11 @@ describe('RegistryService', () => {
DummyComponent
],
providers: [
- { provide: RequestService, useValue: getMockRequestService() },
- { provide: RemoteDataBuildService, useValue: rdbStub },
- { provide: HALEndpointService, useValue: halServiceStub },
- { provide: Store, useClass: MockStore },
- { provide: NotificationsService, useValue: new NotificationsServiceStub() },
+ {provide: RequestService, useValue: getMockRequestService()},
+ {provide: RemoteDataBuildService, useValue: rdbStub},
+ {provide: HALEndpointService, useValue: halServiceStub},
+ {provide: Store, useClass: MockStore},
+ {provide: NotificationsService, useValue: new NotificationsServiceStub()},
RegistryService
]
});
@@ -162,7 +160,7 @@ describe('RegistryService', () => {
page: 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(() => {
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
@@ -191,7 +189,7 @@ describe('RegistryService', () => {
page: 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(() => {
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
@@ -220,7 +218,7 @@ describe('RegistryService', () => {
page: 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(() => {
(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', () => {
beforeEach(() => {
spyOn(mockStore, 'dispatch');
@@ -284,7 +253,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryEditSchemaAction with the correct schema', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditSchemaAction(mockSchemasList[0]));
- })
+ });
});
describe('when calling cancelEditMetadataSchema', () => {
@@ -294,7 +263,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryCancelSchemaAction', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelSchemaAction());
- })
+ });
});
describe('when calling selectMetadataSchema', () => {
@@ -304,7 +273,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistrySelectSchemaAction with the correct schema', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectSchemaAction(mockSchemasList[0]));
- })
+ });
});
describe('when calling deselectMetadataSchema', () => {
@@ -314,7 +283,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryDeselectSchemaAction with the correct schema', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectSchemaAction(mockSchemasList[0]));
- })
+ });
});
describe('when calling deselectAllMetadataSchema', () => {
@@ -324,7 +293,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryDeselectAllSchemaAction', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllSchemaAction());
- })
+ });
});
describe('when calling editMetadataField', () => {
@@ -334,7 +303,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryEditFieldAction with the correct Field', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryEditFieldAction(mockFieldsList[0]));
- })
+ });
});
describe('when calling cancelEditMetadataField', () => {
@@ -344,7 +313,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryCancelFieldAction', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryCancelFieldAction());
- })
+ });
});
describe('when calling selectMetadataField', () => {
@@ -354,7 +323,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistrySelectFieldAction with the correct Field', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistrySelectFieldAction(mockFieldsList[0]));
- })
+ });
});
describe('when calling deselectMetadataField', () => {
@@ -364,7 +333,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryDeselectFieldAction with the correct Field', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectFieldAction(mockFieldsList[0]));
- })
+ });
});
describe('when calling deselectAllMetadataField', () => {
@@ -374,7 +343,7 @@ describe('RegistryService', () => {
it('should dispatch a MetadataRegistryDeselectAllFieldAction', () => {
expect(mockStore.dispatch).toHaveBeenCalledWith(new MetadataRegistryDeselectAllFieldAction());
- })
+ });
});
});
@@ -417,7 +386,7 @@ describe('RegistryService', () => {
result.subscribe((response: RestResponse) => {
expect(response.isSuccessful).toBe(true);
});
- })
+ });
});
describe('when deleteMetadataField is called', () => {
@@ -431,7 +400,7 @@ describe('RegistryService', () => {
result.subscribe((response: RestResponse) => {
expect(response.isSuccessful).toBe(true);
});
- })
+ });
});
describe('when clearMetadataSchemaRequests is called', () => {
diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts
index d816c5eab8..206426588e 100644
--- a/src/app/core/registry/registry.service.ts
+++ b/src/app/core/registry/registry.service.ts
@@ -3,13 +3,13 @@ import { Injectable } from '@angular/core';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list';
import { PageInfo } from '../shared/page-info.model';
-import { BitstreamFormat } from './mock-bitstream-format.model';
import {
CreateMetadataFieldRequest,
CreateMetadataSchemaRequest,
DeleteRequest,
GetRequest,
- RestRequest, UpdateMetadataFieldRequest,
+ RestRequest,
+ UpdateMetadataFieldRequest,
UpdateMetadataSchemaRequest
} from '../data/request.models';
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 { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model';
import {
- ErrorResponse, MetadatafieldSuccessResponse, MetadataschemaSuccessResponse,
- RegistryBitstreamformatsSuccessResponse,
+ MetadatafieldSuccessResponse,
+ MetadataschemaSuccessResponse,
RegistryMetadatafieldsSuccessResponse,
- RegistryMetadataschemasSuccessResponse, RestResponse
+ RegistryMetadataschemasSuccessResponse,
+ RestResponse
} from '../cache/response.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service';
import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model';
-import { hasValue, hasNoValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
+import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { URLCombiner } from '../url-combiner/url-combiner';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service';
-import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model';
-import {
- configureRequest,
- getResponseFromEntry,
- getSucceededRemoteData
-} from '../shared/operators';
+import { configureRequest, getResponseFromEntry } from '../shared/operators';
import { createSelector, select, Store } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
@@ -52,9 +47,8 @@ import {
MetadataRegistrySelectFieldAction,
MetadataRegistrySelectSchemaAction
} 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 { ResourceType } from '../shared/resource-type';
import { NormalizedMetadataSchema } from '../metadata/normalized-metadata-schema.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
@@ -79,7 +73,8 @@ export class RegistryService {
private metadataSchemasPath = 'metadataschemas';
private metadataFieldsPath = 'metadatafields';
- private bitstreamFormatsPath = 'bitstreamformats';
+
+ // private bitstreamFormatsPath = 'bitstreamformats';
constructor(protected requestService: RequestService,
private rdb: RemoteDataBuildService,
@@ -197,7 +192,7 @@ export class RegistryService {
*/
public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable>> {
if (hasNoValue(pagination)) {
- pagination = { currentPage: 1, pageSize: 10000 } as any;
+ pagination = {currentPage: 1, pageSize: 10000} as any;
}
const requestObs = this.getMetadataFieldsRequestObs(pagination);
@@ -231,41 +226,7 @@ export class RegistryService {
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
}
- /**
- * Retrieves all bitstream formats
- * @param pagination The pagination info used to retrieve the bitstream formats
- */
- public getBitstreamFormats(pagination: PaginationComponentOptions): Observable>> {
- const requestObs = this.getBitstreamFormatsRequestObs(pagination);
-
- const requestEntryObs = requestObs.pipe(
- flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
- );
-
- const rbrObs: Observable = requestEntryObs.pipe(
- getResponseFromEntry(),
- map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse)
- );
-
- const bitstreamformatsObs: Observable = rbrObs.pipe(
- map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats)
- );
-
- const pageInfoObs: Observable = 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 {
+ public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable {
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
map((url: string) => {
const args: string[] = [];
@@ -327,30 +288,6 @@ export class RegistryService {
);
}
- private getBitstreamFormatsRequestObs(pagination: PaginationComponentOptions): Observable {
- 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 {
- 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) {
this.store.dispatch(new MetadataRegistryEditSchemaAction(schema));
}
@@ -374,7 +311,7 @@ export class RegistryService {
* @param schema The schema that's being selected
*/
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
*/
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
*/
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
*/
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
* @param field The field that's it being deselected
*/
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
*/
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));
}
} else {
- this.showNotifications(true, isUpdate, false, { prefix: schema.prefix });
+ this.showNotifications(true, isUpdate, false, {prefix: schema.prefix});
return response;
}
}),
@@ -521,7 +458,7 @@ export class RegistryService {
public clearMetadataSchemaRequests(): Observable {
return this.halService.getEndpoint(this.metadataSchemasPath).pipe(
tap((href: string) => this.requestService.removeByHrefSubstring(href))
- )
+ );
}
/**
@@ -571,7 +508,7 @@ export class RegistryService {
}
} else {
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;
}
}),
@@ -597,7 +534,7 @@ export class RegistryService {
public clearMetadataFieldRequests(): Observable {
return this.halService.getEndpoint(this.metadataFieldsPath).pipe(
tap((href: string) => this.requestService.removeByHrefSubstring(href))
- )
+ );
}
private delete(path: string, id: number): Observable {
@@ -633,9 +570,9 @@ export class RegistryService {
);
messages.subscribe(([head, content]) => {
if (success) {
- this.notificationsService.success(head, content)
+ this.notificationsService.success(head, content);
} else {
- this.notificationsService.error(head, content)
+ this.notificationsService.error(head, content);
}
});
}
diff --git a/src/app/core/shared/bitstream-format-support-level.ts b/src/app/core/shared/bitstream-format-support-level.ts
new file mode 100644
index 0000000000..d92aac7708
--- /dev/null
+++ b/src/app/core/shared/bitstream-format-support-level.ts
@@ -0,0 +1,5 @@
+export enum BitstreamFormatSupportLevel {
+ Known = 'KNOWN',
+ Unknown = 'UNKNOWN',
+ Supported = 'SUPPORTED'
+}
diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts
index bf50cd832f..0e1279e978 100644
--- a/src/app/core/shared/bitstream-format.model.ts
+++ b/src/app/core/shared/bitstream-format.model.ts
@@ -1,6 +1,6 @@
-
import { CacheableObject, TypedObject } from '../cache/object-cache.reducer';
import { ResourceType } from './resource-type';
+import { BitstreamFormatSupportLevel } from './bitstream-format-support-level';
/**
* 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
*/
- supportLevel: number;
+ supportLevel: BitstreamFormatSupportLevel;
/**
* 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
*/
- extensions: string;
+ extensions: string[];
/**
* The link to the rest endpoint where this Bitstream Format can be found
@@ -49,4 +49,11 @@ export class BitstreamFormat implements CacheableObject {
*/
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;
+
}
diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts
index 515ea3cd69..a3e625c022 100644
--- a/src/app/core/shared/item.model.ts
+++ b/src/app/core/shared/item.model.ts
@@ -5,10 +5,11 @@ import { DSpaceObject } from './dspace-object.model';
import { Collection } from './collection.model';
import { RemoteData } from '../data/remote-data';
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 { Relationship } from './item-relationships/relationship.model';
import { ResourceType } from './resource-type';
+import { getSucceededRemoteData } from './operators';
export class Item extends DSpaceObject {
static type = new ResourceType('item');
@@ -97,7 +98,7 @@ export class Item extends DSpaceObject {
*/
getBitstreamsByBundleName(bundleName: string): Observable {
return this.bitstreams.pipe(
- filter((rd: RemoteData>) => !rd.isResponsePending && isNotUndefined(rd.payload)),
+ getSucceededRemoteData(),
map((rd: RemoteData>) => rd.payload.page),
filter((bitstreams: Bitstream[]) => hasValue(bitstreams)),
take(1),
diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts
index ae46691e39..d46c688e68 100644
--- a/src/app/core/shared/operators.ts
+++ b/src/app/core/shared/operators.ts
@@ -91,7 +91,7 @@ export const toDSpaceObjectListRD = () =>
source.pipe(
filter((rd: RemoteData>>) => rd.hasSucceeded),
map((rd: RemoteData>>) => {
- const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.indexableObject);
+ const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult) => searchResult.indexableObject);
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList;
return Object.assign(rd, { payload: payload });
})
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html
new file mode 100644
index 0000000000..4cb34a140b
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts
new file mode 100644
index 0000000000..d13feda406
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts
new file mode 100644
index 0000000000..06c27ebacf
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html
new file mode 100644
index 0000000000..d7c9b68a24
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts
new file mode 100644
index 0000000000..8c854aeb77
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts
new file mode 100644
index 0000000000..e5183536ef
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html
new file mode 100644
index 0000000000..467cdd1594
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{dso.firstMetadataValue('creativework.editor')}}
+
+ ,
+ {{dso.firstMetadataValue('creativework.publisher')}}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts
new file mode 100644
index 0000000000..0d0f77842a
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts
new file mode 100644
index 0000000000..7f23211538
--- /dev/null
+++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts
index 50ec160650..4033645e1b 100644
--- a/src/app/entity-groups/journal-entities/journal-entities.module.ts
+++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts
@@ -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 { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component';
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 = [
JournalComponent,
@@ -16,7 +19,10 @@ const ENTRY_COMPONENTS = [
JournalVolumeComponent,
JournalListElementComponent,
JournalIssueListElementComponent,
- JournalVolumeListElementComponent
+ JournalVolumeListElementComponent,
+ JournalIssueGridElementComponent,
+ JournalVolumeGridElementComponent,
+ JournalGridElementComponent
];
@NgModule({
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html
new file mode 100644
index 0000000000..104d3a0a57
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{dso.firstMetadataValue('organization.address.addressCountry')}}
+
+ ,
+ {{dso.firstMetadataValue('organization.address.addressLocality')}}
+
+
+
+
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts
new file mode 100644
index 0000000000..15c7b75bf5
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts
new file mode 100644
index 0000000000..0effc22027
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html
new file mode 100644
index 0000000000..86353377fa
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts
new file mode 100644
index 0000000000..25268261e1
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts
new file mode 100644
index 0000000000..bf7b8aa119
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html
new file mode 100644
index 0000000000..a595791cc4
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html
@@ -0,0 +1,25 @@
+
+
+
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts
new file mode 100644
index 0000000000..969912976c
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts
@@ -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']));
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts
new file mode 100644
index 0000000000..15d525fcf2
--- /dev/null
+++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts
@@ -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 {
+}
diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts
index ba28f174df..099fa2a6a3 100644
--- a/src/app/entity-groups/research-entities/research-entities.module.ts
+++ b/src/app/entity-groups/research-entities/research-entities.module.ts
@@ -11,6 +11,9 @@ import { PersonMetadataListElementComponent } from './item-list-elements/person/
import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component';
import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component';
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 = [
OrgunitComponent,
@@ -20,7 +23,10 @@ const ENTRY_COMPONENTS = [
OrgUnitMetadataListElementComponent,
PersonListElementComponent,
PersonMetadataListElementComponent,
- ProjectListElementComponent
+ ProjectListElementComponent,
+ PersonGridElementComponent,
+ OrgunitGridElementComponent,
+ ProjectGridElementComponent
];
@NgModule({
diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts
index 6a96892b06..8d1d5c1dca 100644
--- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts
+++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts
@@ -9,6 +9,7 @@ import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynami
import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
+import { ResourceType } from '../../../core/shared/resource-type';
import { isNotEmpty } from '../../empty.util';
import { Community } from '../../../core/shared/community.model';
@@ -29,7 +30,7 @@ export class ComColFormComponent implements OnInit {
/**
* Type of DSpaceObject that the form represents
*/
- protected type;
+ protected type: ResourceType;
/**
* @type {string} Key prefix used to generate form labels
@@ -110,11 +111,11 @@ export class ComColFormComponent implements OnInit {
private updateFieldTranslations() {
this.formModel.forEach(
(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)) {
fieldModel.errorMessages = {};
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);
});
}
}
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html
index cead04f797..217f9e79cf 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html
@@ -14,7 +14,7 @@
-
+
{{ message | translate:model.validators }}
diff --git a/src/app/shared/items/item-type-decorator.ts b/src/app/shared/items/item-type-decorator.ts
index 2420e71908..3a040ae5bf 100644
--- a/src/app/shared/items/item-type-decorator.ts
+++ b/src/app/shared/items/item-type-decorator.ts
@@ -3,6 +3,7 @@ import { MetadataRepresentationType } from '../../core/shared/metadata-represent
export enum ItemViewMode {
Element = 'element',
+ Card = 'card',
Full = 'full',
Metadata = 'metadata'
}
diff --git a/src/app/shared/lang-switch/lang-switch.component.html b/src/app/shared/lang-switch/lang-switch.component.html
index 745facc95c..b61ec5592e 100644
--- a/src/app/shared/lang-switch/lang-switch.component.html
+++ b/src/app/shared/lang-switch/lang-switch.component.html
@@ -4,7 +4,7 @@