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}}
-
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 @@
-
+
+
+
+
+
+
{{'item.version.create.modal.submitted.text' | translate}}
+
+
+
+
+
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 }}
+
+
0" @fadeIn>