diff --git a/config/config.example.yml b/config/config.example.yml index 9abf167b90..af04859201 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -121,6 +121,9 @@ languages: - code: en label: English active: true + - code: ca + label: Català + active: true - code: cs label: Čeština active: true @@ -310,3 +313,11 @@ info: markdown: enabled: false mathjax: false + +# Which vocabularies should be used for which search filters +# and whether to show the filter in the search sidebar +# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained +vocabularies: + - filter: 'subject' + vocabulary: 'srsc' + enabled: true diff --git a/package.json b/package.json index 52b089be37..dcb629a331 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,9 @@ "clean:log": "rimraf *.log*", "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", + "clean:cli": "rimraf .angular/cache", "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node", + "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", @@ -92,6 +93,7 @@ "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "deepmerge": "^4.2.2", + "ejs": "^3.1.8", "express": "^4.17.1", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.0.0-1", @@ -99,7 +101,7 @@ "http-proxy-middleware": "^1.0.5", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", - "json5": "^2.1.3", + "json5": "^2.2.2", "jsonschema": "1.4.0", "jwt-decode": "^3.1.2", "klaro": "^0.7.18", @@ -146,6 +148,7 @@ "@ngtools/webpack": "^13.2.6", "@nguniversal/builders": "^13.1.1", "@types/deep-freeze": "0.1.2", + "@types/ejs": "^3.1.1", "@types/express": "^4.17.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", diff --git a/server.ts b/server.ts index 608c214076..ecbbb982d4 100644 --- a/server.ts +++ b/server.ts @@ -22,6 +22,7 @@ import 'rxjs'; /* eslint-disable import/no-namespace */ import * as morgan from 'morgan'; import * as express from 'express'; +import * as ejs from 'ejs'; import * as compression from 'compression'; import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ @@ -136,10 +137,23 @@ export function app() { })(_, (options as any), callback) ); + server.engine('ejs', ejs.renderFile); + /* * Register the view engines for html and ejs */ server.set('view engine', 'html'); + server.set('view engine', 'ejs'); + + /** + * Serve the robots.txt ejs template, filling in the origin variable + */ + server.get('/robots.txt', (req, res) => { + res.setHeader('content-type', 'text/plain'); + res.render('assets/robots.txt.ejs', { + 'origin': req.protocol + '://' + req.headers.host + }); + }); /* * Set views folder path to directory where template files are stored diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index 0fc5a574b7..d86adc674b 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.html +++ b/src/app/access-control/group-registry/group-form/group-form.component.html @@ -9,7 +9,18 @@ -

{{messagePrefix + '.head.edit' | translate}}

+

+ + {{messagePrefix + '.head.edit' | translate}} + +

{ fixture.detectChanges(); }); + it('should edit with name and description operations', () => { + const operations = [{ + op: 'add', + path: '/metadata/dc.description', + value: 'testDescription' + }, { + op: 'replace', + path: '/name', + value: 'newGroupName' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + + it('should edit with description operations', () => { + component.groupName.value = null; + component.onSubmit(); + fixture.detectChanges(); + const operations = [{ + op: 'add', + path: '/metadata/dc.description', + value: 'testDescription' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + + it('should edit with name operations', () => { + component.groupDescription.value = null; + component.onSubmit(); + fixture.detectChanges(); + const operations = [{ + op: 'replace', + path: '/name', + value: 'newGroupName' + }]; + expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); + }); + it('should emit the existing group using the correct new values', waitForAsync(() => { fixture.whenStable().then(() => { expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 584b28ba1e..4302d126ea 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -346,8 +346,8 @@ export class GroupFormComponent implements OnInit, OnDestroy { if (hasValue(this.groupDescription.value)) { operations = [...operations, { - op: 'replace', - path: '/metadata/dc.description/0/value', + op: 'add', + path: '/metadata/dc.description', value: this.groupDescription.value }]; } diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html index e5932edf05..8b0ae35bd4 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html @@ -1,9 +1,19 @@

{{messagePrefix + '.head' | translate}}

- +
+ +
+
+
{{ mdValue.newValue.language }}
+ +
+
+
+
+
+ + + + +
+
+ +
+
+ diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss new file mode 100644 index 0000000000..4a207ee1a4 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss @@ -0,0 +1,16 @@ +.ds-success { + background-color: var(--bs-success-bg); + border: 1px solid var(--bs-success); +} + +.ds-drag-handle:not(.disabled) { + cursor: grab; +} + +::ng-deep .edit-field>ngb-tooltip-window .tooltip-inner { + min-width: var(--ds-dso-edit-virtual-tooltip-min-width); +} + +.cdk-drag-placeholder { + opacity: 0; +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts new file mode 100644 index 0000000000..10b3016a52 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -0,0 +1,170 @@ +import { DsoEditMetadataValueComponent } from './dso-edit-metadata-value.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { RelationshipDataService } from '../../../core/data/relationship-data.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { of } from 'rxjs/internal/observable/of'; +import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; +import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; +import { By } from '@angular/platform-browser'; + +const EDIT_BTN = 'edit'; +const CONFIRM_BTN = 'confirm'; +const REMOVE_BTN = 'remove'; +const UNDO_BTN = 'undo'; +const DRAG_BTN = 'drag'; + +describe('DsoEditMetadataValueComponent', () => { + let component: DsoEditMetadataValueComponent; + let fixture: ComponentFixture; + + let relationshipService: RelationshipDataService; + let dsoNameService: DSONameService; + + let editMetadataValue: DsoEditMetadataValue; + let metadataValue: MetadataValue; + + function initServices(): void { + relationshipService = jasmine.createSpyObj('relationshipService', { + resolveMetadataRepresentation: of(new ItemMetadataRepresentation(metadataValue)), + }); + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: 'Related Name', + }); + } + + beforeEach(waitForAsync(() => { + metadataValue = Object.assign(new MetadataValue(), { + value: 'Regular Name', + language: 'en', + place: 0, + authority: undefined, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + + initServices(); + + TestBed.configureTestingModule({ + declarations: [DsoEditMetadataValueComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: RelationshipDataService, useValue: relationshipService }, + { provide: DSONameService, useValue: dsoNameService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoEditMetadataValueComponent); + component = fixture.componentInstance; + component.mdValue = editMetadataValue; + component.saving$ = of(false); + fixture.detectChanges(); + }); + + it('should not show a badge', () => { + expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeNull(); + }); + + describe('when no changes have been made', () => { + assertButton(EDIT_BTN, true, false); + assertButton(CONFIRM_BTN, false); + assertButton(REMOVE_BTN, true, false); + assertButton(UNDO_BTN, true, true); + assertButton(DRAG_BTN, true, false); + }); + + describe('when this is the only metadata value within its field', () => { + beforeEach(() => { + component.isOnlyValue = true; + fixture.detectChanges(); + }); + + assertButton(DRAG_BTN, true, true); + }); + + describe('when the value is marked for removal', () => { + beforeEach(() => { + editMetadataValue.change = DsoEditMetadataChangeType.REMOVE; + fixture.detectChanges(); + }); + + assertButton(REMOVE_BTN, true, true); + assertButton(UNDO_BTN, true, false); + }); + + describe('when the value is being edited', () => { + beforeEach(() => { + editMetadataValue.editing = true; + fixture.detectChanges(); + }); + + assertButton(EDIT_BTN, false); + assertButton(CONFIRM_BTN, true, false); + assertButton(UNDO_BTN, true, false); + }); + + describe('when the value is new', () => { + beforeEach(() => { + editMetadataValue.change = DsoEditMetadataChangeType.ADD; + fixture.detectChanges(); + }); + + assertButton(REMOVE_BTN, true, false); + assertButton(UNDO_BTN, true, false); + }); + + describe('when the metadata value is virtual', () => { + beforeEach(() => { + metadataValue = Object.assign(new MetadataValue(), { + value: 'Virtual Name', + language: 'en', + place: 0, + authority: `${VIRTUAL_METADATA_PREFIX}authority-key`, + }); + editMetadataValue = new DsoEditMetadataValue(metadataValue); + component.mdValue = editMetadataValue; + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should show a badge', () => { + expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeTruthy(); + }); + + assertButton(EDIT_BTN, true, true); + assertButton(CONFIRM_BTN, false); + assertButton(REMOVE_BTN, true, true); + assertButton(UNDO_BTN, true, true); + assertButton(DRAG_BTN, true, false); + }); + + function assertButton(name: string, exists: boolean, disabled: boolean = false): void { + describe(`${name} button`, () => { + let btn: DebugElement; + + beforeEach(() => { + btn = fixture.debugElement.query(By.css(`#metadata-${name}-btn`)); + }); + + if (exists) { + it('should exist', () => { + expect(btn).toBeTruthy(); + }); + + it(`should${disabled ? ' ' : ' not '}be disabled`, () => { + expect(btn.nativeElement.disabled).toBe(disabled); + }); + } else { + it('should not exist', () => { + expect(btn).toBeNull(); + }); + } + }); + } +}); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts new file mode 100644 index 0000000000..3fdcd381ab --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -0,0 +1,126 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; +import { Observable } from 'rxjs/internal/Observable'; +import { + MetadataRepresentation, + MetadataRepresentationType +} from '../../../core/shared/metadata-representation/metadata-representation.model'; +import { RelationshipDataService } from '../../../core/data/relationship-data.service'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { map } from 'rxjs/operators'; +import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { EMPTY } from 'rxjs/internal/observable/empty'; + +@Component({ + selector: 'ds-dso-edit-metadata-value', + styleUrls: ['./dso-edit-metadata-value.component.scss', '../dso-edit-metadata-shared/dso-edit-metadata-cells.scss'], + templateUrl: './dso-edit-metadata-value.component.html', +}) +/** + * Component displaying a single editable row for a metadata value + */ +export class DsoEditMetadataValueComponent implements OnInit { + /** + * The parent {@link DSpaceObject} to display a metadata form for + * Also used to determine metadata-representations in case of virtual metadata + */ + @Input() dso: DSpaceObject; + + /** + * Editable metadata value to show + */ + @Input() mdValue: DsoEditMetadataValue; + + /** + * Type of DSO we're displaying values for + * Determines i18n messages + */ + @Input() dsoType: string; + + /** + * Observable to check if the form is being saved or not + * Will disable certain functionality while saving + */ + @Input() saving$: Observable; + + /** + * Is this value the only one within its list? + * Will disable certain functionality like dragging (because dragging within a list of 1 is pointless) + */ + @Input() isOnlyValue = false; + + /** + * Emits when the user clicked edit + */ + @Output() edit: EventEmitter = new EventEmitter(); + + /** + * Emits when the user clicked confirm + */ + @Output() confirm: EventEmitter = new EventEmitter(); + + /** + * Emits when the user clicked remove + */ + @Output() remove: EventEmitter = new EventEmitter(); + + /** + * Emits when the user clicked undo + */ + @Output() undo: EventEmitter = new EventEmitter(); + + /** + * Emits true when the user starts dragging a value, false when the user stops dragging + */ + @Output() dragging: EventEmitter = new EventEmitter(); + + /** + * The DsoEditMetadataChangeType enumeration for access in the component's template + * @type {DsoEditMetadataChangeType} + */ + public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType; + + /** + * The item this metadata value represents in case it's virtual (if any, otherwise null) + */ + mdRepresentation$: Observable; + + /** + * The route to the item represented by this virtual metadata value (otherwise null) + */ + mdRepresentationItemRoute$: Observable; + + /** + * The name of the item represented by this virtual metadata value (otherwise null) + */ + mdRepresentationName$: Observable; + + constructor(protected relationshipService: RelationshipDataService, + protected dsoNameService: DSONameService) { + } + + ngOnInit(): void { + this.initVirtualProperties(); + } + + /** + * Initialise potential properties of a virtual metadata value + */ + initVirtualProperties(): void { + this.mdRepresentation$ = this.mdValue.newValue.isVirtual ? + this.relationshipService.resolveMetadataRepresentation(this.mdValue.newValue, this.dso, 'Item') + .pipe( + map((mdRepresentation: MetadataRepresentation) => + mdRepresentation.representationType === MetadataRepresentationType.Item ? mdRepresentation as ItemMetadataRepresentation : null + ) + ) : EMPTY; + this.mdRepresentationItemRoute$ = this.mdRepresentation$.pipe( + map((mdRepresentation: ItemMetadataRepresentation) => mdRepresentation ? getItemPageRoute(mdRepresentation) : null), + ); + this.mdRepresentationName$ = this.mdRepresentation$.pipe( + map((mdRepresentation: ItemMetadataRepresentation) => mdRepresentation ? this.dsoNameService.getName(mdRepresentation) : null), + ); + } +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html new file mode 100644 index 0000000000..24c3dc5cd7 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -0,0 +1,91 @@ + + diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss new file mode 100644 index 0000000000..4e5e9ff1d4 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss @@ -0,0 +1,21 @@ +.lbl-cell { + min-width: var(--ds-dso-edit-field-width); + max-width: var(--ds-dso-edit-field-width); + background-color: var(--bs-gray-100); + font-weight: bold; + padding: 1rem; + border: 1px solid var(--bs-gray-200); + + &.ds-success { + background-color: var(--bs-success-bg); + border: 1px solid var(--bs-success); + } +} + +.ds-field-row { + border: 1px solid var(--bs-gray-400); +} + +.reset-order-button:hover { + cursor: pointer; +} diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts new file mode 100644 index 0000000000..7067c44fbb --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts @@ -0,0 +1,193 @@ +import { DsoEditMetadataComponent } from './dso-edit-metadata.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DebugElement, Injectable, NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { Item } from '../../core/shared/item.model'; +import { MetadataValue } from '../../core/shared/metadata.models'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { By } from '@angular/platform-browser'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service'; +import { ITEM } from '../../core/shared/item.resource-type'; +import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator'; +import { Operation } from 'fast-json-patch'; +import { RemoteData } from '../../core/data/remote-data'; +import { Observable } from 'rxjs/internal/Observable'; + +const ADD_BTN = 'add'; +const REINSTATE_BTN = 'reinstate'; +const SAVE_BTN = 'save'; +const DISCARD_BTN = 'discard'; + +@Injectable() +class TestDataService { + patch(object: Item, operations: Operation[]): Observable> { + return createSuccessfulRemoteDataObject$(object); + } +} + +describe('DsoEditMetadataComponent', () => { + let component: DsoEditMetadataComponent; + let fixture: ComponentFixture; + + let notificationsService: NotificationsService; + + let dso: DSpaceObject; + + beforeEach(waitForAsync(() => { + dso = Object.assign(new Item(), { + type: ITEM, + metadata: { + 'dc.title': [ + Object.assign(new MetadataValue(), { + value: 'Test Title', + language: 'en', + place: 0, + }), + ], + 'dc.subject': [ + Object.assign(new MetadataValue(), { + value: 'Subject One', + language: 'en', + place: 0, + }), + Object.assign(new MetadataValue(), { + value: 'Subject Two', + language: 'en', + place: 1, + }), + Object.assign(new MetadataValue(), { + value: 'Subject Three', + language: 'en', + place: 2, + }), + ], + }, + }); + + notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']); + + TestBed.configureTestingModule({ + declarations: [DsoEditMetadataComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + TestDataService, + { provide: DATA_SERVICE_FACTORY, useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService) }, + { provide: NotificationsService, useValue: notificationsService }, + ArrayMoveChangeAnalyzer, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoEditMetadataComponent); + component = fixture.componentInstance; + component.dso = dso; + fixture.detectChanges(); + }); + + describe('when no changes have been made', () => { + assertButton(ADD_BTN, true, false); + assertButton(REINSTATE_BTN, false); + assertButton(SAVE_BTN, true, true); + assertButton(DISCARD_BTN, true, true); + }); + + describe('when the form contains changes', () => { + beforeEach(() => { + component.form.fields['dc.title'][0].newValue.value = 'Updated Title Once'; + component.form.fields['dc.title'][0].confirmChanges(); + component.form.resetReinstatable(); + component.onValueSaved(); + fixture.detectChanges(); + }); + + assertButton(SAVE_BTN, true, false); + assertButton(DISCARD_BTN, true, false); + + describe('and they were discarded', () => { + beforeEach(() => { + component.discard(); + fixture.detectChanges(); + }); + + assertButton(REINSTATE_BTN, true, false); + assertButton(SAVE_BTN, true, true); + assertButton(DISCARD_BTN, false); + + describe('and a new change is made', () => { + beforeEach(() => { + component.form.fields['dc.title'][0].newValue.value = 'Updated Title Twice'; + component.form.fields['dc.title'][0].confirmChanges(); + component.form.resetReinstatable(); + component.onValueSaved(); + fixture.detectChanges(); + }); + + assertButton(REINSTATE_BTN, false); + assertButton(SAVE_BTN, true, false); + assertButton(DISCARD_BTN, true, false); + }); + }); + }); + + describe('when a new value is present', () => { + beforeEach(() => { + component.add(); + fixture.detectChanges(); + }); + + assertButton(ADD_BTN, true, true); + + it('should display a row with a field selector and metadata value', () => { + expect(fixture.debugElement.query(By.css('ds-metadata-field-selector'))).toBeTruthy(); + expect(fixture.debugElement.query(By.css('ds-dso-edit-metadata-value'))).toBeTruthy(); + }); + + describe('and gets assigned to a metadata field', () => { + beforeEach(() => { + component.form.newValue.newValue.value = 'New Subject'; + component.form.setMetadataField('dc.subject'); + component.form.resetReinstatable(); + component.onValueSaved(); + fixture.detectChanges(); + }); + + assertButton(ADD_BTN, true, false); + + it('should not display the separate row with field selector and metadata value anymore', () => { + expect(fixture.debugElement.query(By.css('ds-metadata-field-selector'))).toBeNull(); + expect(fixture.debugElement.query(By.css('ds-dso-edit-metadata-value'))).toBeNull(); + }); + }); + }); + + function assertButton(name: string, exists: boolean, disabled: boolean = false): void { + describe(`${name} button`, () => { + let btn: DebugElement; + + beforeEach(() => { + btn = fixture.debugElement.query(By.css(`#dso-${name}-btn`)); + }); + + if (exists) { + it('should exist', () => { + expect(btn).toBeTruthy(); + }); + + it(`should${disabled ? ' ' : ' not '}be disabled`, () => { + expect(btn.nativeElement.disabled).toBe(disabled); + }); + } else { + it('should not exist', () => { + expect(btn).toBeNull(); + }); + } + }); + } + +}); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts new file mode 100644 index 0000000000..d67a7ea738 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -0,0 +1,261 @@ +import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AlertType } from '../../shared/alert/aletr-type'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { DsoEditMetadataForm } from './dso-edit-metadata-form'; +import { map } from 'rxjs/operators'; +import { ActivatedRoute, Data } from '@angular/router'; +import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { RemoteData } from '../../core/data/remote-data'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { + getFirstCompletedRemoteData, +} from '../../core/shared/operators'; +import { UpdateDataService } from '../../core/data/update-data.service'; +import { ResourceType } from '../../core/shared/resource-type'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { MetadataFieldSelectorComponent } from './metadata-field-selector/metadata-field-selector.component'; +import { Observable } from 'rxjs/internal/Observable'; +import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service'; +import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { HALDataService } from '../../core/data/base/hal-data-service.interface'; + +@Component({ + selector: 'ds-dso-edit-metadata', + styleUrls: ['./dso-edit-metadata.component.scss'], + templateUrl: './dso-edit-metadata.component.html', +}) +/** + * Component showing a table of all metadata on a DSpaceObject and options to modify them + */ +export class DsoEditMetadataComponent implements OnInit, OnDestroy { + /** + * DSpaceObject to edit metadata for + */ + @Input() dso: DSpaceObject; + + /** + * Reference to the component responsible for showing a metadata-field selector + * Used to validate its contents (existing metadata field) before adding a new metadata value + */ + @ViewChild(MetadataFieldSelectorComponent) metadataFieldSelectorComponent: MetadataFieldSelectorComponent; + + /** + * Resolved update data-service for the given DSpaceObject (depending on its type, e.g. ItemDataService for an Item) + * Used to send the PATCH request + */ + @Input() updateDataService: UpdateDataService; + + /** + * Type of the DSpaceObject in String + * Used to resolve i18n messages + */ + dsoType: string; + + /** + * A dynamic form object containing all information about the metadata and the changes made to them, see {@link DsoEditMetadataForm} + */ + form: DsoEditMetadataForm; + + /** + * The metadata field entered by the user for a new metadata value + */ + newMdField: string; + + // Properties determined by the state of the dynamic form, updated by onValueSaved() + isReinstatable: boolean; + hasChanges: boolean; + isEmpty: boolean; + + /** + * Whether or not the form is currently being submitted + */ + saving$: BehaviorSubject = new BehaviorSubject(false); + + /** + * Tracks for which metadata-field a drag operation is taking place + * Null when no drag is currently happening for any field + * This is a BehaviorSubject that is passed down to child components, to give them the power to alter the state + */ + draggingMdField$: BehaviorSubject = new BehaviorSubject(null); + + /** + * Whether or not the metadata field is currently being validated + */ + loadingFieldValidation$: BehaviorSubject = new BehaviorSubject(false); + + /** + * Combination of saving$ and loadingFieldValidation$ + * Emits true when any of the two emit true + */ + savingOrLoadingFieldValidation$: Observable; + + /** + * The AlertType enumeration for access in the component's template + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + + /** + * Subscription for updating the current DSpaceObject + * Unsubscribed from in ngOnDestroy() + */ + dsoUpdateSubscription: Subscription; + + constructor(protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + protected parentInjector: Injector, + protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer, + @Inject(DATA_SERVICE_FACTORY) protected getDataServiceFor: (resourceType: ResourceType) => GenericConstructor>) { + } + + /** + * Read the route (or parent route)'s data to retrieve the current DSpaceObject + * After it's retrieved, initialise the data-service and form + */ + ngOnInit(): void { + if (hasNoValue(this.dso)) { + this.dsoUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( + map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)), + map((data: any) => data.dso) + ).subscribe((rd: RemoteData) => { + this.dso = rd.payload; + this.initDataService(); + this.initForm(); + }); + } else { + this.initDataService(); + this.initForm(); + } + this.savingOrLoadingFieldValidation$ = observableCombineLatest([this.saving$, this.loadingFieldValidation$]).pipe( + map(([saving, loading]: [boolean, boolean]) => saving || loading), + ); + } + + /** + * Initialise (resolve) the data-service for the current DSpaceObject + */ + initDataService(): void { + let type: ResourceType; + if (typeof this.dso.type === 'string') { + type = new ResourceType(this.dso.type); + } else { + type = this.dso.type; + } + if (hasNoValue(this.updateDataService)) { + const provider = this.getDataServiceFor(type); + this.updateDataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + } + this.dsoType = type.value; + } + + /** + * Initialise the dynamic form object by passing the DSpaceObject's metadata + * Call onValueSaved() to update the form's state properties + */ + initForm(): void { + this.form = new DsoEditMetadataForm(this.dso.metadata); + this.onValueSaved(); + } + + /** + * Update the form's state properties + */ + onValueSaved(): void { + this.hasChanges = this.form.hasChanges(); + this.isReinstatable = this.form.isReinstatable(); + this.isEmpty = Object.keys(this.form.fields).length === 0; + } + + /** + * Submit the current changes to the form by retrieving json PATCH operations from the form and sending it to the + * DSpaceObject's data-service + * Display notificiations and reset the form afterwards if successful + */ + submit(): void { + this.saving$.next(true); + this.updateDataService.patch(this.dso, this.form.getOperations(this.arrayMoveChangeAnalyser)).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + this.saving$.next(false); + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.error.title`), rd.errorMessage); + } else { + this.notificationsService.success( + this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.saved.title`), + this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.saved.content`) + ); + this.dso = rd.payload; + this.initForm(); + } + }); + } + + /** + * Confirm the newly added value + * @param saved Whether or not the value was manually saved (only then, add the value to its metadata field) + */ + confirmNewValue(saved: boolean): void { + if (saved) { + this.setMetadataField(); + } + } + + /** + * Set the metadata field of the temporary added new metadata value + * This will move the new value to its respective parent metadata field + * Validate the metadata field first + */ + setMetadataField(): void { + this.form.resetReinstatable(); + this.loadingFieldValidation$.next(true); + this.metadataFieldSelectorComponent.validate().subscribe((valid: boolean) => { + this.loadingFieldValidation$.next(false); + if (valid) { + this.form.setMetadataField(this.newMdField); + this.onValueSaved(); + } + }); + } + + /** + * Add a new temporary metadata value + */ + add(): void { + this.newMdField = undefined; + this.form.add(); + } + + /** + * Discard all changes within the current form + */ + discard(): void { + this.form.discard(); + this.onValueSaved(); + } + + /** + * Restore any changes previously discarded from the form + */ + reinstate(): void { + this.form.reinstate(); + this.onValueSaved(); + } + + /** + * Unsubscribe from any open subscriptions + */ + ngOnDestroy(): void { + if (hasValue(this.dsoUpdateSubscription)) { + this.dsoUpdateSubscription.unsubscribe(); + } + } + +} diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html new file mode 100644 index 0000000000..4c310bd81b --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -0,0 +1,19 @@ +
+ +
{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}
+ +
diff --git a/src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.scss b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.scss similarity index 100% rename from src/app/shared/item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component.scss rename to src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.scss diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts new file mode 100644 index 0000000000..e0fde0e8f2 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts @@ -0,0 +1,122 @@ +import { MetadataFieldSelectorComponent } from './metadata-field-selector.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RegistryService } from '../../../core/registry/registry.service'; +import { MetadataField } from '../../../core/metadata/metadata-field.model'; +import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { By } from '@angular/platform-browser'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; + +describe('MetadataFieldSelectorComponent', () => { + let component: MetadataFieldSelectorComponent; + let fixture: ComponentFixture; + + let registryService: RegistryService; + let notificationsService: NotificationsService; + + let metadataSchema: MetadataSchema; + let metadataFields: MetadataField[]; + + beforeEach(waitForAsync(() => { + metadataSchema = Object.assign(new MetadataSchema(), { + id: 0, + prefix: 'dc', + namespace: 'http://dublincore.org/documents/dcmi-terms/', + }); + metadataFields = [ + Object.assign(new MetadataField(), { + id: 0, + element: 'description', + qualifier: undefined, + schema: createSuccessfulRemoteDataObject$(metadataSchema), + }), + Object.assign(new MetadataField(), { + id: 1, + element: 'description', + qualifier: 'abstract', + schema: createSuccessfulRemoteDataObject$(metadataSchema), + }), + ]; + + registryService = jasmine.createSpyObj('registryService', { + queryMetadataFields: createSuccessfulRemoteDataObject$(createPaginatedList(metadataFields)), + }); + notificationsService = jasmine.createSpyObj('notificationsService', ['error', 'success']); + + TestBed.configureTestingModule({ + declarations: [MetadataFieldSelectorComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: RegistryService, useValue: registryService }, + { provide: NotificationsService, useValue: notificationsService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MetadataFieldSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('when a query is entered', () => { + const query = 'test query'; + + beforeEach(() => { + component.showInvalid = true; + component.query$.next(query); + }); + + it('should reset showInvalid', () => { + expect(component.showInvalid).toBeFalse(); + }); + + it('should query the registry service for metadata fields and include the schema', () => { + expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema')); + }); + }); + + describe('validate', () => { + it('should return an observable true and show no feedback if the current mdField exists in registry', (done) => { + component.mdField = 'dc.description.abstract'; + component.validate().subscribe((result) => { + expect(result).toBeTrue(); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.invalid-feedback'))).toBeNull(); + done(); + }); + }); + + it('should return an observable false and show invalid feedback if the current mdField is missing in registry', (done) => { + component.mdField = 'dc.fake.field'; + component.validate().subscribe((result) => { + expect(result).toBeFalse(); + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('.invalid-feedback'))).toBeTruthy(); + done(); + }); + }); + + describe('when querying the metadata fields returns an error response', () => { + beforeEach(() => { + (registryService.queryMetadataFields as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Failed')); + }); + + it('should return an observable false and show a notification', (done) => { + component.mdField = 'dc.description.abstract'; + component.validate().subscribe((result) => { + expect(result).toBeFalse(); + expect(notificationsService.error).toHaveBeenCalled(); + done(); + }); + }); + }); + }); +}); diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts new file mode 100644 index 0000000000..5053a4b83d --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -0,0 +1,188 @@ +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { + getAllSucceededRemoteData, getFirstCompletedRemoteData, + metadataFieldsToString +} from '../../../core/shared/operators'; +import { Observable } from 'rxjs/internal/Observable'; +import { RegistryService } from '../../../core/registry/registry.service'; +import { FormControl } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { hasValue } from '../../../shared/empty.util'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { of } from 'rxjs/internal/observable/of'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'ds-metadata-field-selector', + styleUrls: ['./metadata-field-selector.component.scss'], + templateUrl: './metadata-field-selector.component.html' +}) +/** + * Component displaying a searchable input for metadata-fields + */ +export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterViewInit { + /** + * Type of the DSpaceObject + * Used to resolve i18n messages + */ + @Input() dsoType: string; + + /** + * The currently entered metadata field + */ + @Input() mdField: string; + + /** + * If true, the input will be automatically focussed upon when the component is first loaded + */ + @Input() autofocus = false; + + /** + * Emit any changes made to the metadata field + * This will only emit after a debounce takes place to avoid constant emits when the user is typing + */ + @Output() mdFieldChange = new EventEmitter(); + + /** + * Reference to the metadata-field's input + */ + @ViewChild('mdFieldInput', { static: true }) mdFieldInput: ElementRef; + + /** + * List of available metadata field options to choose from, dependent on the current query the user entered + * Shows up in a dropdown below the input + */ + mdFieldOptions$: Observable; + + /** + * FormControl for the input + */ + public input: FormControl = new FormControl(); + + /** + * The current query to update mdFieldOptions$ for + * This is controlled by a debounce, to avoid too many requests + */ + query$: BehaviorSubject = new BehaviorSubject(null); + + /** + * The amount of time to debounce the query for (in ms) + */ + debounceTime = 300; + + /** + * Whether or not the the user just selected a value + * This flag avoids the metadata field from updating twice, which would result in the dropdown opening again right after selecting a value + */ + selectedValueLoading = false; + + /** + * Whether or not to show the invalid feedback + * True when validate() is called and the mdField isn't present in the available metadata fields retrieved from the server + */ + showInvalid = false; + + /** + * Subscriptions to unsubscribe from on destroy + */ + subs: Subscription[] = []; + + constructor(protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { + } + + /** + * Subscribe to any changes made to the input, with a debounce and fire a query, as well as emit the change from this component + * Update the mdFieldOptions$ depending on the query$ fired by querying the server + */ + ngOnInit(): void { + this.subs.push( + this.input.valueChanges.pipe( + debounceTime(this.debounceTime), + ).subscribe((valueChange) => { + if (!this.selectedValueLoading) { + this.query$.next(valueChange); + } + this.selectedValueLoading = false; + this.mdField = valueChange; + this.mdFieldChange.emit(this.mdField); + }), + ); + this.mdFieldOptions$ = this.query$.pipe( + distinctUntilChanged(), + switchMap((query: string) => { + this.showInvalid = false; + if (query !== null) { + return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe( + getAllSucceededRemoteData(), + metadataFieldsToString(), + ); + } else { + return [[]]; + } + }), + ); + } + + /** + * Focus the input if autofocus is enabled + */ + ngAfterViewInit(): void { + if (this.autofocus) { + this.mdFieldInput.nativeElement.focus(); + } + } + + /** + * Validate the metadata field to check if it exists on the server and return an observable boolean for success/error + * Upon subscribing to the returned observable, the showInvalid flag is updated accordingly to show the feedback under the input + */ + validate(): Observable { + return this.registryService.queryMetadataFields(this.mdField, null, true, false, followLink('schema')).pipe( + getFirstCompletedRemoteData(), + switchMap((rd) => { + if (rd.hasSucceeded) { + return of(rd).pipe( + metadataFieldsToString(), + take(1), + map((fields: string[]) => fields.indexOf(this.mdField) > -1), + tap((exists: boolean) => this.showInvalid = !exists), + ); + } else { + this.notificationsService.error(this.translate.instant(`${this.dsoType}.edit.metadata.metadatafield.error`), rd.errorMessage); + return [false]; + } + }), + ); + } + + /** + * Select a metadata field from the dropdown options + * @param mdFieldOption + */ + select(mdFieldOption: string): void { + this.selectedValueLoading = true; + this.input.setValue(mdFieldOption); + } + + /** + * Unsubscribe from any open subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub: Subscription) => hasValue(sub)).forEach((sub: Subscription) => sub.unsubscribe()); + } +} diff --git a/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts new file mode 100644 index 0000000000..ba21907c99 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component.ts @@ -0,0 +1,33 @@ +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { DsoEditMetadataComponent } from './dso-edit-metadata.component'; +import { Component, Input } from '@angular/core'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { UpdateDataService } from '../../core/data/update-data.service'; + +@Component({ + selector: 'ds-themed-dso-edit-metadata', + styleUrls: [], + templateUrl: './../../shared/theme-support/themed.component.html', +}) +export class ThemedDsoEditMetadataComponent extends ThemedComponent { + + @Input() dso: DSpaceObject; + + @Input() updateDataService: UpdateDataService; + + protected inAndOutputNames: (keyof DsoEditMetadataComponent & keyof this)[] = ['dso', 'updateDataService']; + + protected getComponentName(): string { + return 'DsoEditMetadataComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./dso-edit-metadata.component`); + } + + +} diff --git a/src/app/dso-shared/dso-shared.module.ts b/src/app/dso-shared/dso-shared.module.ts new file mode 100644 index 0000000000..7d44d6a920 --- /dev/null +++ b/src/app/dso-shared/dso-shared.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { DsoEditMetadataComponent } from './dso-edit-metadata/dso-edit-metadata.component'; +import { MetadataFieldSelectorComponent } from './dso-edit-metadata/metadata-field-selector/metadata-field-selector.component'; +import { DsoEditMetadataFieldValuesComponent } from './dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component'; +import { DsoEditMetadataValueComponent } from './dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component'; +import { DsoEditMetadataHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component'; +import { DsoEditMetadataValueHeadersComponent } from './dso-edit-metadata/dso-edit-metadata-value-headers/dso-edit-metadata-value-headers.component'; +import { ThemedDsoEditMetadataComponent } from './dso-edit-metadata/themed-dso-edit-metadata.component'; + +@NgModule({ + imports: [ + SharedModule, + ], + declarations: [ + DsoEditMetadataComponent, + ThemedDsoEditMetadataComponent, + MetadataFieldSelectorComponent, + DsoEditMetadataFieldValuesComponent, + DsoEditMetadataValueComponent, + DsoEditMetadataHeadersComponent, + DsoEditMetadataValueHeadersComponent, + ], + exports: [ + DsoEditMetadataComponent, + ThemedDsoEditMetadataComponent, + MetadataFieldSelectorComponent, + DsoEditMetadataFieldValuesComponent, + DsoEditMetadataValueComponent, + DsoEditMetadataHeadersComponent, + DsoEditMetadataValueHeadersComponent, + ], +}) +export class DsoSharedModule { + +} diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index bf73a62447..2d1f9b477e 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss index 3575cae797..b810d2ddd4 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.scss @@ -1 +1,2 @@ @import '../../../../../styles/variables.scss'; + diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index b370431a27..010b5ee5b5 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index 27ee373237..88ed9e17dd 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 4469a2ac29..7e20edca6b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -34,6 +34,7 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history import { VersionDataService } from '../../../../core/data/version-data.service'; import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; import { SearchService } from '../../../../core/shared/search/search.service'; +import { mockRouteService } from '../../../../item-page/simple/item-types/shared/item.component.spec'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -99,7 +100,7 @@ describe('JournalComponent', () => { { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, - { provide: RouteService, useValue: {} } + { provide: RouteService, useValue: mockRouteService } ], schemas: [NO_ERRORS_SCHEMA] 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 3bf861e10d..07585d4d16 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -20,6 +20,7 @@ import { JournalVolumeSidebarSearchListElementComponent } from './item-list-elem import { JournalIssueSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal-issue/journal-issue-sidebar-search-list-element.component'; import { JournalSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/journal/journal-sidebar-search-list-element.component'; import { ItemSharedModule } from '../../item-page/item-shared.module'; +import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -47,7 +48,8 @@ const ENTRY_COMPONENTS = [ imports: [ CommonModule, ItemSharedModule, - SharedModule + SharedModule, + ResultsBackButtonModule ], declarations: [ ...ENTRY_COMPONENTS diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index ac92e4ad6d..d8b3815f6e 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index a73236006b..20e458a906 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -1,3 +1,4 @@ +
diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 3fb5e2d520..ee75abaae8 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -1,3 +1,4 @@ +
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 721a22be08..e3525cc881 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -29,6 +29,7 @@ import { OrgUnitSidebarSearchListElementComponent } from './item-list-elements/s import { PersonSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component'; import { ProjectSidebarSearchListElementComponent } from './item-list-elements/sidebar-search-list-elements/project/project-sidebar-search-list-element.component'; import { ItemSharedModule } from '../../item-page/item-shared.module'; +import { ResultsBackButtonModule } from '../../shared/results-back-button/results-back-button.module'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -69,7 +70,8 @@ const COMPONENTS = [ CommonModule, ItemSharedModule, SharedModule, - NgbTooltipModule + NgbTooltipModule, + ResultsBackButtonModule ], declarations: [ ...COMPONENTS, diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts index 7a38a02cf4..954f7bc591 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -61,7 +61,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants; if (this.useNameVariants) { - const defaultValue = this.dsoTitle; + const defaultValue = this.dso ? this.dsoNameService.getName(this.dso) : undefined; const alternatives = this.allMetadataValues(this.alternativeField); this.allSuggestions = [defaultValue, ...alternatives]; diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index 7d761c42dd..305407f8d2 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -55,7 +55,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu ngOnInit() { super.ngOnInit(); - const defaultValue = this.dsoTitle; + const defaultValue = this.dso ? this.dsoNameService.getName(this.dso) : undefined; const alternatives = this.allMetadataValues(this.alternativeField); this.allSuggestions = [defaultValue, ...alternatives]; diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 88236d381e..97265d7a23 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -64,7 +64,7 @@