diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 63bc5983042ef89081adf3b9703df4b276f63ff4..234967231e32e38e7786b2090c9288d3ebfbaf7b 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -2,16 +2,47 @@ "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", "404.link.home-page": "Take me to the home page", "404.page-not-found": "page not found", + "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", + "admin.registries.bitstream-formats.create.failure.head": "Failure", + "admin.registries.bitstream-formats.create.head": "Create Bitstream format", + "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", + "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", + "admin.registries.bitstream-formats.create.success.head": "Success", + "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.failure.head": "Failure", + "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.success.head": "Success", "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", - "admin.registries.bitstream-formats.formats.no-items": "No bitstream formats to show.", - "admin.registries.bitstream-formats.formats.table.internal": "internal", - "admin.registries.bitstream-formats.formats.table.mimetype": "MIME Type", - "admin.registries.bitstream-formats.formats.table.name": "Name", - "admin.registries.bitstream-formats.formats.table.supportLevel.0": "Unknown", - "admin.registries.bitstream-formats.formats.table.supportLevel.1": "Known", - "admin.registries.bitstream-formats.formats.table.supportLevel.2": "Support", - "admin.registries.bitstream-formats.formats.table.supportLevel.head": "Support Level", + "admin.registries.bitstream-formats.edit.description.hint": "", + "admin.registries.bitstream-formats.edit.description.label": "Description", + "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", + "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", + "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", + "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", + "admin.registries.bitstream-formats.edit.failure.head": "Failure", + "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", + "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.", + "admin.registries.bitstream-formats.edit.internal.label": "Internal", + "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", + "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", + "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", + "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", + "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", + "admin.registries.bitstream-formats.edit.success.head": "Success", + "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", + "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", "admin.registries.bitstream-formats.head": "Bitstream Format Registry", + "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", + "admin.registries.bitstream-formats.table.delete": "Delete selected", + "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", + "admin.registries.bitstream-formats.table.internal": "internal", + "admin.registries.bitstream-formats.table.mimetype": "MIME Type", + "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.return": "Return", + "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", + "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", + "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", + "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", "admin.registries.metadata.form.create": "Create metadata schema", @@ -101,6 +132,7 @@ "collection.form.tableofcontents": "News (HTML)", "collection.form.title": "Name", "collection.page.browse.recent.head": "Recent Submissions", + "collection.page.browse.recent.empty": "No items to show", "collection.page.license": "License", "collection.page.news": "News", "community.create.head": "Create a Community", @@ -659,4 +691,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 8e3c322bc827a9616de2bc4ddd5da2a9dc2386f4..afdc46bf172a41b27130edcc379bfb92eb02861a 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 c7890e669718629c02abcdc751bce22181432709..bbeb59f0abf65b59e27c3a693769e805c28c8d79 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 0000000000000000000000000000000000000000..2b65b369b2960b18b6156099e6a3f303b306177e --- /dev/null +++ b/src/app/+admin/admin-registries/bitstream-formats/add-bitstream-format/add-bitstream-format.component.html @@ -0,0 +1,11 @@ +<div class="container"> + <div class="row"> + <div class="col-12 mb-4"> + <h2 id="sub-header" + class="border-bottom mb-2">{{ 'admin.registries.bitstream-formats.create.new' | translate }}</h2> + + <ds-bitstream-format-form (updatedFormat)="createBitstreamFormat($event)"></ds-bitstream-format-form> + + </div> + </div> +</div> \ 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 0000000000000000000000000000000000000000..0a10633956ca0a9f1179bc8a36ec90707ad35bda --- /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<AddBitstreamFormatComponent>; + + const bitstreamFormat = new BitstreamFormat(); + bitstreamFormat.uuid = 'test-uuid-1'; + bitstreamFormat.id = 'test-uuid-1'; + bitstreamFormat.shortDescription = 'Unknown'; + bitstreamFormat.description = 'Unknown data format'; + bitstreamFormat.mimetype = 'application/octet-stream'; + bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown; + bitstreamFormat.internal = false; + bitstreamFormat.extensions = null; + + let router; + let notificationService: NotificationsServiceStub; + let bitstreamFormatDataService: BitstreamFormatDataService; + + const initAsync = () => { + router = new RouterStub(); + notificationService = new NotificationsServiceStub(); + bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { + createBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')), + clearBitStreamFormatRequests: observableOf(null) + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [AddBitstreamFormatComponent], + providers: [ + {provide: Router, useValue: router}, + {provide: NotificationsService, useValue: notificationService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }; + + const initBeforeEach = () => { + fixture = TestBed.createComponent(AddBitstreamFormatComponent); + comp = fixture.componentInstance; + + fixture.detectChanges(); + }; + + describe('createBitstreamFormat success', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should send the updated form to the service, show a notification and navigate to ', () => { + comp.createBitstreamFormat(bitstreamFormat); + + expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat); + expect(notificationService.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']); + + }); + }); + describe('createBitstreamFormat error', () => { + beforeEach(async(() => { + router = new RouterStub(); + notificationService = new NotificationsServiceStub(); + bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { + createBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')), + clearBitStreamFormatRequests: observableOf(null) + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [AddBitstreamFormatComponent], + providers: [ + {provide: Router, useValue: router}, + {provide: NotificationsService, useValue: notificationService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + beforeEach(initBeforeEach); + it('should send the updated form to the service, show a notification and navigate to ', () => { + comp.createBitstreamFormat(bitstreamFormat); + + expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat); + expect(notificationService.error).toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + + }); + }); +}); 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 0000000000000000000000000000000000000000..9712be70cade4539d58968e3dbc19af48f9e0e7d --- /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 0000000000000000000000000000000000000000..58b0686dfd7809abd201cd9659b992eb5e52e07e --- /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 0000000000000000000000000000000000000000..76576afc7a134db4502b5febb6946ce67b502e83 --- /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 0000000000000000000000000000000000000000..41880bf16cd37e29a500184832b74eb071e05295 --- /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 0000000000000000000000000000000000000000..67f6aa373e5f50545369ea5032310d422e568ffa --- /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 1ac547653ff972713028e67f3a832cab84a5342e..e5cf7cf5ecc338d4c76057e207455f795623c1df 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 @@ <div class="bitstream-formats row"> <div class="col-12"> - <h2 id="header" class="border-bottom pb-2">{{'admin.registries.bitstream-formats.head' | translate}}</h2> + <h2 id="header" class="border-bottom pb-2 ">{{'admin.registries.bitstream-formats.head' | translate}}</h2> + + <p id="description">{{'admin.registries.bitstream-formats.description' | translate}}</p> + <p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p> - <p id="description" class="pb-2">{{'admin.registries.bitstream-formats.description' | translate}}</p> <ds-pagination *ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0" - [paginationOptions]="config" + [paginationOptions]="pageConfig" [pageInfoState]="(bitstreamFormats | async)?.payload" [collectionSize]="(bitstreamFormats | async)?.payload?.totalElements" [hideGear]="true" @@ -18,25 +20,38 @@ <table id="formats" class="table table-striped table-hover"> <thead> <tr> - <th scope="col">{{'admin.registries.bitstream-formats.formats.table.name' | translate}}</th> - <th scope="col">{{'admin.registries.bitstream-formats.formats.table.mimetype' | translate}}</th> - <th scope="col">{{'admin.registries.bitstream-formats.formats.table.supportLevel.head' | translate}}</th> + <th scope="col"></th> + <th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th> + <th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th> + <th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th> </tr> </thead> <tbody> <tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page"> - <td>{{bitstreamFormat.shortDescription}}</td> - <td>{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.formats.table.internal' | translate}})</span></td> - <td>{{'admin.registries.bitstream-formats.formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</td> + <td> + <label> + <input type="checkbox" + [checked]="isSelected(bitstreamFormat) | async" + (change)="selectBitStreamFormat(bitstreamFormat, $event)" + > + </label> + </td> + <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td> + <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td> + <td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td> </tr> </tbody> </table> </div> </ds-pagination> <div *ngIf="(bitstreamFormats | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert"> - {{'admin.registries.bitstream-formats.formats.no-items' | translate}} + {{'admin.registries.bitstream-formats.no-items' | translate}} </div> + <div> + <button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button> + <button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button> + </div> </div> </div> </div> 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 3a680c906b7dbd3c8f2d528df06cb623ea4d57ca..e672dc82ea5ce3ed3602eb9b249144d55edbee87 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<BitstreamFormatsComponent>; - 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 - } + 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 mockFormats = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFormatsList)); - const registryServiceStub = { - getBitstreamFormats: () => mockFormats - }; + 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'); + }); + }); + + 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 four formats', () => { - const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement; - expect(tbody.children.length).toBe(4); + describe('isSelected', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should return an observable of true if the provided bistream is in the list returned by the service', () => { + const result = comp.isSelected(bitstreamFormat1); + + expect(result).toBeObservable(cold('b', {b: true})); + }); + it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { + const format = new BitstreamFormat(); + format.uuid = 'new'; + + const result = comp.isSelected(format); + + expect(result).toBeObservable(cold('b', {b: false})); + }); + }); + + describe('deselectAll', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should deselect all bitstreamFormats', () => { + comp.deselectAll(); + expect(bitstreamFormatService.deselectAllBitstreamFormats).toHaveBeenCalled(); + }); + + it('should be called when the deselect all button is clicked', () => { + spyOn(comp, 'deselectAll'); + const deselectAllButton = fixture.debugElement.query(By.css('button.deselect')); + deselectAllButton.triggerEventHandler('click', null); + + expect(comp.deselectAll).toHaveBeenCalled(); + + }); }); - 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('deleteFormats success', () => { + beforeEach(async(() => { + notificationsServiceStub = new NotificationsServiceStub(); - const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(1)')).nativeElement; - expect(licenseName.textContent).toBe('License'); + scheduler = getTestScheduler(); - const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(1)')).nativeElement; - expect(ccLicenseName.textContent).toBe('CC License'); + 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') + }); - const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(1)')).nativeElement; - expect(adobeName.textContent).toBe('Adobe PDF'); + 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 bc0cbb8da61fdd838e06f7292b65fc29f7934659..cb7aa1ef91d7e88a39177f3bf3e1df0f4d14d518 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<RemoteData<PaginatedList<BitstreamFormat>>>; + /** + * A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats + */ + pageState: BehaviorSubject<string>; + + /** + * The current pagination configuration for the page used by the FindAll method + * Currently simply renders all bitstream formats + */ + config: FindAllOptions = Object.assign(new FindAllOptions(), { + elementsPerPage: 20 + }); + /** * The current pagination configuration for the page * 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<boolean> { + return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( + map((bitstreamFormats: BitstreamFormat[]) => { + return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null; + }) + ); + } + + /** + * Selects or deselects a bitstream format based on the checkbox state + * @param bitstreamFormat + * @param event + */ + selectBitStreamFormat(bitstreamFormat: BitstreamFormat, event) { + event.target.checked ? + this.bitstreamFormatService.selectBitstreamFormat(bitstreamFormat) : + this.bitstreamFormatService.deselectBitstreamFormat(bitstreamFormat); + } + + /** + * Show notifications for an amount of deleted bitstream formats + * @param success Whether or not the notification should be a success message (error message when false) + * @param amount The amount of deleted bitstream formats + */ + private showNotification(success: boolean, amount: number) { + const prefix = 'admin.registries.bitstream-formats.delete'; + const suffix = success ? 'success' : 'failure'; + + const messages = observableCombineLatest( + this.translateService.get(`${prefix}.${suffix}.head`), + this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount}) + ); + messages.subscribe(([head, content]) => { + + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } + }); } /** @@ -38,14 +145,26 @@ export class BitstreamFormatsComponent { * @param event The page change event */ 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 0000000000000000000000000000000000000000..0800c501692435f6486a8654a6c89bf7352144df --- /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 0000000000000000000000000000000000000000..f6eef741fd3719c23fefcd49b3f0f832f53e0e0f --- /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<RemoteData<BitstreamFormat>> { + constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { + } + + /** + * Method for resolving an bitstreamFormat based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<<RemoteData<BitstreamFormat>> Emits the found bitstreamFormat based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<BitstreamFormat>> { + return this.bitstreamFormatDataService.findById(route.params.id) + .pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} 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 0000000000000000000000000000000000000000..f57ec9cd382a117e1b97fe95db16d7b03a26db81 --- /dev/null +++ b/src/app/+admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.html @@ -0,0 +1,11 @@ +<div class="container"> + <div class="row"> + <div class="col-12 mb-4"> + <h2 id="sub-header" + class="border-bottom mb-2">{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}</h2> + + <ds-bitstream-format-form [bitstreamFormat]="(bitstreamFormatRD$ | async)?.payload" (updatedFormat)="updateFormat($event)"></ds-bitstream-format-form> + + </div> + </div> +</div> \ 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 0000000000000000000000000000000000000000..cfa93a15a84c80d74e26a4fb1b895e7c7c7ba58c --- /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<EditBitstreamFormatComponent>; + + const bitstreamFormat = new BitstreamFormat(); + bitstreamFormat.uuid = 'test-uuid-1'; + bitstreamFormat.id = 'test-uuid-1'; + bitstreamFormat.shortDescription = 'Unknown'; + bitstreamFormat.description = 'Unknown data format'; + bitstreamFormat.mimetype = 'application/octet-stream'; + bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown; + bitstreamFormat.internal = false; + bitstreamFormat.extensions = null; + + const routeStub = { + data: observableOf({ + bitstreamFormat: new RemoteData(false, false, true, null, bitstreamFormat) + }) + }; + + let router; + let notificationService: NotificationsServiceStub; + let bitstreamFormatDataService: BitstreamFormatDataService; + + const initAsync = () => { + router = new RouterStub(); + notificationService = new NotificationsServiceStub(); + bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { + updateBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')) + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [EditBitstreamFormatComponent], + providers: [ + {provide: ActivatedRoute, useValue: routeStub}, + {provide: Router, useValue: router}, + {provide: NotificationsService, useValue: notificationService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }; + + const initBeforeEach = () => { + fixture = TestBed.createComponent(EditBitstreamFormatComponent); + comp = fixture.componentInstance; + + fixture.detectChanges(); + }; + + describe('init', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should initialise the bitstreamFormat based on the route', () => { + + comp.bitstreamFormatRD$.subscribe((format: RemoteData<BitstreamFormat>) => { + expect(format).toEqual(new RemoteData(false, false, true, null, bitstreamFormat)); + }); + }); + }); + describe('updateFormat success', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should send the updated form to the service, show a notification and navigate to ', () => { + comp.updateFormat(bitstreamFormat); + + expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat); + expect(notificationService.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']); + + }); + }); + describe('updateFormat error', () => { + beforeEach(async( () => { + router = new RouterStub(); + notificationService = new NotificationsServiceStub(); + bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', { + updateBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')) + }); + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [EditBitstreamFormatComponent], + providers: [ + {provide: ActivatedRoute, useValue: routeStub}, + {provide: Router, useValue: router}, + {provide: NotificationsService, useValue: notificationService}, + {provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + })); + beforeEach(initBeforeEach); + it('should send the updated form to the service, show a notification and navigate to ', () => { + comp.updateFormat(bitstreamFormat); + + expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat); + expect(notificationService.error).toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + + }); + }); +}); 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 0000000000000000000000000000000000000000..0fdcc75689b3552929b0f9bdef414b404921eeb5 --- /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<RemoteData<BitstreamFormat>>; + + constructor( + private route: ActivatedRoute, + private router: Router, + private notificationService: NotificationsService, + private translateService: TranslateService, + private bitstreamFormatDataService: BitstreamFormatDataService, + ) { + } + + ngOnInit(): void { + this.bitstreamFormatRD$ = this.route.data.pipe( + map((data) => data.bitstreamFormat as RemoteData<BitstreamFormat>) + ); + } + + /** + * Updates the bitstream format based on the provided bitstream format emitted by the form. + * When successful, a success notification will be shown and the user will be navigated back to the overview page. + * When failed, an error notification will be shown. + */ + updateFormat(bitstreamFormat: BitstreamFormat) { + this.bitstreamFormatDataService.updateBitstreamFormat(bitstreamFormat).pipe(take(1) + ).subscribe((response: RestResponse) => { + if (response.isSuccessful) { + this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.edit.success.head'), + this.translateService.get('admin.registries.bitstream-formats.edit.success.content')); + this.router.navigate([getBitstreamFormatsModulePath()]); + } else { + this.notificationService.error('admin.registries.bitstream-formats.edit.failure.head', + 'admin.registries.bitstream-formats.create.edit.content'); + } + } + ); + } +} 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 0000000000000000000000000000000000000000..be6ebf2599333fe332be03de4435a1aefa7b9d11 --- /dev/null +++ b/src/app/+admin/admin-registries/bitstream-formats/format-form/format-form.component.html @@ -0,0 +1,3 @@ +<ds-form *ngIf="formModel" + [formId]="'comcol-form-id'" + [formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form> \ 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 0000000000000000000000000000000000000000..2870705fc8fa2b6d27e6307a434e5abca6ebd330 --- /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<FormatFormComponent>; + + const router = new RouterStub(); + + const bitstreamFormat = new BitstreamFormat(); + bitstreamFormat.uuid = 'test-uuid-1'; + bitstreamFormat.id = 'test-uuid-1'; + bitstreamFormat.shortDescription = 'Unknown'; + bitstreamFormat.description = 'Unknown data format'; + bitstreamFormat.mimetype = 'application/octet-stream'; + bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown; + bitstreamFormat.internal = false; + bitstreamFormat.extensions = []; + + const submittedBitstreamFormat = new BitstreamFormat(); + submittedBitstreamFormat.id = bitstreamFormat.id; + submittedBitstreamFormat.shortDescription = bitstreamFormat.shortDescription; + submittedBitstreamFormat.mimetype = bitstreamFormat.mimetype; + submittedBitstreamFormat.description = bitstreamFormat.description; + submittedBitstreamFormat.supportLevel = bitstreamFormat.supportLevel; + submittedBitstreamFormat.internal = bitstreamFormat.internal; + submittedBitstreamFormat.extensions = bitstreamFormat.extensions; + + const initAsync = () => { + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([]), ReactiveFormsModule, FormsModule, TranslateModule.forRoot(), NgbModule.forRoot()], + declarations: [FormatFormComponent], + providers: [ + {provide: Router, useValue: router}, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + }; + + const initBeforeEach = () => { + fixture = TestBed.createComponent(FormatFormComponent); + comp = fixture.componentInstance; + + comp.bitstreamFormat = bitstreamFormat; + fixture.detectChanges(); + }; + + describe('initialise', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + it('should initialises the values in the form', () => { + + expect((comp.formModel[0] as DynamicInputModel).value).toBe(bitstreamFormat.shortDescription); + expect((comp.formModel[1] as DynamicInputModel).value).toBe(bitstreamFormat.mimetype); + expect((comp.formModel[2] as DynamicInputModel).value).toBe(bitstreamFormat.description); + expect((comp.formModel[3] as DynamicInputModel).value).toBe(bitstreamFormat.supportLevel); + expect((comp.formModel[4] as DynamicCheckboxModel).value).toBe(bitstreamFormat.internal); + + const formArray = (comp.formModel[5] as DynamicFormArrayModel); + const extensions = []; + for (let i = 0; i < formArray.groups.length; i++) { + const value = (formArray.get(i).get(0) as DynamicInputModel).value; + if (!isEmpty(value)) { + extensions.push((formArray.get(i).get(0) as DynamicInputModel).value); + } + } + + expect(extensions).toEqual(bitstreamFormat.extensions); + + }); + }); + describe('onSubmit', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + + it('should emit the bitstreamFormat currently present in the form', () => { + spyOn(comp.updatedFormat, 'emit'); + comp.onSubmit(); + + expect(comp.updatedFormat.emit).toHaveBeenCalledWith(submittedBitstreamFormat); + }); + }); + describe('onCancel', () => { + beforeEach(async(initAsync)); + beforeEach(initBeforeEach); + + it('should navigate back to the bitstream overview', () => { + comp.onCancel(); + expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']); + }); + }); +}); 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 0000000000000000000000000000000000000000..505ccccd91a9489bdc25216da7efb24b862d6c41 --- /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<BitstreamFormat> = new EventEmitter<BitstreamFormat>(); + + /** + * The different supported support level of the bitstream format + */ + supportLevelOptions = [{label: BitstreamFormatSupportLevel.Known, value: BitstreamFormatSupportLevel.Known}, + {label: BitstreamFormatSupportLevel.Unknown, value: BitstreamFormatSupportLevel.Unknown}, + {label: BitstreamFormatSupportLevel.Supported, value: BitstreamFormatSupportLevel.Supported}]; + + /** + * Styling element for repeatable field + */ + arrayElementLayout: DynamicFormControlLayout = { + grid: { + group: 'form-row', + }, + }; + + /** + * Styling element for element of repeatable field + */ + arrayInputElementLayout: DynamicFormControlLayout = { + grid: { + host: 'col' + } + }; + + /** + * The form model representing the bitstream format + */ + formModel: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'shortDescription', + name: 'shortDescription', + label: 'admin.registries.bitstream-formats.edit.shortDescription.label', + hint: 'admin.registries.bitstream-formats.edit.shortDescription.hint', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'Please enter a name for this bitstream format' + }, + }), + new DynamicInputModel({ + id: 'mimetype', + name: 'mimetype', + label: 'admin.registries.bitstream-formats.edit.mimetype.label', + hint: 'admin.registries.bitstream-formats.edit.mimetype.hint', + + }), + new DynamicTextAreaModel({ + id: 'description', + name: 'description', + label: 'admin.registries.bitstream-formats.edit.description.label', + hint: 'admin.registries.bitstream-formats.edit.description.hint', + + }), + new DynamicSelectModel({ + id: 'supportLevel', + name: 'supportLevel', + options: this.supportLevelOptions, + label: 'admin.registries.bitstream-formats.edit.supportLevel.label', + hint: 'admin.registries.bitstream-formats.edit.supportLevel.hint', + value: this.supportLevelOptions[0].value + + }), + new DynamicCheckboxModel({ + id: 'internal', + name: 'internal', + label: 'Internal', + hint: 'admin.registries.bitstream-formats.edit.internal.hint', + }), + new DynamicFormArrayModel({ + id: 'extensions', + name: 'extensions', + label: 'admin.registries.bitstream-formats.edit.extensions.label', + groupFactory: () => [ + new DynamicInputModel({ + id: 'extension', + placeholder: 'admin.registries.bitstream-formats.edit.extensions.placeholder', + }, this.arrayInputElementLayout) + ] + }, this.arrayElementLayout), + ]; + + constructor(private dynamicFormService: DynamicFormService, + private translateService: TranslateService, + private router: Router) { + + } + + ngOnInit(): void { + + this.initValues(); + } + + /** + * Initializes the form based on the provided bitstream format + */ + initValues() { + this.formModel.forEach( + (fieldModel: DynamicFormControlModel) => { + if (fieldModel.name === 'extensions') { + if (hasValue(this.bitstreamFormat.extensions)) { + const extenstions = this.bitstreamFormat.extensions; + const formArray = (fieldModel as DynamicFormArrayModel); + for (let i = 0; i < extenstions.length; i++) { + formArray.insertGroup(i).group[0] = new DynamicInputModel({ + id: `extension-${i}`, + value: extenstions[i] + }, this.arrayInputElementLayout); + } + } + } else { + if (hasValue(this.bitstreamFormat[fieldModel.name])) { + (fieldModel as DynamicInputModel).value = this.bitstreamFormat[fieldModel.name]; + } + } + }); + } + + /** + * Creates an updated bistream format based on the current values in the form + * Emits the updated bitstream format trouhg the updatedFormat emitter + */ + onSubmit() { + const updatedBitstreamFormat = Object.assign(new BitstreamFormat(), + { + id: this.bitstreamFormat.id + }); + + this.formModel.forEach( + (fieldModel: DynamicFormControlModel) => { + if (fieldModel.name === 'extensions') { + const formArray = (fieldModel as DynamicFormArrayModel); + const extensions = []; + for (let i = 0; i < formArray.groups.length; i++) { + const value = (formArray.get(i).get(0) as DynamicInputModel).value; + if (!isEmpty(value)) { + extensions.push((formArray.get(i).get(0) as DynamicInputModel).value); + } + } + updatedBitstreamFormat.extensions = extensions; + } else { + updatedBitstreamFormat[fieldModel.name] = (fieldModel as DynamicInputModel).value; + } + }); + this.updatedFormat.emit(updatedBitstreamFormat); + } + + /** + * Cancels the edit/create action of the bitstream format and navigates back to the bitstream format registry + */ + onCancel() { + this.router.navigate([getBitstreamFormatsModulePath()]); + } +} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index 71af51c68347cb9198a92ed0b8ee0cef409cc6ab..2003ecf124a004de9698ed055c824c5a8cdc1242 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,11 +1,19 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { getAdminModulePath } from '../app-routing.module'; + +const REGISTRIES_MODULE_PATH = 'registries'; + +export function getRegistriesModulePath() { + return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString(); +} @NgModule({ imports: [ RouterModule.forChild([ { - path: 'registries', + path: REGISTRIES_MODULE_PATH, loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule' } ]) diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 91239de17c64de4fa8a0b4351b0503c557645f64..2b16bc1ca688a9c797d0a38340432c8a72bc001d 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -52,6 +52,9 @@ message="{{'error.recent-submissions' | translate}}"></ds-error> <ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading> + <div *ngIf="!itemRD?.isLoading && itemRD?.payload?.page.length === 0" class="alert alert-info w-100" role="alert"> + {{'collection.page.browse.recent.empty' | translate}} + </div> </ng-container> </div> <ds-error *ngIf="collectionRD?.hasFailed" diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 6743028b6c37760bb621bcfb0e41026356c284de..f510ccf19be23185e4282993bf92436bd40306bf 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -62,7 +62,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field GenericItemPageFieldComponent, RelatedEntitiesSearchComponent, RelatedItemsComponent, - MetadataRepresentationListComponent + MetadataRepresentationListComponent, + ItemPageTitleFieldComponent ], entryComponents: [ PublicationComponent diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 86364aca895d831432b60da2d21137da9e20180a..e1ddc2b8895752bbe1002933ae58072677b068f3 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 ea2512a974464c042ff49a36f28cfbfaf4217936..e3333fb34a279d0059d520c2cab4d3d08a2dbe06 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<AppState> = { 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 5ee135b530a8e629288b15243f58cae7c9a6219a..2283ecb368c47d63b8f0f3ba7f42a5b7b7ee1d42 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<BitstreamFormat> * 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<BitstreamFormat> * 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 1d22b5fefe23467c0225a7d8cc37b62ec185ae17..60d1bce3b8be7621f2c1b2f95084b910bf77a066 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 0000000000000000000000000000000000000000..f3ce4782366df8c6fff3d7fd5f6e3529f2f2d151 --- /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<CoreState>; + + const objectCache = {} as ObjectCacheService; + const halEndpointService = { + getEndpoint(linkPath: string): Observable<string> { + return cold('a', {a: bitstreamFormatsEndpoint}); + } + } as HALEndpointService; + + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + const rdbService = {} as RemoteDataBuildService; + + function initTestService(halService) { + return new BitstreamFormatDataService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + describe('getBrowseEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the browse endpoint', () => { + const result = service.getBrowseEndpoint(); + const expected = cold('b', {b: bitstreamFormatsEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getUpdateEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the update endpoint', () => { + const formatId = 'format-id'; + + const result = service.getUpdateEndpoint(formatId); + const expected = cold('b', {b: bitstreamFormatsIdEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('getCreateEndpoint', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should get the create endpoint ', () => { + + const result = service.getCreateEndpoint(); + const expected = cold('b', {b: bitstreamFormatsEndpoint}); + + expect(result).toBeObservable(expected); + }); + }); + + describe('updateBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should update the bitstream format', () => { + const updatedBistreamFormat = new BitstreamFormat(); + updatedBistreamFormat.uuid = 'updated-uuid'; + + const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const result = service.updateBitstreamFormat(updatedBistreamFormat); + + expect(result).toBeObservable(expected); + + }); + }); + + describe('createBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + })); + it('should create a new bitstream format', () => { + const newFormat = new BitstreamFormat(); + newFormat.uuid = 'new-uuid'; + + const expected = cold('(b)', {b: new RestResponse(true, 200, 'Success')}); + const result = service.createBitstreamFormat(newFormat); + + expect(result).toBeObservable(expected); + }); + }); + + describe('clearBitStreamFormatRequests', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + const halService = { + getEndpoint(linkPath: string): Observable<string> { + return observableOf(bitstreamFormatsEndpoint); + } + } as HALEndpointService; + service = initTestService(halService); + service.clearBitStreamFormatRequests().subscribe(); + })); + it('should remove the bitstream format hrefs in the request service', () => { + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(bitstreamFormatsEndpoint); + }); + }); + + describe('selectBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + })); + it('should add a selected bitstream to the store', () => { + const format = new BitstreamFormat(); + format.uuid = 'uuid'; + + service.selectBitstreamFormat(format); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistrySelectAction(format)); + }); + }); + + describe('deselectBitstreamFormat', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + })); + it('should remove a bitstream from the store', () => { + const format = new BitstreamFormat(); + format.uuid = 'uuid'; + + service.deselectBitstreamFormat(format); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAction(format)); + }); + }); + + describe('deselectAllBitstreamFormats', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: cold('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + service = initTestService(halEndpointService); + spyOn(store, 'dispatch'); + + })); + it('should remove all bitstreamFormats from the store', () => { + service.deselectAllBitstreamFormats(); + expect(store.dispatch).toHaveBeenCalledWith(new BitstreamFormatsRegistryDeselectAllAction()); + }); + }); + + describe('delete', () => { + beforeEach(async(() => { + scheduler = getTestScheduler(); + requestService = jasmine.createSpyObj('requestService', { + configure: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: hot('a', {a: responseCacheEntry}), + generateRequestId: 'request-id', + removeByHrefSubstring: {} + }); + const halService = { + getEndpoint(linkPath: string): Observable<string> { + return observableOf(bitstreamFormatsEndpoint); + } + } as HALEndpointService; + service = initTestService(halService); + })); + it('should delete a bitstream format', () => { + const format = new BitstreamFormat(); + format.uuid = 'format-uuid'; + format.id = 'format-id'; + + const expected = cold('(b|)', {b: true}); + const result = service.delete(format); + + expect(result).toBeObservable(expected); + }); + }); +}); 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 0000000000000000000000000000000000000000..a5638183c01031b463a17d9a1a35d0615a5ec8da --- /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<BitstreamFormat> { + + protected linkPath = 'bitstreamformats'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store<CoreState>, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer<BitstreamFormat>) { + super(); + } + + /** + * Get the endpoint for browsing bitstream formats + * @param {FindAllOptions} options + * @returns {Observable<string>} + */ + getBrowseEndpoint(options: FindAllOptions = {}, linkPath?: string): Observable<string> { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Get the endpoint to update an existing bitstream format + * @param formatId + */ + public getUpdateEndpoint(formatId: string): Observable<string> { + return this.getBrowseEndpoint().pipe( + map((endpoint: string) => this.getIDHref(endpoint, formatId)) + ); + } + + /** + * Get the endpoint to create a new bitstream format + */ + public getCreateEndpoint(): Observable<string> { + return this.getBrowseEndpoint(); + } + + /** + * Update an existing bitstreamFormat + * @param bitstreamFormat + */ + updateBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> { + const requestId = this.requestService.generateRequestId(); + + this.getUpdateEndpoint(bitstreamFormat.id).pipe( + distinctUntilChanged(), + map((endpointURL: string) => + new PutRequest(requestId, endpointURL, bitstreamFormat)), + configureRequest(this.requestService)).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + + } + + /** + * Create a new BitstreamFormat + * @param BitstreamFormat + */ + public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> { + const requestId = this.requestService.generateRequestId(); + + this.getCreateEndpoint().pipe( + map((endpointURL: string) => { + return new PostRequest(requestId, endpointURL, bitstreamFormat); + }), + configureRequest(this.requestService) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry() + ); + } + + /** + * Clears the cache of the list of BitstreamFormats + */ + public clearBitStreamFormatRequests(): Observable<string> { + return this.getBrowseEndpoint().pipe( + tap((href: string) => this.requestService.removeByHrefSubstring(href)) + ); + } + + /** + * Gets all the selected BitstreamFormats from the store + */ + public getSelectedBitstreamFormats(): Observable<BitstreamFormat[]> { + return this.store.pipe(select(selectedBitstreamFormatSelector)); + } + + /** + * Adds a BistreamFormat to the selected BitstreamFormats in the store + * @param bitstreamFormat + */ + public selectBitstreamFormat(bitstreamFormat: BitstreamFormat) { + this.store.dispatch(new BitstreamFormatsRegistrySelectAction(bitstreamFormat)); + } + + /** + * Removes a BistreamFormat from the list of selected BitstreamFormats in the store + * @param bitstreamFormat + */ + public deselectBitstreamFormat(bitstreamFormat: BitstreamFormat) { + this.store.dispatch(new BitstreamFormatsRegistryDeselectAction(bitstreamFormat)); + } + + /** + * Removes all BitstreamFormats from the list of selected BitstreamFormats in the store + */ + public deselectAllBitstreamFormats() { + this.store.dispatch(new BitstreamFormatsRegistryDeselectAllAction()); + } + + /** + * Delete an existing DSpace Object on the server + * @param format The DSpace Object to be removed + * Return an observable that emits true when the deletion was successful, false when it failed + */ + delete(format: BitstreamFormat): Observable<boolean> { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, format.id))); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, format.id); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } +} 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 f5811e367c77494b9800504a9359a8cd0312d663..0000000000000000000000000000000000000000 --- 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 47e306d62483e06988c518b775f440b4d24c9e31..455a8043dab4bb2ed4932e0d41a1b0e74c8f5e70 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<RequestEntry>, payloadObs: Observable<any>) => { 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 d816c5eab8681f860bfe278da967e66a0eab28f7..206426588e31a9ddf24c2fd97c3e42e8eb9d8b0f 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<RemoteData<PaginatedList<MetadataField>>> { 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<RemoteData<PaginatedList<BitstreamFormat>>> { - const requestObs = this.getBitstreamFormatsRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rbrObs: Observable<RegistryBitstreamformatsResponse> = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse) - ); - - const bitstreamformatsObs: Observable<BitstreamFormat[]> = rbrObs.pipe( - map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats) - ); - - const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(bitstreamformatsObs, pageInfoObs).pipe( - map(([bitstreamformats, pageInfo]) => { - return new PaginatedList(pageInfo, bitstreamformats); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); - } - - private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> { + public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable<RestRequest> { 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<RestRequest> { - return this.halService.getEndpoint(this.bitstreamFormatsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor<ResponseParsingService> { - return RegistryBitstreamformatsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - - /** - * Method to start editing a metadata schema, dispatches an edit schema action - * @param schema The schema that's being edited - */ public editMetadataSchema(schema: MetadataSchema) { 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<string> { 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<string> { return this.halService.getEndpoint(this.metadataFieldsPath).pipe( tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ) + ); } private delete(path: string, id: number): Observable<RestResponse> { @@ -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 0000000000000000000000000000000000000000..d92aac7708368c649532e4a678f30408a2d062c0 --- /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 bf50cd832f552962fca735319c4bd2784855cc57..0e1279e97844bed638ff6b677cf9bbb6ce783c9f 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/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4cb34a140b179b0161a5cf6632e7e1a7831aab78 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -0,0 +1,30 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('creativework.datePublished')" class="item-date card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('journal.title')" class="item-journal-title card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('journal.title')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..68a05b66c3cf2c84fc918be1056044a03a3a583a --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalIssueGridElementComponent } from './journal-issue-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.datePublished': [ + { + language: null, + value: '2015-06-26' + } + ], + 'journal.title': [ + { + language: 'en_US', + value: 'The journal title' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalIssueGridElementComponent', getEntityGridElementTestComponent(JournalIssueGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'journal-title'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..06c27ebacf60ab6cc05b64739a174d0f3a19b2e5 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalIssue', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-issue-grid-element', + styleUrls: ['./journal-issue-grid-element.component.scss'], + templateUrl: './journal-issue-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal Issue + */ +export class JournalIssueGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d7c9b68a24e92f25e7fcd9136bf27e0435a39a55 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -0,0 +1,30 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('creativework.datePublished')" class="item-date card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('dc.description')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a8fca5fc689626c5a96bd56ec144d06d0e21cdd --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalVolumeGridElementComponent } from './journal-volume-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.datePublished': [ + { + language: null, + value: '2015-06-26' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'A description for the journal volume' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalVolumeGridElementComponent', getEntityGridElementTestComponent(JournalVolumeGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'description'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5183536ef710c636079cda85e4b30884bea0079 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('JournalVolume', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-volume-grid-element', + styleUrls: ['./journal-volume-grid-element.component.scss'], + templateUrl: './journal-volume-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal Volume + */ +export class JournalVolumeGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..467cdd15941084097624c03d0911608373e26538 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -0,0 +1,35 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('creativework.editor')" + class="item-publisher card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span class="item-editor">{{dso.firstMetadataValue('creativework.editor')}}</span> + <span *ngIf="dso.hasMetadata('creativework.publisher')" class="item-publisher"> + <span>, </span> + {{dso.firstMetadataValue('creativework.publisher')}} + </span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('dc.description')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c12c1a2667c07838ce04afa0e77165d91d33ee3 --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts @@ -0,0 +1,53 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { JournalGridElementComponent } from './journal-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'creativework.editor': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'creativework.publisher': [ + { + language: 'en_US', + value: 'A company' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'This is the description' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('JournalGridElementComponent', getEntityGridElementTestComponent(JournalGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['editor', 'publisher', 'description'])); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f23211538ec1c3e0035a18b0edec8d48f83c96c --- /dev/null +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Journal', ItemViewMode.Card) +@Component({ + selector: 'ds-journal-grid-element', + styleUrls: ['./journal-grid-element.component.scss'], + templateUrl: './journal-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Journal + */ +export class JournalGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index 50ec1606509b91fbcfb51f70bae6834216b99f53..4033645e1b32a321801f3986db8fc4d78279621e 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -9,6 +9,9 @@ import { JournalListElementComponent } from './item-list-elements/journal/journa import { JournalIssueListElementComponent } from './item-list-elements/journal-issue/journal-issue-list-element.component'; import { JournalVolumeListElementComponent } from './item-list-elements/journal-volume/journal-volume-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { JournalIssueGridElementComponent } from './item-grid-elements/journal-issue/journal-issue-grid-element.component'; +import { JournalVolumeGridElementComponent } from './item-grid-elements/journal-volume/journal-volume-grid-element.component'; +import { JournalGridElementComponent } from './item-grid-elements/journal/journal-grid-element.component'; const ENTRY_COMPONENTS = [ JournalComponent, @@ -16,7 +19,10 @@ const ENTRY_COMPONENTS = [ JournalVolumeComponent, JournalListElementComponent, JournalIssueListElementComponent, - JournalVolumeListElementComponent + JournalVolumeListElementComponent, + JournalIssueGridElementComponent, + JournalVolumeGridElementComponent, + JournalGridElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..104d3a0a579d1bc46c05e35e2f5d537d10623297 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.html @@ -0,0 +1,35 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('organization.legalName')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('organization.foundingDate')" class="item-date card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span [innerHTML]="firstMetadataValue('organization.foundingDate')"></span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('organization.address.addressCountry')" + class="item-location card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span class="item-country">{{dso.firstMetadataValue('organization.address.addressCountry')}}</span> + <span *ngIf="dso.hasMetadata('organization.address.addressLocality')" class="item-city"> + <span>, </span> + {{dso.firstMetadataValue('organization.address.addressLocality')}} + </span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..39ddea4c7bbcfb5ca29f2d07aae67cb66229f150 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.spec.ts @@ -0,0 +1,53 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { OrgunitGridElementComponent } from './orgunit-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'organization.foundingDate': [ + { + language: null, + value: '2015-06-26' + } + ], + 'organization.address.addressCountry': [ + { + language: 'en_US', + value: 'Belgium' + } + ], + 'organization.address.addressLocality': [ + { + language: 'en_US', + value: 'Brussels' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('OrgunitGridElementComponent', getEntityGridElementTestComponent(OrgunitGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['date', 'country', 'city'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..0effc22027dba70cd985abad0029882c5f50e7d5 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/orgunit/orgunit-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('OrgUnit', ItemViewMode.Card) +@Component({ + selector: 'ds-orgunit-grid-element', + styleUrls: ['./orgunit-grid-element.component.scss'], + templateUrl: './orgunit-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Organisation Unit + */ +export class OrgunitGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..86353377fa79b253e00fbdfa5a1c06798a61e4f1 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -0,0 +1,30 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('person.familyName') + ', ' + dso.firstMetadataValue('person.givenName')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('person.email')" class="item-email card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span [innerHTML]="firstMetadataValue('person.email')"></span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('person.jobTitle')" class="item-jobtitle card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('person.jobTitle')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0f8e4c29e61541058433783c43a18fba7825435 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { PersonGridElementComponent } from './person-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'person.email': [ + { + language: 'en_US', + value: 'Smith-Donald@gmail.com' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Web Developer' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('PersonGridElementComponent', getEntityGridElementTestComponent(PersonGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['email', 'jobtitle'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf7b8aa119ea3f58406ffe0c38c5d424481e1791 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; +import { focusShadow } from '../../../../shared/animations/focus'; + +@rendersItemType('Person', ItemViewMode.Card) +@Component({ + selector: 'ds-person-grid-element', + styleUrls: ['./person-grid-element.component.scss'], + templateUrl: './person-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Person + */ +export class PersonGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a595791cc42e2d5b7c6ab4d4e52997a397e99d01 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -0,0 +1,25 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata('dc.description')" class="item-description card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('dc.description')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ad26935b768c5768ca6858f996f9d26939c5113 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts @@ -0,0 +1,41 @@ +import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getEntityGridElementTestComponent } from '../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec'; +import { ProjectGridElementComponent } from './project-grid-element.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.description': [ + { + language: 'en_US', + value: 'The project description' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('ProjectGridElementComponent', getEntityGridElementTestComponent(ProjectGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['description'])); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..15d525fcf2c46afc87171e93fd97b18538e1fe69 --- /dev/null +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.ts @@ -0,0 +1,17 @@ +import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../shared/animations/focus'; +import { TypedItemSearchResultGridElementComponent } from '../../../../shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; + +@rendersItemType('Project', ItemViewMode.Card) +@Component({ + selector: 'ds-project-grid-element', + styleUrls: ['./project-grid-element.component.scss'], + templateUrl: './project-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Project + */ +export class ProjectGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index ba28f174df8c243afaab16610e93874f7e7c3bdc..099fa2a6a346eae4f22c3409b5b3d057891b9f72 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -11,6 +11,9 @@ import { PersonMetadataListElementComponent } from './item-list-elements/person/ import { PersonListElementComponent } from './item-list-elements/person/person-list-element.component'; import { ProjectListElementComponent } from './item-list-elements/project/project-list-element.component'; import { TooltipModule } from 'ngx-bootstrap'; +import { PersonGridElementComponent } from './item-grid-elements/person/person-grid-element.component'; +import { OrgunitGridElementComponent } from './item-grid-elements/orgunit/orgunit-grid-element.component'; +import { ProjectGridElementComponent } from './item-grid-elements/project/project-grid-element.component'; const ENTRY_COMPONENTS = [ OrgunitComponent, @@ -20,7 +23,10 @@ const ENTRY_COMPONENTS = [ OrgUnitMetadataListElementComponent, PersonListElementComponent, PersonMetadataListElementComponent, - ProjectListElementComponent + ProjectListElementComponent, + PersonGridElementComponent, + OrgunitGridElementComponent, + ProjectGridElementComponent ]; @NgModule({ diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index 6a96892b06cd8a587446512086535e2443b9c877..8d1d5c1dcabf1bebd01f2e3f0a43ae2ce6acc96c 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<T extends DSpaceObject> 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<T extends DSpaceObject> 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 5692c27d207cca80a28dddae93554bae4904bc11..52a924604f90f4bb4d0b27776db0022391663df6 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 @@ -15,7 +15,7 @@ <ng-container #componentViewContainer></ng-container> <small *ngIf="hasHint && (!showErrorMessages || errorMessages.length === 0)" - class="text-muted" [innerHTML]="model.hint" [ngClass]="getClass('element', 'hint')"></small> + class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small> <div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]"> <small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small> diff --git a/src/app/shared/items/item-type-decorator.ts b/src/app/shared/items/item-type-decorator.ts index 2420e719087a7b876bb0745a7de3f2143f9e548c..3a040ae5bfd70bf3f4e01b576d4eb0e750a2c7fd 100644 --- a/src/app/shared/items/item-type-decorator.ts +++ b/src/app/shared/items/item-type-decorator.ts @@ -3,6 +3,7 @@ import { MetadataRepresentationType } from '../../core/shared/metadata-represent export enum ItemViewMode { Element = 'element', + Card = 'card', Full = 'full', Metadata = 'metadata' } diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html index c0c3c1f65fe1473593170c5a54a0a8eaa92a80d3..5b09d09a55daab41785bd318ae16b1c1e501bcb8 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html @@ -1,4 +1,3 @@ <div class="thumbnail"> - <img *ngIf="thumbnail && thumbnail.content" [src]="thumbnail.content" (error)="errorHandler($event)"/> - <img *ngIf="!(thumbnail && thumbnail.content)" [src]="holderSource | dsSafeUrl"/> + <img [src]="src | dsSafeUrl" (error)="errorHandler($event)"/> </div> diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts index 2d2bd6305a8fda6143d1723fa030d5ce00765b20..170ca34b42ed206a0b14fe766607264e8acbe632 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts @@ -6,7 +6,7 @@ import { GridThumbnailComponent } from './grid-thumbnail.component'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { SafeUrlPipe } from '../../utils/safe-url-pipe'; -describe('ThumbnailComponent', () => { +describe('GridThumbnailComponent', () => { let comp: GridThumbnailComponent; let fixture: ComponentFixture<GridThumbnailComponent>; let de: DebugElement; @@ -36,7 +36,7 @@ describe('ThumbnailComponent', () => { it('should display placeholder', () => { fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.holderSource); + expect(image.getAttribute('src')).toBe(comp.defaultImage); }); }); diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts index 8ca93470da4dd680f2e16d76925b8e86fbee2b00..6ae0c2d37eb414ff220252d4fe9d21e6c857e5e7 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../../core/shared/bitstream.model'; +import { hasValue } from '../../empty.util'; /** * This component renders a given Bitstream as a thumbnail. @@ -12,7 +13,7 @@ import { Bitstream } from '../../../core/shared/bitstream.model'; styleUrls: ['./grid-thumbnail.component.scss'], templateUrl: './grid-thumbnail.component.html' }) -export class GridThumbnailComponent { +export class GridThumbnailComponent implements OnInit { @Input() thumbnail: Bitstream; @@ -21,10 +22,19 @@ export class GridThumbnailComponent { /** * The default 'holder.js' image */ - holderSource = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; + @Input() defaultImage? = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; + src: string; errorHandler(event) { - event.currentTarget.src = this.holderSource; + event.currentTarget.src = this.defaultImage; + } + + ngOnInit(): void { + if (hasValue(this.thumbnail) && this.thumbnail.content) { + this.src = this.thumbnail.content; + } else { + this.src = this.defaultImage + } } } diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e2477524cad43f80184d9f1136616ba258cf8b7f --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -0,0 +1,34 @@ +<ds-truncatable [id]="dso.id"> + <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> + <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> + <div> + <ds-grid-thumbnail [thumbnail]="this.item.getThumbnail() | async"> + </ds-grid-thumbnail> + </div> + </a> + <div class="card-body"> + <ds-item-type-badge [object]="object"></ds-item-type-badge> + <ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4"> + <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> + </ds-truncatable-part> + <p *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" + class="item-authors card-text text-muted"> + <ds-truncatable-part [id]="dso.id" [minLines]="1"> + <span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{dso.firstMetadataValue('dc.date.issued')}}</span> + <span *ngFor="let author of dso.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">, + <span [innerHTML]="author"></span> + </span> + </ds-truncatable-part> + </p> + <p *ngIf="dso.hasMetadata('dc.description.abstract')" class="item-abstract card-text"> + <ds-truncatable-part [id]="dso.id" [minLines]="3"> + <span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span> + </ds-truncatable-part> + </p> + <div class="text-center"> + <a [routerLink]="['/items/' + dso.id]" + class="lead btn btn-primary viewButton">View</a> + </div> + </div> + </div> +</ds-truncatable> diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.scss b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f067a21ae0a295895df1536473fe9e97eb7db6db --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.spec.ts @@ -0,0 +1,124 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TruncatePipe } from '../../../../utils/truncate.pipe'; +import { TruncatableService } from '../../../../truncatable/truncatable.service'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { PublicationGridElementComponent } from './publication-grid-element.component'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { ITEM } from '../../../../items/switcher/item-type-switcher.component'; + +const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithMetadata.hitHighlights = {}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is an abstract' + } + ] + } +}); + +const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutMetadata.hitHighlights = {}; +mockItemWithoutMetadata.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('PublicationGridElementComponent', getEntityGridElementTestComponent(PublicationGridElementComponent, mockItemWithMetadata, mockItemWithoutMetadata, ['authors', 'date', 'abstract'])); + +/** + * Create test cases for a grid component of an entity. + * @param component The component's class + * @param searchResultWithMetadata An ItemSearchResult containing an item with metadata that should be displayed in the grid element + * @param searchResultWithoutMetadata An ItemSearchResult containing an item that's missing the metadata that should be displayed in the grid element + * @param fieldsToCheck A list of fields to check. The tests expect to find html elements with class ".item-${field}", so make sure they exist in the html template of the grid element. + * For example: If one of the fields to check is labeled "authors", the html template should contain at least one element with class ".item-authors" that's + * present when the author metadata is available. + */ +export function getEntityGridElementTestComponent(component, searchResultWithMetadata: ItemSearchResult, searchResultWithoutMetadata: ItemSearchResult, fieldsToCheck: string[]) { + return () => { + let comp; + let fixture; + + const truncatableServiceStub: any = { + isCollapsed: (id: number) => observableOf(true), + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + declarations: [component, TruncatePipe], + providers: [ + { provide: TruncatableService, useValue: truncatableServiceStub }, + {provide: ITEM, useValue: searchResultWithoutMetadata} + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(component, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(component); + comp = fixture.componentInstance; + })); + + fieldsToCheck.forEach((field) => { + describe(`when the item has "${field}" metadata`, () => { + beforeEach(() => { + comp.dso = searchResultWithMetadata.indexableObject; + fixture.detectChanges(); + }); + + it(`should show the "${field}" field`, () => { + const itemAuthorField = fixture.debugElement.query(By.css(`.item-${field}`)); + expect(itemAuthorField).not.toBeNull(); + }); + }); + + describe(`when the item has no "${field}" metadata`, () => { + beforeEach(() => { + comp.dso = searchResultWithoutMetadata.indexableObject; + fixture.detectChanges(); + }); + + it(`should not show the "${field}" field`, () => { + const itemAuthorField = fixture.debugElement.query(By.css(`.item-${field}`)); + expect(itemAuthorField).toBeNull(); + }); + }); + }); + } +} diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1bcd028baf31f741593513e399af9e585c935475 --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.ts @@ -0,0 +1,18 @@ +import { TypedItemSearchResultGridElementComponent } from '../typed-item-search-result-grid-element.component'; +import { DEFAULT_ITEM_TYPE, ItemViewMode, rendersItemType } from '../../../../items/item-type-decorator'; +import { Component } from '@angular/core'; +import { focusShadow } from '../../../../animations/focus'; + +@rendersItemType('Publication', ItemViewMode.Card) +@rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Card) +@Component({ + selector: 'ds-publication-grid-element', + styleUrls: ['./publication-grid-element.component.scss'], + templateUrl: './publication-grid-element.component.html', + animations: [focusShadow] +}) +/** + * The component for displaying a grid element for an item of the type Publication + */ +export class PublicationGridElementComponent extends TypedItemSearchResultGridElementComponent { +} diff --git a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4ace8d0b204fd7cf49ec26429deeb579c98de4c --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.spec.ts @@ -0,0 +1,83 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TruncatePipe } from '../../../utils/truncate.pipe'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { createRelationshipsObservable } from '../../../../+item-page/simple/item-types/shared/item.component.spec'; +import { of as observableOf } from 'rxjs'; +import { MetadataMap } from '../../../../core/shared/metadata.models'; +import { TypedItemSearchResultGridElementComponent } from './typed-item-search-result-grid-element.component'; + +const mockItem: Item = Object.assign(new Item(), { + bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))), + metadata: [], + relationships: createRelationshipsObservable() +}); +const mockSearchResult = { + indexableObject: mockItem as Item, + hitHighlights: new MetadataMap() +} as ItemSearchResult; + +describe('TypedItemSearchResultGridElementComponent', () => { + let comp: TypedItemSearchResultGridElementComponent; + let fixture: ComponentFixture<TypedItemSearchResultGridElementComponent>; + + describe('when injecting an Item', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TypedItemSearchResultGridElementComponent, TruncatePipe], + providers: [ + {provide: TruncatableService, useValue: {}}, + {provide: ITEM, useValue: mockItem} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(TypedItemSearchResultGridElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(TypedItemSearchResultGridElementComponent); + comp = fixture.componentInstance; + })); + + it('should initiate item, object and dso correctly', () => { + expect(comp.item).toBe(mockItem); + expect(comp.dso).toBe(mockItem); + expect(comp.object.indexableObject).toBe(mockItem); + }) + }); + + describe('when injecting an ItemSearchResult', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TypedItemSearchResultGridElementComponent, TruncatePipe], + providers: [ + {provide: TruncatableService, useValue: {}}, + {provide: ITEM, useValue: mockSearchResult} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(TypedItemSearchResultGridElementComponent, { + set: {changeDetection: ChangeDetectionStrategy.Default} + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(TypedItemSearchResultGridElementComponent); + comp = fixture.componentInstance; + })); + + it('should initiate item, object and dso correctly', () => { + expect(comp.item).toBe(mockItem); + expect(comp.dso).toBe(mockItem); + expect(comp.object.indexableObject).toBe(mockItem); + }) + }); +}); diff --git a/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4f470c05271bfd8d2fe50273870c0108e956421 --- /dev/null +++ b/src/app/shared/object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component.ts @@ -0,0 +1,37 @@ +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../core/shared/item.model'; +import { SearchResultGridElementComponent } from '../../search-result-grid-element/search-result-grid-element.component'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; +import { Component, Inject } from '@angular/core'; +import { ITEM } from '../../../items/switcher/item-type-switcher.component'; +import { hasValue } from '../../../empty.util'; +import { MetadataMap } from '../../../../core/shared/metadata.models'; + +/** + * A generic component for displaying item grid elements + */ +@Component({ + selector: 'ds-item-search-result-grid-element', + template: '' +}) +export class TypedItemSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> { + item: Item; + + constructor( + protected truncatableService: TruncatableService, + @Inject(ITEM) public obj: Item | ItemSearchResult, + ) { + super(undefined, truncatableService); + if (hasValue((obj as any).indexableObject)) { + this.object = obj as ItemSearchResult; + this.dso = this.object.indexableObject; + } else { + this.object = { + indexableObject: obj as Item, + hitHighlights: new MetadataMap() + }; + this.dso = obj as Item; + } + this.item = this.dso; + } +} diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 437dfc3b434eb47e586f54243a8f9baa06eb3e94..8f19309d89fd948d5597dcf5127610283ac03628 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -4,6 +4,11 @@ ds-wrapper-grid-element ::ng-deep { div.thumbnail > img { height: $card-thumbnail-height; width: 100%; + display: block; + min-width: 100%; + min-height: 100%; + object-fit: cover; + object-position: 50% 15%; } div.card { margin-top: $ds-wrapper-grid-spacing; diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html index c7e2f524f389306a7c44d8c599a4b28f09b2c4f5..d433c7acf24ce47b1cc4c820e700795566529541 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.html @@ -1,33 +1 @@ -<ds-truncatable [id]="dso.id"> - <div class="card" [@focusShadow]="(isCollapsed() | async)?'blur':'focus'"> - <a [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> - <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail()"> - </ds-grid-thumbnail> - </div> - </a> - <div class="card-body"> - <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4"> - <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4> - </ds-truncatable-part> - <p *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" - class="item-authors card-text text-muted"> - <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1"> - <span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{dso.firstMetadataValue('dc.date.issued')}}</span> - <span *ngFor="let author of dso.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">, - <span [innerHTML]="author"></span> - </span> - </ds-truncatable-part> - </p> - <p class="item-abstract card-text"> - <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3"> - <span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span> - </ds-truncatable-part> - </p> - <div class="text-center"> - <a [routerLink]="['/items/' + dso.id]" - class="lead btn btn-primary viewButton">View</a> - </div> - </div> - </div> -</ds-truncatable> +<ds-item-type-switcher [object]="object" [viewMode]="viewMode"></ds-item-type-switcher> diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts index 655fd268a75d786d09a751e2841fb5286ab47c67..282478ec33c4ed083b3978e57515bec5de318647 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.spec.ts @@ -8,6 +8,7 @@ import { Item } from '../../../../core/shared/item.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { ItemViewMode } from '../../../items/item-type-decorator'; let itemSearchResultGridElementComponent: ItemSearchResultGridElementComponent; let fixture: ComponentFixture<ItemSearchResultGridElementComponent>; @@ -16,41 +17,17 @@ const truncatableServiceStub: any = { isCollapsed: (id: number) => observableOf(true), }; -const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult(); -mockItemWithAuthorAndDate.hitHighlights = {}; -mockItemWithAuthorAndDate.indexableObject = Object.assign(new Item(), { - bitstreams: observableOf({}), - metadata: { - 'dc.contributor.author': [ - { - language: 'en_US', - value: 'Smith, Donald' - } - ], - 'dc.date.issued': [ - { - language: null, - value: '2015-06-26' - } - ] - } -}); +const type = 'authorOfPublication'; -const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult(); -mockItemWithoutAuthorAndDate.hitHighlights = {}; -mockItemWithoutAuthorAndDate.indexableObject = Object.assign(new Item(), { +const mockItemWithRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithRelationshipType.hitHighlights = {}; +mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { bitstreams: observableOf({}), metadata: { - 'dc.title': [ + 'relationship.type': [ { language: 'en_US', - value: 'This is just another title' - } - ], - 'dc.type': [ - { - language: null, - value: 'Article' + value: type } ] } @@ -63,7 +40,7 @@ describe('ItemSearchResultGridElementComponent', () => { declarations: [ItemSearchResultGridElementComponent, TruncatePipe], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockItemWithoutAuthorAndDate) } + { provide: 'objectElementProvider', useValue: (mockItemWithRelationshipType) } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemSearchResultGridElementComponent, { @@ -76,51 +53,9 @@ describe('ItemSearchResultGridElementComponent', () => { itemSearchResultGridElementComponent = fixture.componentInstance; })); - describe('When the item has an author', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should show the author paragraph', () => { - const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); - expect(itemAuthorField).not.toBeNull(); - }); - }); - - describe('When the item has no author', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should not show the author paragraph', () => { - const itemAuthorField = fixture.debugElement.query(By.css('p.item-authors')); - expect(itemAuthorField).toBeNull(); - }); - }); - - describe('When the item has an issuedate', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should show the issuedate span', () => { - const itemAuthorField = fixture.debugElement.query(By.css('span.item-date')); - expect(itemAuthorField).not.toBeNull(); - }); - }); - - describe('When the item has no issuedate', () => { - beforeEach(() => { - itemSearchResultGridElementComponent.dso = mockItemWithoutAuthorAndDate.indexableObject; - fixture.detectChanges(); - }); - - it('should not show the issuedate span', () => { - const dateField = fixture.debugElement.query(By.css('span.item-date')); - expect(dateField).toBeNull(); - }); + it('should show send the object to item-type-switcher using viewMode "Card"', () => { + const itemTypeSwitcherComp = fixture.debugElement.query(By.css('ds-item-type-switcher')).componentInstance; + expect(itemTypeSwitcherComp.object).toBe(mockItemWithRelationshipType); + expect(itemTypeSwitcherComp.viewMode).toEqual(ItemViewMode.Card); }); }); diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts index 30c36b3af9f38b04cb35be09ff3399ede565fa4f..7bbe41fe60c475b4ba296d434cacc5fe0c7a5a95 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts @@ -6,6 +6,7 @@ import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { SetViewMode } from '../../../view-mode'; import { focusShadow } from '../../../../shared/animations/focus'; +import { ItemViewMode } from '../../../items/item-type-decorator'; @Component({ selector: 'ds-item-search-result-grid-element', @@ -15,4 +16,6 @@ import { focusShadow } from '../../../../shared/animations/focus'; }) @renderElementsFor(ItemSearchResult, SetViewMode.Grid) -export class ItemSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> {} +export class ItemSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> { + viewMode = ItemViewMode.Card; +} diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 0961dc96ee3c1fd39f0dd8e78c3390c5d1f16480..5f31d52ae7add55e9f990b1e53bf7a02cdae2cb7 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -7,6 +7,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m import { TruncatableService } from '../../truncatable/truncatable.service'; import { Observable } from 'rxjs'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { hasValue } from '../../empty.util'; @Component({ selector: 'ds-search-result-grid-element', @@ -16,9 +17,11 @@ import { Metadata } from '../../../core/shared/metadata.utils'; export class SearchResultGridElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> { dso: K; - public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, private truncatableService: TruncatableService) { + public constructor(@Inject('objectElementProvider') public listableObject: ListableObject, protected truncatableService: TruncatableService) { super(listableObject); - this.dso = this.object.indexableObject; + if (hasValue(this.object)) { + this.dso = this.object.indexableObject; + } } /** diff --git a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts index 9adf255523b2da9d616a3c3d31fec1844569c8ab..082347be0b7e19213fd06721d2319ad7e32a719d 100644 --- a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.spec.ts @@ -24,7 +24,7 @@ const mockSearchResult = { hitHighlights: new MetadataMap() } as ItemSearchResult; -describe('ItemSearchResultComponent', () => { +describe('TypedItemSearchResultListElementComponent', () => { let comp: TypedItemSearchResultListElementComponent; let fixture: ComponentFixture<TypedItemSearchResultListElementComponent>; diff --git a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts index 7df3ab5681f065cf9d26029ce11fb18a34dca54a..dd1b5a7e5f64c8d3709975d4a4aa19aa07319104 100644 --- a/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component.ts @@ -11,7 +11,7 @@ import { MetadataMap } from '../../../../core/shared/metadata.models'; * A generic component for displaying item list elements */ @Component({ - selector: 'ds-item-search-result', + selector: 'ds-item-search-result-list-element', template: '' }) export class TypedItemSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> { diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.html b/src/app/shared/object-list/item-type-badge/item-type-badge.component.html new file mode 100644 index 0000000000000000000000000000000000000000..35d76638018a58a452f9517300abcb11ce08e719 --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.html @@ -0,0 +1,3 @@ +<div *ngIf="object && object.indexableObject && object.indexableObject.firstMetadataValue('relationship.type') as type"> + <span class="badge badge-light">{{ type.toLowerCase() + '.listelement.badge' | translate }}</span> +</div> diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts b/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..04c40b73ff18911f22179c24d5e011a1777971ef --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.spec.ts @@ -0,0 +1,83 @@ +import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../core/shared/item.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ItemTypeBadgeComponent } from './item-type-badge.component'; +import { By } from '@angular/platform-browser'; + +let comp: ItemTypeBadgeComponent; +let fixture: ComponentFixture<ItemTypeBadgeComponent>; + +const type = 'authorOfPublication'; + +const mockItemWithRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithRelationshipType.hitHighlights = {}; +mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'relationship.type': [ + { + language: 'en_US', + value: type + } + ] + } +}); + +const mockItemWithoutRelationshipType: ItemSearchResult = new ItemSearchResult(); +mockItemWithoutRelationshipType.hitHighlights = {}; +mockItemWithoutRelationshipType.indexableObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } +}); + +describe('ItemTypeBadgeComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ItemTypeBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemTypeBadgeComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(ItemTypeBadgeComponent); + comp = fixture.componentInstance; + })); + + describe('When the item has a relationship type', () => { + beforeEach(() => { + comp.object = mockItemWithRelationshipType; + fixture.detectChanges(); + }); + + it('should show the relationship type badge', () => { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge.nativeElement.textContent).toContain(type.toLowerCase()); + }); + }); + + describe('When the item has no relationship type', () => { + beforeEach(() => { + comp.object = mockItemWithoutRelationshipType; + fixture.detectChanges(); + }); + + it('should not show a badge', () => { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ffba33758561a63dbfdb6d57956a7711a4b7431 --- /dev/null +++ b/src/app/shared/object-list/item-type-badge/item-type-badge.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; +import { ListableObject } from '../../object-collection/shared/listable-object.model'; +import { SearchResult } from '../../../+search-page/search-result.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-item-type-badge', + templateUrl: './item-type-badge.component.html' +}) +export class ItemTypeBadgeComponent { + @Input() object: SearchResult<DSpaceObject>; +} diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index a2617a956f2a316ced6592f8cb2fb45e51c1bf26..051a27bde7cc83af756d8e6c5710038ec820a6cc 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -1,4 +1,2 @@ -<div *ngIf="object && object.indexableObject && object.indexableObject.firstMetadataValue('relationship.type') as type"> - <span class="badge badge-light">{{ type.toLowerCase() + '.listelement.badge' | translate }}</span> -</div> +<ds-item-type-badge [object]="object"></ds-item-type-badge> <ds-item-type-switcher [object]="object" [viewMode]="viewMode"></ds-item-type-switcher> diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts index a370d3a6329223a5b5573317de4e0f3aae05764c..8f410184042dad4020212b5aa95439e8d1edaa4a 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts @@ -33,20 +33,6 @@ mockItemWithRelationshipType.indexableObject = Object.assign(new Item(), { } }); -const mockItemWithoutRelationshipType: ItemSearchResult = new ItemSearchResult(); -mockItemWithoutRelationshipType.hitHighlights = {}; -mockItemWithoutRelationshipType.indexableObject = Object.assign(new Item(), { - bitstreams: observableOf({}), - metadata: { - 'dc.title': [ - { - language: 'en_US', - value: 'This is just another title' - } - ] - } -}); - describe('ItemSearchResultListElementComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ @@ -54,7 +40,7 @@ describe('ItemSearchResultListElementComponent', () => { declarations: [ItemSearchResultListElementComponent, TruncatePipe], providers: [ { provide: TruncatableService, useValue: truncatableServiceStub }, - { provide: 'objectElementProvider', useValue: (mockItemWithoutRelationshipType) } + { provide: 'objectElementProvider', useValue: (mockItemWithRelationshipType) } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemSearchResultListElementComponent, { @@ -67,27 +53,8 @@ describe('ItemSearchResultListElementComponent', () => { itemSearchResultListElementComponent = fixture.componentInstance; })); - describe('When the item has a relationship type', () => { - beforeEach(() => { - itemSearchResultListElementComponent.object = mockItemWithRelationshipType; - fixture.detectChanges(); - }); - - it('should show the relationship type badge', () => { - const badge = fixture.debugElement.query(By.css('span.badge')); - expect(badge.nativeElement.textContent).toContain(type.toLowerCase()); - }); - }); - - describe('When the item has no relationship type', () => { - beforeEach(() => { - itemSearchResultListElementComponent.object = mockItemWithoutRelationshipType; - fixture.detectChanges(); - }); - - it('should not show a badge', () => { - const badge = fixture.debugElement.query(By.css('span.badge')); - expect(badge).toBeNull(); - }); + it('should show a badge on top of the list element', () => { + const badge = fixture.debugElement.query(By.css('ds-item-type-badge')).componentInstance; + expect(badge.object).toBe(mockItemWithRelationshipType); }); }); diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index 22a58dd7fc94e645fe1f4aba4c4b37fd02bf7fca..c16a1530263521e81f0cc4624cd5dc9347c2872e 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,9 +1,9 @@ <div *ngIf="currentPageState == undefined || currentPageState == currentPage"> <div class="pagination-masked clearfix top"> <div class="row"> - <div *ngIf="!hidePaginationDetail" class="col-auto pagination-info"> - <span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span> - <span class="align-middle" *ngIf="collectionSize">{{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}}</span> + <div *ngIf="!hidePaginationDetail && collectionSize > 0" class="col-auto pagination-info"> + <span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span> + <span class="align-middle">{{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}}</span> </div> <div class="col"> <div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right"> diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 816139c8b96c082fa0adf211c4f5fafe65f3a675..66afb1e41bc333e2d33fb38366ee668494f9a2b2 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -138,6 +138,9 @@ import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; +import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component'; +import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; +import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -256,8 +259,10 @@ const COMPONENTS = [ CollectionSearchResultListElementComponent, ItemSearchResultListElementComponent, TypedItemSearchResultListElementComponent, + TypedItemSearchResultGridElementComponent, ItemTypeSwitcherComponent, - BrowseByComponent + BrowseByComponent, + ItemTypeBadgeComponent ]; const ENTRY_COMPONENTS = [ @@ -275,6 +280,7 @@ const ENTRY_COMPONENTS = [ CommunityGridElementComponent, SearchResultGridElementComponent, PublicationListElementComponent, + PublicationGridElementComponent, BrowseEntryListElementComponent, MyDSpaceResultDetailElementComponent, SearchResultGridElementComponent, diff --git a/themes/mantis/app/+item-page/simple/item-types/journal-issue/journal-issue.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/journal-issue/journal-issue.component.html rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/journal-issue/journal-issue.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss similarity index 85% rename from themes/mantis/app/+item-page/simple/item-types/journal-issue/journal-issue.component.scss rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss index 7ce24acc15823eb8ba3f168b5171e641f8bdb175..3caa55f533547857b1eb26990ea9fd2861b2ed53 100644 --- a/themes/mantis/app/+item-page/simple/item-types/journal-issue/journal-issue.component.scss +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/journal-issue/journal-issue.component.scss'; +@import 'src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss'; :host { > * { diff --git a/themes/mantis/app/+item-page/simple/item-types/journal-volume/journal-volume.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/journal-volume/journal-volume.component.html rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/journal-volume/journal-volume.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss similarity index 85% rename from themes/mantis/app/+item-page/simple/item-types/journal-volume/journal-volume.component.scss rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss index ab1bc700b1907613c7706126fe1aaf3cf32730d3..5c2534b3182ab5968631cf6efd4271c33c9b1bc2 100644 --- a/themes/mantis/app/+item-page/simple/item-types/journal-volume/journal-volume.component.scss +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/journal-volume/journal-volume.component.scss'; +@import 'src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.scss'; :host { > * { diff --git a/themes/mantis/app/+item-page/simple/item-types/journal/journal.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/journal/journal.component.html rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/journal/journal.component.scss b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss similarity index 89% rename from themes/mantis/app/+item-page/simple/item-types/journal/journal.component.scss rename to themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss index 6d97cbf5c35168c9143fdffae59b5a722d642f83..5c0d1c44b829110e776c37447990cc568fb45103 100644 --- a/themes/mantis/app/+item-page/simple/item-types/journal/journal.component.scss +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/journal/journal.component.scss'; +@import 'src/app/entity-groups/journal-entities/item-pages/journal/journal.component.scss'; :host { > * { diff --git a/themes/mantis/app/+item-page/simple/item-types/orgunit/orgunit.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/orgunit/orgunit.component.html rename to themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/orgunit/orgunit.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss similarity index 86% rename from themes/mantis/app/+item-page/simple/item-types/orgunit/orgunit.component.scss rename to themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss index 5b2bdb0382717852ad7e355bd89cabb2f1ecf19f..54651aede0f5c7487ed59528d20b1a3d1fb84af5 100644 --- a/themes/mantis/app/+item-page/simple/item-types/orgunit/orgunit.component.scss +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/orgunit/orgunit.component.scss'; +@import 'src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss'; :host { > * { diff --git a/themes/mantis/app/+item-page/simple/item-types/person/person.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/person/person.component.html rename to themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/person/person.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss similarity index 89% rename from themes/mantis/app/+item-page/simple/item-types/person/person.component.scss rename to themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss index 3b454aab0e81ecda662544377df6534089cf94f2..48571b05b23ff32e31e076a0c1b95bed0e328098 100644 --- a/themes/mantis/app/+item-page/simple/item-types/person/person.component.scss +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/person/person.component.scss'; +@import 'src/app/entity-groups/research-entities/item-pages/person/person.component.scss'; :host { > * { diff --git a/themes/mantis/app/+item-page/simple/item-types/project/project.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html similarity index 100% rename from themes/mantis/app/+item-page/simple/item-types/project/project.component.html rename to themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html diff --git a/themes/mantis/app/+item-page/simple/item-types/project/project.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss similarity index 86% rename from themes/mantis/app/+item-page/simple/item-types/project/project.component.scss rename to themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss index 9c9aa9c629931071328255060cdd0926e1a368b7..d2707d30ccb0455daba9bacadcfe1058216d93d3 100644 --- a/themes/mantis/app/+item-page/simple/item-types/project/project.component.scss +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+item-page/simple/item-types/project/project.component.scss'; +@import 'src/app/entity-groups/research-entities/item-pages/project/project.component.scss'; :host { > * {