diff --git a/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts new file mode 100644 index 0000000000..7d22bf0b61 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-identifiers.model.ts @@ -0,0 +1,8 @@ +/* + * Object model for the data returned by the REST API to present minted identifiers in a submission section + */ +export interface WorkspaceitemSectionIdentifiersObject { + doi?: string + handle?: string + otherIdentifiers?: string[] +} diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index 1112d740ed..f50c3ac67c 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -3,6 +3,7 @@ import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.mod import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model'; +import {WorkspaceitemSectionIdentifiersObject} from "./workspaceitem-section-identifiers.model"; import { WorkspaceitemSectionSherpaPoliciesObject } from './workspaceitem-section-sherpa-policies.model'; /** @@ -23,4 +24,5 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionCcLicenseObject | WorkspaceitemSectionAccessesObject | WorkspaceitemSectionSherpaPoliciesObject + | WorkspaceitemSectionIdentifiersObject | string; diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.html b/src/app/submission/sections/identifiers/section-identifiers.component.html new file mode 100644 index 0000000000..1c78119931 --- /dev/null +++ b/src/app/submission/sections/identifiers/section-identifiers.component.html @@ -0,0 +1,35 @@ + + + + +
+ {{'submission.sections.identifiers.info' | translate}} + +
+
diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts new file mode 100644 index 0000000000..378ec911c7 --- /dev/null +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -0,0 +1,247 @@ +import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgxPaginationModule } from 'ngx-pagination'; +import { cold } from 'jasmine-marbles'; +import {Observable, of as observableOf} from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; + +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { SectionsService } from '../sections.service'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; +import { getMockFormService } from '../../../shared/mocks/form-service.mock'; +import { FormService } from '../../../shared/form/form.service'; +import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsType } from '../sections-type'; +import {mockSectionsData, mockSubmissionCollectionId, mockSubmissionId} from '../../../shared/mocks/submission.mock'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SubmissionSectionIdentifiersComponent } from './section-identifiers.component'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; +import { License } from '../../../core/shared/license.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; +import { Item } from '../../../core/shared/item.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; + +function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService { + return jasmine.createSpyObj('FormOperationsService', { + getConfigAll: jasmine.createSpy('getConfigAll'), + getConfigByHref: jasmine.createSpy('getConfigByHref'), + getConfigByName: jasmine.createSpy('getConfigByName'), + getConfigBySearch: jasmine.createSpy('getConfigBySearch') + }); +} + +function getMockCollectionDataService(): CollectionDataService { + return jasmine.createSpyObj('CollectionDataService', { + findById: jasmine.createSpy('findById'), + findByHref: jasmine.createSpy('findByHref') + }); +} + +const mockItem = Object.assign(new Item(), { + id: 'fake-match-id', + handle: 'fake/handle', + metadata: { + 'dc.title': [ + { + language: null, + value: 'mockmatch' + } + ] + }, +}); + +// Mock identifier data to use with tests +const identifierData: WorkspaceitemSectionIdentifiersObject = { + doi: 'https://doi.org/10.33515/dspace/1', + handle: '123456789/999', + otherIdentifiers: ['123-123-123', 'ANBX-159'] +}; + +// Mock section object to use with tests +const sectionObject: SectionDataObject = { + config: 'https://dspace.org/api/config/submissionforms/identifiers', + mandatory: true, + opened: true, + data: identifierData, + errorsToShow: [], + serverValidationErrors: [], + header: 'submission.sections.submit.progressbar.identifiers', + id: 'identifiers', + sectionType: SectionsType.Identifiers, + sectionVisibility: null +}; + +describe('SubmissionSectionIdentifiersComponent test suite', () => { + let comp: SubmissionSectionIdentifiersComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let submissionServiceStub: any = new SubmissionServiceStub(); + const sectionsServiceStub: any = new SectionsServiceStub(); + let formService: any; + let formOperationsService: any; + let formBuilderService: any; + let collectionDataService: any; + + const submissionId = mockSubmissionId; + const collectionId = mockSubmissionCollectionId; + const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), + }); + + const licenseText = 'License text'; + const mockCollection = Object.assign(new Collection(), { + name: 'Community 1-Collection 1', + id: collectionId, + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1' + }], + license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })) + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgxPaginationModule, + NoopAnimationsModule, + TranslateModule.forRoot(), + ], + declarations: [ + SubmissionSectionIdentifiersComponent, + TestComponent, + ObjNgFor, + VarDirective, + ], + providers: [ + { provide: CollectionDataService, useValue: getMockCollectionDataService() }, + { provide: SectionFormOperationsService, useValue: getMockFormOperationsService() }, + { provide: FormService, useValue: getMockFormService() }, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: SubmissionFormsConfigService, useValue: getMockSubmissionFormsConfigService() }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: SectionsService, useClass: SectionsServiceStub }, + { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: 'collectionIdProvider', useValue: collectionId }, + { provide: 'sectionDataProvider', useValue: sectionObject }, + { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: PaginationService, useValue: paginationService }, + ChangeDetectorRef, + FormBuilderService, + SubmissionSectionIdentifiersComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.getSectionData.and.returnValue(observableOf(identifierData)); + const html = ``; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionSectionIdentifiersComponent', inject([SubmissionSectionIdentifiersComponent], (idComp: SubmissionSectionIdentifiersComponent) => { + expect(idComp).toBeDefined(); + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionSectionIdentifiersComponent); + comp = fixture.componentInstance; + compAsAny = comp; + submissionServiceStub = TestBed.inject(SubmissionService); + formService = TestBed.inject(FormService); + formBuilderService = TestBed.inject(FormBuilderService); + formOperationsService = TestBed.inject(SectionFormOperationsService); + collectionDataService = TestBed.inject(CollectionDataService); + compAsAny.pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionObject.id); + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + // Test initialisation of the submission section + it('Should init section properly', () => { + collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + compAsAny.submissionService.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem); + spyOn(comp, 'getSectionStatus').and.returnValue(observableOf(true)); + spyOn(comp, 'getIdentifierData').and.returnValue(observableOf(identifierData)); + expect(comp.isLoading).toBeTruthy(); + comp.onSectionInit(); + fixture.detectChanges(); + expect(comp.isLoading).toBeFalsy(); + }); + + // The following tests look for proper logic in the getSectionStatus() implementation + // These are very simple as we don't really have a 'false' state unless we're still loading + it('Should return TRUE if the isLoading is FALSE', () => { + compAsAny.isLoading = false; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: true + })); + }); + it('Should return FALSE if the identifier data is missing handle', () => { + compAsAny.isLoadin = true; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: false + })); + }); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.ts b/src/app/submission/sections/identifiers/section-identifiers.component.ts new file mode 100644 index 0000000000..9f5d65c2aa --- /dev/null +++ b/src/app/submission/sections/identifiers/section-identifiers.component.ts @@ -0,0 +1,138 @@ +import {ChangeDetectionStrategy, Component, Inject, Input} from '@angular/core'; + +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { SectionsType } from '../sections-type'; +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionDataObject } from '../models/section-data.model'; +import { SubmissionService } from '../../submission.service'; +import { AlertType } from '../../../shared/alert/aletr-type'; +import { SectionsService } from '../sections.service'; +import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { SubmissionVisibility } from '../../utils/visibility.util'; +import {distinctUntilChanged, filter, map} from 'rxjs/operators'; +import {hasValue} from '../../../shared/empty.util'; + +/** + * This simple component displays DOI, handle and other identifiers that are already minted for the item in + * a workflow / submission section. + * ShowMintIdentifierStep will attempt to reserve an identifier before injecting result data for this component. + * + * @author Kim Shepherd + */ +@Component({ + selector: 'ds-submission-section-identifiers', + templateUrl: './section-identifiers.component.html', + changeDetection: ChangeDetectionStrategy.Default +}) + +@renderSectionFor(SectionsType.Identifiers) +export class SubmissionSectionIdentifiersComponent extends SectionModelComponent { + /** + * The Alert categories. + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + + /** + * Variable to track if the section is loading. + * @type {boolean} + */ + public isLoading = true; + + /** + * Observable identifierData subject + * @type {Observable} + */ + public identifierData$: Observable = new Observable(); + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + public subbedIdentifierData: WorkspaceitemSectionIdentifiersObject; + + /** + * Initialize instance variables. + * + * @param {PaginationService} paginationService + * @param {TranslateService} translate + * @param {SectionsService} sectionService + * @param {SubmissionService} submissionService + * @param {string} injectedCollectionId + * @param {SectionDataObject} injectedSectionData + * @param {string} injectedSubmissionId + */ + constructor(protected translate: TranslateService, + protected sectionService: SectionsService, + protected submissionService: SubmissionService, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + //this.identifierData$ = {} as Observable; + /* + this.subs.push( + this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType).pipe( + filter((identiferData: WorkspaceitemSectionIdentifiersObject) => hasValue()), + distinctUntilChanged()).subscribe((identifierData: WorkspaceitemSectionIdentifiersObject) => { + this.subbedIdentifierData = identifierData; + } + ) + )*/ + super.ngOnInit(); + } + + /** + * Initialize all instance variables and retrieve configuration. + */ + onSectionInit() { + this.isLoading = false; + this.identifierData$ = this.getIdentifierData(); + } + + /** + * Check if identifier section has read-only visibility + */ + isReadOnly(): boolean { + return SubmissionVisibility.isReadOnly( + this.sectionData.sectionVisibility, + this.submissionService.getSubmissionScope() + ); + } + + /** + * Unsubscribe from all subscriptions, if needed. + */ + onSectionDestroy(): void { + return; + } + + /** + * Get section status. Because this simple component never requires human interaction, this is basically + * always going to be the opposite of "is this section still loading". This is not the place for API response + * error checking but determining whether the step can 'proceed'. + * + * @return Observable + * the section status + */ + public getSectionStatus(): Observable { + return observableOf(!this.isLoading); + } + + /** + * Get identifier data (from the REST service) as a simple object with doi, handle, otherIdentifiers variables + * and as an observable so it can update in real-time. + */ + getIdentifierData() { + return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as + Observable; + } + +} diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 6b6f839b7c..6fb7380822 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -7,4 +7,5 @@ export enum SectionsType { collection = 'collection', AccessesCondition = 'accessCondition', SherpaPolicies = 'sherpaPolicy', + Identifiers = 'identifiers', } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index cab4f19c33..91f782225a 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -65,6 +65,7 @@ import { MetadataInformationComponent } from './sections/sherpa-policies/metadata-information/metadata-information.component'; import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; +import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -93,6 +94,7 @@ const DECLARATIONS = [ SubmissionSectionUploadFileComponent, SubmissionSectionUploadFileEditComponent, SubmissionSectionUploadFileViewComponent, + SubmissionSectionIdentifiersComponent, SubmissionImportExternalComponent, ThemedSubmissionImportExternalComponent, SubmissionImportExternalSearchbarComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f68c0ff2ce..3416a7cf9d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4282,7 +4282,17 @@ "submission.sections.general.sections_not_valid": "There are incomplete sections.", + "submission.sections.identifiers.info": "The following identifiers will be created for your item:", + "submission.sections.identifiers.no_handle": "No handles have been minted for this item.", + + "submission.sections.identifiers.no_doi": "No DOIs have been minted for this item.", + + "submission.sections.identifiers.handle_label": "Handle: ", + + "submission.sections.identifiers.doi_label": "DOI: ", + + "submission.sections.identifiers.otherIdentifiers_label": "Other identifiers: ", "submission.sections.submit.progressbar.accessCondition": "Item access conditions",