diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 2c743e219d..ee8c4d685f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -22,7 +22,7 @@ import { import { isEqual } from 'lodash'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { select, Store } from '@ngrx/store'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { Angulartics2GoogleAnalytics } from 'angulartics2'; @@ -49,6 +49,7 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { getDefaultThemeConfig } from '../config/config.util'; import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; +import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface'; @Component({ selector: 'ds-app', @@ -106,6 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit { private localeService: LocaleService, private breadcrumbsService: BreadcrumbsService, private modalService: NgbModal, + private modalConfig: NgbModalConfig, @Optional() private cookiesService: KlaroService, @Optional() private googleAnalyticsService: GoogleAnalyticsService, ) { @@ -166,6 +168,16 @@ export class AppComponent implements OnInit, AfterViewInit { } ngOnInit() { + /** Implement behavior for interface {@link ModalBeforeDismiss} */ + this.modalConfig.beforeDismiss = async function () { + if (typeof this?.componentInstance?.beforeDismiss === 'function') { + return this.componentInstance.beforeDismiss(); + } + + // fall back to default behavior + return true; + }; + this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( distinctUntilChanged() ); 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 6e73935672..dbd9d03994 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 @@ -3,6 +3,9 @@ {{'journalissue.page.titleprefix' | translate}}
+
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts index f96379dafd..f5e9dc9b2b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; @listableObjectComponent('JournalIssue', ViewMode.StandalonePage) @Component({ @@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh /** * The component for displaying metadata and relations of an item of the type Journal Issue */ -export class JournalIssueComponent extends ItemComponent { +export class JournalIssueComponent extends VersionedItemComponent { } 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 5d4d8d06ce..8b19c37033 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 @@ -3,6 +3,9 @@ {{'journalvolume.page.titleprefix' | translate}}
+
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts index eeb93e7070..cc09be7959 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; @listableObjectComponent('JournalVolume', ViewMode.StandalonePage) @Component({ @@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh /** * The component for displaying metadata and relations of an item of the type Journal Volume */ -export class JournalVolumeComponent extends ItemComponent { +export class JournalVolumeComponent extends VersionedItemComponent { } 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 d51c55e5d6..45cbc1f839 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 @@ -3,6 +3,9 @@ {{'journal.page.titleprefix' | translate}}
+
@@ -44,7 +47,8 @@ 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 0e756b7dc9..3ed73e7891 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 @@ -29,6 +29,11 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { JournalComponent } from './journal.component'; import { RouteService } from '../../../../core/services/route.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service'; +import { VersionDataService } from '../../../../core/data/version-data.service'; +import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; +import { SearchService } from '../../../../core/shared/search/search.service'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -65,12 +70,15 @@ describe('JournalComponent', () => { }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule, + ], declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe], providers: [ { provide: ItemDataService, useValue: {} }, @@ -86,7 +94,11 @@ describe('JournalComponent', () => { { provide: DSOChangeAnalyzer, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: VersionHistoryDataService, useValue: {} }, + { provide: VersionDataService, useValue: {} }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: WorkspaceitemDataService, useValue: {} }, + { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: {} } ], diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts index 3fe0903145..acfd31d8f6 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; @listableObjectComponent('Journal', ViewMode.StandalonePage) @Component({ @@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh /** * The component for displaying metadata and relations of an item of the type Journal */ -export class JournalComponent extends ItemComponent { +export class JournalComponent extends VersionedItemComponent { } 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 4315d2a91c..ff4bc4d226 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 @@ -3,6 +3,9 @@ {{'orgunit.page.titleprefix' | translate}}
+
@@ -54,12 +57,12 @@ [relationTypes]="[{ label: 'isOrgUnitOfPerson', filter: 'isOrgUnitOfPerson', - configuration: 'person' + configuration: 'person-relationships' }, { label: 'isOrgUnitOfProject', filter: 'isOrgUnitOfProject', - configuration: 'project' + configuration: 'project-relationships' }]"> diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts index ab756db562..cbf8497f35 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; @listableObjectComponent('OrgUnit', ViewMode.StandalonePage) @Component({ @@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh /** * The component for displaying metadata and relations of an item of the type Organisation Unit */ -export class OrgUnitComponent extends ItemComponent { +export class OrgUnitComponent extends VersionedItemComponent { } 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 8523f398cb..ace42f844e 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 @@ -2,8 +2,11 @@

{{'person.page.titleprefix' | translate}}

-
+
+
@@ -63,7 +66,8 @@
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts index 27fdd2ab15..8fde5ee69a 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts @@ -1,9 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { - listableObjectComponent -} from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; import { MetadataValue } from '../../../../core/shared/metadata.models'; @listableObjectComponent('Person', ViewMode.StandalonePage) @@ -15,7 +13,7 @@ import { MetadataValue } from '../../../../core/shared/metadata.models'; /** * The component for displaying metadata and relations of an item of the type Person */ -export class PersonComponent extends ItemComponent { +export class PersonComponent extends VersionedItemComponent { /** * Returns the metadata values to be used for the page title. @@ -36,4 +34,5 @@ export class PersonComponent extends ItemComponent { } return metadataValues; } + } 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 7960631f3d..a068878fb4 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 @@ -3,6 +3,9 @@ {{'project.page.titleprefix' | translate}}
+
diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts index e53d8afd69..066427fc0d 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../../../../item-page/simple/item-types/versioned-item/versioned-item.component'; @listableObjectComponent('Project', ViewMode.StandalonePage) @Component({ @@ -12,5 +12,5 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh /** * The component for displaying metadata and relations of an item of the type Project */ -export class ProjectComponent extends ItemComponent { +export class ProjectComponent extends VersionedItemComponent { } diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 96454914cd..d83202ce12 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -12,6 +12,9 @@ {{'publication.page.titleprefix' | translate}}
+
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index 761849f232..404890e36d 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -33,6 +33,11 @@ import { import { PublicationComponent } from './publication.component'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { RouteService } from '../../../../core/services/route.service'; +import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service'; +import { VersionDataService } from '../../../../core/data/version-data.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; +import { SearchService } from '../../../../core/shared/search/search.service'; const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [iiifEnabled], @@ -64,12 +69,15 @@ describe('PublicationComponent', () => { } }; TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule, + ], declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe], providers: [ { provide: ItemDataService, useValue: {} }, @@ -85,7 +93,11 @@ describe('PublicationComponent', () => { { provide: HttpClient, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, + { provide: VersionHistoryDataService, useValue: {} }, + { provide: VersionDataService, useValue: {} }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: WorkspaceitemDataService, useValue: {} }, + { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService } ], diff --git a/src/app/item-page/simple/item-types/publication/publication.component.ts b/src/app/item-page/simple/item-types/publication/publication.component.ts index 5ace8d0473..ba5037a104 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { ItemComponent } from '../shared/item.component'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { VersionedItemComponent } from '../versioned-item/versioned-item.component'; /** * Component that represents a publication Item page @@ -14,6 +14,6 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh templateUrl: './publication.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PublicationComponent extends ItemComponent { +export class PublicationComponent extends VersionedItemComponent { } diff --git a/src/app/item-page/simple/item-types/shared/item.component.spec.ts b/src/app/item-page/simple/item-types/shared/item.component.spec.ts index 6f7684b896..e5287f674d 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Store } from '@ngrx/store'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import {Observable, of as observableOf} from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; @@ -32,8 +32,13 @@ import { ItemComponent } from './item.component'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { RouteService } from '../../../../core/services/route.service'; import { MetadataValue } from '../../../../core/shared/metadata.models'; -import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service'; -import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service'; +import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; +import { SearchService } from '../../../../core/shared/search/search.service'; +import { VersionDataService } from '../../../../core/data/version-data.service'; +import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; +import { ResearcherProfileService } from '../../../../core/profile/researcher-profile.service'; export const iiifEnabled = Object.assign(new MetadataValue(),{ 'value': 'true', @@ -77,12 +82,15 @@ export function getItemPageFieldsTest(mockItem: Item, component) { }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule, + ], declarations: [component, GenericItemPageFieldComponent, TruncatePipe], providers: [ { provide: ItemDataService, useValue: {} }, @@ -96,9 +104,13 @@ export function getItemPageFieldsTest(mockItem: Item, component) { { provide: HALEndpointService, useValue: {} }, { provide: HttpClient, useValue: {} }, { provide: DSOChangeAnalyzer, useValue: {} }, + { provide: VersionHistoryDataService, useValue: {} }, + { provide: VersionDataService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: WorkspaceitemDataService, useValue: {} }, + { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: {} }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ResearcherProfileService, useValue: {} } diff --git a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts index cd2eb3a19b..7f61cee10b 100644 --- a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts +++ b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts @@ -62,6 +62,8 @@ export class VersionedItemComponent extends ItemComponent { activeModal.componentInstance.createVersionEvent.pipe( switchMap((summary: string) => this.versionHistoryService.createVersion(item._links.self.href, summary)), getFirstCompletedRemoteData(), + // close model (should be displaying loading/waiting indicator) when version creation failed/succeeded + tap(() => activeModal.close()), // show success/failure notification tap((res: RemoteData) => { this.itemVersionShared.notifyCreateNewVersion(res); }), // get workspace item diff --git a/src/app/shared/interfaces/modal-before-dismiss.interface.ts b/src/app/shared/interfaces/modal-before-dismiss.interface.ts new file mode 100644 index 0000000000..fca28e1cff --- /dev/null +++ b/src/app/shared/interfaces/modal-before-dismiss.interface.ts @@ -0,0 +1,25 @@ +import { NgbModalConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +/** + * If a component implementing this interface is used to create a modal (i.e. it is passed to {@link NgbModal#open}), + * and that modal is dismissed, then {@link #beforeDismiss} will be called. + * + * This behavior is implemented for the whole app, by setting a default value for {@link NgbModalConfig#beforeDismiss} + * in {@link AppComponent}. + * + * Docs: https://ng-bootstrap.github.io/#/components/modal/api + */ +export interface ModalBeforeDismiss { + + /** + * Callback right before the modal will be dismissed. + * If this function returns: + * - false + * - a promise resolved with false + * - a promise that is rejected + * then the modal won't be dismissed. + * Docs: https://ng-bootstrap.github.io/#/components/modal/api#NgbModalOptions + */ + beforeDismiss(): boolean | Promise; + +} diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html index a083679108..0aa22f80cf 100644 --- a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html +++ b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.html @@ -1,4 +1,4 @@ -
+
+ + + + + diff --git a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts index 31bb3078c0..23ee62e628 100644 --- a/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts +++ b/src/app/shared/item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component.ts @@ -1,16 +1,19 @@ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { BehaviorSubject } from 'rxjs'; +import { ModalBeforeDismiss } from '../../../interfaces/modal-before-dismiss.interface'; @Component({ selector: 'ds-item-versions-summary-modal', templateUrl: './item-versions-summary-modal.component.html', styleUrls: ['./item-versions-summary-modal.component.scss'] }) -export class ItemVersionsSummaryModalComponent { +export class ItemVersionsSummaryModalComponent implements OnInit, ModalBeforeDismiss { versionNumber: number; newVersionSummary: string; firstVersion = true; + submitted$: BehaviorSubject; @Output() createVersionEvent: EventEmitter = new EventEmitter(); @@ -19,13 +22,24 @@ export class ItemVersionsSummaryModalComponent { ) { } + ngOnInit() { + this.submitted$ = new BehaviorSubject(false); + } + onModalClose() { this.activeModal.dismiss(); } + beforeDismiss(): boolean | Promise { + // prevent the modal from being dismissed after version creation is initiated + return !this.submitted$.getValue(); + } + onModalSubmit() { this.createVersionEvent.emit(this.newVersionSummary); - this.activeModal.close(); + this.submitted$.next(true); + // NOTE: the caller of this modal is responsible for closing it, + // e.g. after the version creation POST request completed. } } diff --git a/src/app/shared/item/item-versions/item-versions.component.ts b/src/app/shared/item/item-versions/item-versions.component.ts index abd21e6f4f..b7b8182658 100644 --- a/src/app/shared/item/item-versions/item-versions.component.ts +++ b/src/app/shared/item/item-versions/item-versions.component.ts @@ -340,6 +340,9 @@ export class ItemVersionsComponent implements OnInit { version.item.pipe(getFirstSucceededRemoteDataPayload()) ])), mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)), + getFirstCompletedRemoteData(), + // close model (should be displaying loading/waiting indicator) when version creation failed/succeeded + tap(() => activeModal.close()), // show success/failure notification tap((newVersionRD: RemoteData) => { this.itemVersionShared.notifyCreateNewVersion(newVersionRD); diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.html b/src/app/shared/search/search-export-csv/search-export-csv.component.html new file mode 100644 index 0000000000..7bf8704300 --- /dev/null +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.scss b/src/app/shared/search/search-export-csv/search-export-csv.component.scss new file mode 100644 index 0000000000..4b0ab3c44a --- /dev/null +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.scss @@ -0,0 +1,4 @@ +.export-button { + background: var(--ds-admin-sidebar-bg); + border-color: var(--ds-admin-sidebar-bg); +} \ No newline at end of file diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts new file mode 100644 index 0000000000..82c15feeac --- /dev/null +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts @@ -0,0 +1,182 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { SearchExportCsvComponent } from './search-export-csv.component'; +import { ScriptDataService } from '../../../core/data/processes/script-data.service'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { Script } from '../../../process-page/scripts/script.model'; +import { Process } from '../../../process-page/processes/process.model'; +import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; +import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths'; +import { SearchFilter } from '../models/search-filter.model'; +import { PaginatedSearchOptions } from '../models/paginated-search-options.model'; + +describe('SearchExportCsvComponent', () => { + let component: SearchExportCsvComponent; + let fixture: ComponentFixture; + + let scriptDataService: ScriptDataService; + let authorizationDataService: AuthorizationDataService; + let notificationsService; + let router; + + const script = Object.assign(new Script(), {id: 'metadata-export-search', name: 'metadata-export-search'}); + const process = Object.assign(new Process(), {processId: 5, scriptName: 'metadata-export-search'}); + + const searchConfig = new PaginatedSearchOptions({ + configuration: 'test-configuration', + scope: 'test-scope', + query: 'test-query', + filters: [ + new SearchFilter('f.filter1', ['filter1value1,equals', 'filter1value2,equals']), + new SearchFilter('f.filter2', ['filter2value1,contains']), + new SearchFilter('f.filter3', ['[2000 TO 2001]'], 'equals') + ] + }); + + function initBeforeEachAsync() { + scriptDataService = jasmine.createSpyObj('scriptDataService', { + findById: createSuccessfulRemoteDataObject$(script), + invoke: createSuccessfulRemoteDataObject$(process) + }); + authorizationDataService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + + notificationsService = new NotificationsServiceStub(); + + router = jasmine.createSpyObj('authorizationService', ['navigateByUrl']); + TestBed.configureTestingModule({ + declarations: [SearchExportCsvComponent], + imports: [TranslateModule.forRoot(), NgbModule], + providers: [ + {provide: ScriptDataService, useValue: scriptDataService}, + {provide: AuthorizationDataService, useValue: authorizationDataService}, + {provide: NotificationsService, useValue: notificationsService}, + {provide: Router, useValue: router}, + ] + }).compileComponents(); + } + + function initBeforeEach() { + fixture = TestBed.createComponent(SearchExportCsvComponent); + component = fixture.componentInstance; + component.searchConfig = searchConfig; + fixture.detectChanges(); + } + + describe('init', () => { + describe('comp', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should init the comp', () => { + expect(component).toBeTruthy(); + }); + }); + describe('when the user is an admin and the metadata-export-search script is present ', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should add the button', () => { + const debugElement = fixture.debugElement.query(By.css('button.export-button')); + expect(debugElement).toBeDefined(); + }); + }); + describe('when the user is not an admin', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + (authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should not add the button', () => { + const debugElement = fixture.debugElement.query(By.css('button.export-button')); + expect(debugElement).toBeNull(); + }); + }); + describe('when the metadata-export-search script is not present', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + (scriptDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not found', 404)); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should should not add the button', () => { + const debugElement = fixture.debugElement.query(By.css('button.export-button')); + expect(debugElement).toBeNull(); + }); + }); + }); + describe('export', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should call the invoke script method with the correct parameters', () => { + component.export(); + expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-search', + [ + {name: '-q', value: searchConfig.query}, + {name: '-s', value: searchConfig.scope}, + {name: '-c', value: searchConfig.configuration}, + {name: '-f', value: 'filter1,equals=filter1value1'}, + {name: '-f', value: 'filter1,equals=filter1value2'}, + {name: '-f', value: 'filter2,contains=filter2value1'}, + {name: '-f', value: 'filter3,equals=[2000 TO 2001]'}, + ], []); + + component.searchConfig = null; + fixture.detectChanges(); + + component.export(); + expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-search', [], []); + + }); + it('should show a success message when the script was invoked successfully and redirect to the corresponding process page', () => { + component.export(); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessDetailRoute(process.processId)); + }); + it('should show an error message when the script was not invoked successfully and stay on the current page', () => { + (scriptDataService.invoke as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Error', 500)); + + component.export(); + + expect(notificationsService.error).toHaveBeenCalled(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); + describe('clicking the button', () => { + beforeEach(waitForAsync(() => { + initBeforeEachAsync(); + })); + beforeEach(() => { + initBeforeEach(); + }); + it('should trigger the export function', () => { + spyOn(component, 'export'); + + const debugElement = fixture.debugElement.query(By.css('button.export-button')); + debugElement.triggerEventHandler('click', null); + + expect(component.export).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.ts new file mode 100644 index 0000000000..6ad105342f --- /dev/null +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.ts @@ -0,0 +1,110 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; +import { ScriptDataService } from '../../../core/data/processes/script-data.service'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { map } from 'rxjs/operators'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { hasValue, isNotEmpty } from '../../empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Process } from '../../../process-page/processes/process.model'; +import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { PaginatedSearchOptions } from '../models/paginated-search-options.model'; + +@Component({ + selector: 'ds-search-export-csv', + styleUrls: ['./search-export-csv.component.scss'], + templateUrl: './search-export-csv.component.html', +}) +/** + * Display a button to export the current search results as csv + */ +export class SearchExportCsvComponent implements OnInit { + + /** + * The current configuration of the search + */ + @Input() searchConfig: PaginatedSearchOptions; + + /** + * Observable used to determine whether the button should be shown + */ + shouldShowButton$: Observable; + + /** + * The message key used for the tooltip of the button + */ + tooltipMsg = 'metadata-export-search.tooltip'; + + constructor(private scriptDataService: ScriptDataService, + private authorizationDataService: AuthorizationDataService, + private notificationsService: NotificationsService, + private translateService: TranslateService, + private router: Router + ) { + } + + ngOnInit(): void { + const scriptExists$ = this.scriptDataService.findById('metadata-export-search').pipe( + getFirstCompletedRemoteData(), + map((rd) => rd.isSuccess && hasValue(rd.payload)) + ); + + const isAuthorized$ = this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf); + + this.shouldShowButton$ = observableCombineLatest([scriptExists$, isAuthorized$]).pipe( + map(([scriptExists, isAuthorized]: [boolean, boolean]) => scriptExists && isAuthorized) + ); + } + + /** + * Start the export of the items based on the current search configuration + */ + export() { + const parameters = []; + if (hasValue(this.searchConfig)) { + if (isNotEmpty(this.searchConfig.query)) { + parameters.push({name: '-q', value: this.searchConfig.query}); + } + if (isNotEmpty(this.searchConfig.scope)) { + parameters.push({name: '-s', value: this.searchConfig.scope}); + } + if (isNotEmpty(this.searchConfig.configuration)) { + parameters.push({name: '-c', value: this.searchConfig.configuration}); + } + if (isNotEmpty(this.searchConfig.filters)) { + this.searchConfig.filters.forEach((filter) => { + if (hasValue(filter.values)) { + filter.values.forEach((value) => { + let operator; + let filterValue; + if (hasValue(filter.operator)) { + operator = filter.operator; + filterValue = value; + } else { + operator = value.substring(value.lastIndexOf(',') + 1); + filterValue = value.substring(0, value.lastIndexOf(',')); + } + const valueToAdd = `${filter.key.substring(2)},${operator}=${filterValue}`; + parameters.push({name: '-f', value: valueToAdd}); + }); + } + }); + } + } + + this.scriptDataService.invoke('metadata-export-search', parameters, []).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get('metadata-export-search.submit.success')); + this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); + } else { + this.notificationsService.error(this.translateService.get('metadata-export-search.submit.error')); + } + }); + } +} diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index c383a2fa1a..e506fd2b8e 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -1,4 +1,7 @@ +

{{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

+ +