diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 63bc598304..f3236251cf 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", @@ -659,4 +690,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 @@ +
+
+
+

{{ 'admin.registries.bitstream-formats.create.new' | translate }}

+ + + +
+
+
\ 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 @@
- + + +

{{'admin.registries.bitstream-formats.description' | translate}}

+

{{'admin.registries.bitstream-formats.create.new' | translate}}

-

{{'admin.registries.bitstream-formats.description' | translate}}

- {{'admin.registries.bitstream-formats.formats.table.name' | translate}} - {{'admin.registries.bitstream-formats.formats.table.mimetype' | translate}} - {{'admin.registries.bitstream-formats.formats.table.supportLevel.head' | translate}} + + {{'admin.registries.bitstream-formats.table.name' | translate}} + {{'admin.registries.bitstream-formats.table.mimetype' | translate}} + {{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}} - {{bitstreamFormat.shortDescription}} - {{bitstreamFormat.mimetype}} ({{'admin.registries.bitstream-formats.formats.table.internal' | translate}}) - {{'admin.registries.bitstream-formats.formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}} + + + + {{bitstreamFormat.shortDescription}} + {{bitstreamFormat.mimetype}} ({{'admin.registries.bitstream-formats.table.internal' | translate}}) + {{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}
+
+ + +
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 @@ +
+
+
+

{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}

+ + + +
+
+
\ 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/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.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/core.module.ts b/src/app/core/core.module.ts index 1d22b5fefe..60d1bce3b8 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -107,6 +107,7 @@ import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing 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 +154,7 @@ const PROVIDERS = [ PaginationComponentOptions, ResourcePolicyService, RegistryService, + BitstreamFormatDataService, NormalizedObjectBuildService, RemoteDataBuildService, RequestService, 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/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/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/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 22a58dd7fc..c16a153026 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,9 +1,9 @@
-
- {{ 'pagination.showing.label' | translate }} - {{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}} +
+ {{ 'pagination.showing.label' | translate }} + {{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}}