diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts index 35f9bee04f..8e3e3a2747 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts @@ -26,11 +26,13 @@ import { LdnService } from '../ldn-services-model/ldn-services.model'; import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data'; import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; import { Operation } from 'fast-json-patch'; -import { RestRequestMethod } from 'src/app/core/data/rest-request-method'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data'; -import { ldnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model'; -import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; -import { hasValue } from 'src/app/shared/empty.util'; +import { LdnServiceConstrain } from '../ldn-services-model/ldn-service.constrain.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; +import { SearchDataImpl } from '../../../core/data/base/search-data'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; /** * A service responsible for fetching/sending data from/to the REST API on the ldnservices endpoint @@ -43,6 +45,9 @@ export class LdnServicesService extends IdentifiableDataService impl private deleteData: DeleteDataImpl; private patchData: PatchDataImpl; private comparator: ChangeAnalyzer; + private searchData: SearchDataImpl; + + private findByPatternEndpoint = 'byInboundPattern'; constructor( protected requestService: RequestService, @@ -54,6 +59,7 @@ export class LdnServicesService extends IdentifiableDataService impl super('ldnservices', requestService, rdbService, objectCache, halService); this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); @@ -84,6 +90,12 @@ export class LdnServicesService extends IdentifiableDataService impl return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + const params = [new RequestParam('pattern', pattern)]; + const findListOptions = Object.assign(new FindListOptions(), options, { searchParams: params }); + return this.searchData.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { return this.deleteData.delete(objectId, copyVirtualMetadata); } @@ -92,7 +104,7 @@ export class LdnServicesService extends IdentifiableDataService impl return this.deleteData.deleteByHref(href, copyVirtualMetadata); } - public invoke(serviceName: string, serviceId: string, parameters: ldnServiceConstrain[], files: File[]): Observable> { + public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable> { const requestId = this.requestService.generateRequestId(); this.getBrowseEndpoint().pipe( take(1), @@ -115,7 +127,7 @@ export class LdnServicesService extends IdentifiableDataService impl ); } - private getInvocationFormData(constrain: ldnServiceConstrain[], files: File[]): FormData { + private getInvocationFormData(constrain: LdnServiceConstrain[], files: File[]): FormData { const form: FormData = new FormData(); form.set('properties', JSON.stringify(constrain)); files.forEach((file: File) => { diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts index 69a9baf273..500cefbd52 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts @@ -1,3 +1,3 @@ -export class ldnServiceConstrain { +export class LdnServiceConstrain { void: any; } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 88da00f02c..d0f2dbbbaf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -190,7 +190,11 @@ import { SuggestionSource } from './suggestion-notifications/reciter-suggestions import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; -import { Itemfilter } from "../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters"; +import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; +import { + CoarNotifyConfigDataService +} from '../submission/sections/section-coar-notify/coar-notify-config-data.service'; +import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -315,7 +319,8 @@ const PROVIDERS = [ OrcidHistoryDataService, SupervisionOrderDataService, LdnServicesService, - LdnItemfiltersService + LdnItemfiltersService, + CoarNotifyConfigDataService ]; /** @@ -398,7 +403,8 @@ export const models = SuggestionTarget, SuggestionSource, LdnService, - Itemfilter + Itemfilter, + SubmissionCoarNotifyConfig ]; diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts index a3ccd49dac..dd19c3fb8b 100644 --- a/src/app/core/submission/models/workspaceitem-sections.model.ts +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -26,3 +26,5 @@ export type WorkspaceitemSectionDataType | WorkspaceitemSectionSherpaPoliciesObject | WorkspaceitemSectionIdentifiersObject | string; + + diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 4a916cfe23..b3536a6fbf 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -9,7 +9,8 @@
- + [sectionData]="object"> +
+ diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html index e6ae9d1b9c..99bcec168f 100644 --- a/src/app/submission/sections/container/section-container.component.html +++ b/src/app/submission/sections/container/section-container.component.html @@ -48,4 +48,4 @@ - \ No newline at end of file + diff --git a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts new file mode 100644 index 0000000000..7b8d309667 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../../../core/data/base/data-service.decorator'; +import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; +import { FindAllData, FindAllDataImpl } from '../../../core/data/base/find-all-data'; +import { DeleteData, DeleteDataImpl } from '../../../core/data/base/delete-data'; +import { RequestService } from '../../../core/data/request.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { map, take } from 'rxjs/operators'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { MultipartPostRequest } from '../../../core/data/request.models'; +import { RestRequest } from '../../../core/data/rest-request.model'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; +import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; +import { CreateData, CreateDataImpl } from '../../../core/data/base/create-data'; +import { PatchData, PatchDataImpl } from '../../../core/data/base/patch-data'; +import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; +import { Operation } from 'fast-json-patch'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { hasValue } from '../../../shared/empty.util'; + + +/** + * A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint + */ +@Injectable() +@dataService(SUBMISSION_COAR_NOTIFY_CONFIG) +export class CoarNotifyConfigDataService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData { + createData: CreateDataImpl; + private findAllData: FindAllDataImpl; + private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; + private comparator: ChangeAnalyzer; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('submissioncoarnotifyconfigs', requestService, rdbService, objectCache, halService); + + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); + } + + + create(object: SubmissionCoarNotifyConfig): Observable> { + return this.createData.create(object); + } + + patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + update(object: SubmissionCoarNotifyConfig): Observable> { + return this.patchData.update(object); + } + + commitUpdates(method?: RestRequestMethod): void { + return this.patchData.commitUpdates(method); + } + + createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable { + return this.patchData.createPatchFromCache(object); + } + + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } + + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + public invoke(serviceName: string, serviceId: string, files: File[]): Observable> { + const requestId = this.requestService.generateRequestId(); + this.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'submissioncoarnotifyconfigmodel', serviceId).toString()), + map((endpoint: string) => { + const body = this.getInvocationFormData(files); + return new MultipartPostRequest(requestId, endpoint, body); + }) + ).subscribe((request: RestRequest) => this.requestService.send(request)); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + public SubmissionCoarNotifyConfigModelWithNameExistsAndCanExecute(scriptName: string): Observable { + return this.findById(scriptName).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + return hasValue(rd.payload); + }), + ); + } + + private getInvocationFormData(files: File[]): FormData { + const form: FormData = new FormData(); + files.forEach((file: File) => { + form.append('file', file); + }); + return form; + } +} diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts new file mode 100644 index 0000000000..53e41783ce --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts @@ -0,0 +1,13 @@ +/** + * The resource type for Ldn-Services + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +import { ResourceType } from '../../../core/shared/resource-type'; + + +export const SUBMISSION_COAR_NOTIFY_CONFIG = new ResourceType('submissioncoarnotifyconfig'); + +export const COAR_NOTIFY_WORKSPACEITEM = new ResourceType('workspaceitem'); + diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html new file mode 100644 index 0000000000..cd287912ef --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html @@ -0,0 +1,124 @@ +
+ +
+ +
+
+
+
+ + +
+ +
+
+ + {{'submission.section.section-coar-notify.small.notification' | translate : {pattern : pattern} }} + + + + {{ error.message | translate}} + + +
+
+ +
+
{{ 'submission.section.section-coar-notify.selection.description' | translate }}
+
+ {{ ldnServiceByPattern[pattern][serviceIndex].description }} +
+ + + {{ 'submission.section.section-coar-notify.selection.no-description' | translate }} + + +
+
+
+
+
+
+ + {{ 'submission.section.section-coar-notify.notification.error' | translate }} + +
+
+
+
+
+
+ +

+ {{'submission.section.section-coar-notify.info.no-pattern' | translate }} +

+
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss new file mode 100644 index 0000000000..3ac8827f74 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss @@ -0,0 +1,4 @@ +// Getting styles for NgbDropdown +@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss'; +@import '../../../shared/form/form.component.scss'; + diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts new file mode 100644 index 0000000000..c83479284a --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify.component'; + +describe('LdnServiceComponent', () => { + let component: SubmissionSectionCoarNotifyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubmissionSectionCoarNotifyComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SubmissionSectionCoarNotifyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts new file mode 100644 index 0000000000..83dad65f3b --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts @@ -0,0 +1,294 @@ +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { Observable, Subscription } from 'rxjs'; +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionsService } from '../sections.service'; +import { SectionDataObject } from '../models/section-data.model'; + +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; + +import { getFirstCompletedRemoteData, getPaginatedListPayload, getRemoteDataPayload } from '../../../core/shared/operators'; +import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { LdnService } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; +import { filter, map, take, tap } from 'rxjs/operators'; +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { SubmissionSectionError } from '../../objects/submission-section-error.model'; + +/** + * This component represents a section that contains the submission section-coar-notify form. + */ +@Component({ + selector: 'ds-submission-section-coar-notify', + templateUrl: './section-coar-notify.component.html', + styleUrls: ['./section-coar-notify.component.scss'], + providers: [NgbDropdown] +}) +@renderSectionFor(SectionsType.CoarNotify) +export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent { + + /** + * Contains an array of string patterns. + */ + patterns: string[] = []; + /** + * An object that maps string keys to arrays of LdnService objects. + * Used to store LdnService objects by pattern. + */ + ldnServiceByPattern: { [key: string]: LdnService[] } = {}; + /** + * A map representing all services for each pattern + * { + * 'pattern': { + * 'index': 'service.id' + * } + * } + * + * @type {{ [key: string]: {[key: number]: number} }} + * @memberof SubmissionSectionCoarNotifyComponent + */ + previousServices: { [key: string]: {[key: number]: number} } = {}; + + /** + * The [[JsonPatchOperationPathCombiner]] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + /** + * A map representing all field on their way to be removed + * @type {Map} + */ + protected fieldsOnTheirWayToBeRemoved: Map = new Map(); + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + constructor(protected ldnServicesService: LdnServicesService, + protected formOperationsService: SectionFormOperationsService, + protected operationsBuilder: JsonPatchOperationsBuilder, + protected sectionService: SectionsService, + protected coarNotifyConfigDataService: CoarNotifyConfigDataService, + protected chd: ChangeDetectorRef, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + /** + * Initialize all instance variables + */ + onSectionInit() { + this.setCoarNotifyConfig(); + this.getSectionServerErrorsAndSetErrorsToDisplay(); + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + } + + /** + * Method called when section is initialized + * Retriev available NotifyConfigs + */ + setCoarNotifyConfig() { + this.subs.push( + this.coarNotifyConfigDataService.findAll().pipe( + getFirstCompletedRemoteData() + ).subscribe((data) => { + if (data.hasSucceeded) { + this.patterns = data.payload.page[0].patterns; + this.initSelectedServicesByPattern(); + } + })); + } + + /** + * Handles the change event of a select element. + * @param pattern - The pattern of the select element. + * @param index - The index of the select element. + */ + onChange(pattern: string, index: number, selectedService: LdnService | null) { + // do nothing if the selected value is the same as the previous one + if (this.ldnServiceByPattern[pattern][index]?.id === selectedService?.id) { + return; + } + + // initialize the previousServices object for the pattern if it does not exist + if (!this.previousServices[pattern]) { + this.previousServices[pattern] = {}; + } + + if (hasNoValue(selectedService)) { + // on value change, remove the path when the selected value is null + // and remove the previous value stored for the same index and pattern + this.operationsBuilder.remove(this.pathCombiner.getPath([pattern, index.toString()])); + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + this.ldnServiceByPattern[pattern][index] = null; + this.previousServices[pattern][index] = null; + return; + } + // store the previous value + this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index]?.id; + // set the new value + this.ldnServiceByPattern[pattern][index] = selectedService; + + const hasPrevValueStored = hasValue(this.previousServices[pattern][index]) && this.previousServices[pattern][index] !== selectedService.id; + if (hasPrevValueStored) { + // replace the path + // when there is a previous value stored and it is different from the new one + this.operationsBuilder.replace(this.pathCombiner.getPath([pattern, index.toString()]), selectedService.id, true); + } else { + // add the path when there is no previous value stored + this.operationsBuilder.add(this.pathCombiner.getPath([pattern, '-']), [selectedService.id], false, true); + } + // set the previous value to the new value + this.previousServices[pattern][index] = this.ldnServiceByPattern[pattern][index].id; + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + this.chd.detectChanges(); + } + + /** + * Initializes the selected services by pattern. + * Loops through each pattern and filters the services based on the pattern. + * If the section data has a value for the pattern, it adds the service to the selected services by pattern. + * If the section data does not have a value for the pattern, it adds a null service to the selected services by pattern, + * so that the select element is initialized with a null value and to display the default select input. + */ + initSelectedServicesByPattern(): void { + this.patterns.forEach((pattern) => { + if (hasValue(this.sectionData.data[pattern])) { + this.subs.push( + this.filterServices(pattern) + .subscribe((services: LdnService[]) => { + const selectedServices = services.filter((service) => { + const selection = (this.sectionData.data[pattern] as LdnService[]).find((s: LdnService) => s.id === service.id); + this.addService(pattern, selection); + return this.sectionData.data[pattern].includes(service.id); + }); + this.ldnServiceByPattern[pattern] = selectedServices; + }) + ); + } else { + this.ldnServiceByPattern[pattern] = []; + this.addService(pattern, null); + } + }); + } + + /** + * Adds a new service to the selected services for the given pattern. + * @param pattern - The pattern to add the new service to. + * @param newService - The new service to add. + */ + addService(pattern: string, newService: LdnService) { + // Your logic to add a new service to the selected services for the pattern + // Example: Push the newService to the array corresponding to the pattern + if (!this.ldnServiceByPattern[pattern]) { + this.ldnServiceByPattern[pattern] = []; + } + this.ldnServiceByPattern[pattern].push(newService); + } + + /** + * Removes the service at the specified index from the array corresponding to the pattern. + * (part of next phase of implementation) + */ + removeService(pattern: string, serviceIndex: number) { + if (this.ldnServiceByPattern[pattern]) { + // Remove the service at the specified index from the array + this.ldnServiceByPattern[pattern].splice(serviceIndex, 1); + } + } + + /** + * Method called when dropdowns for the section are initialized + * Retrieve services with corresponding patterns to the dropdowns. + */ + filterServices(pattern: string): Observable { + return this.ldnServicesService.findByInboundPattern(pattern).pipe( + getFirstCompletedRemoteData(), + tap((rd) => { + if (rd.hasFailed) { + throw new Error(`Failed to retrieve services for pattern ${pattern}`); + } + }), + filter((rd) => rd.hasSucceeded), + getRemoteDataPayload(), + getPaginatedListPayload(), + map((res: LdnService[]) => res.filter((service) => + this.hasInboundPattern(service, pattern))) + ); + } + + /** + * Checks if the given service has the specified inbound pattern type. + * @param service - The service to check. + * @param patternType - The inbound pattern type to look for. + * @returns True if the service has the specified inbound pattern type, false otherwise. + */ + hasInboundPattern(service: any, patternType: string): boolean { + return service.notifyServiceInboundPatterns.some((pattern: { pattern: string }) => { + return pattern.pattern === patternType; + }); + } + + /** + * Retrieves server errors for the current section and sets them to display. + * @returns An Observable that emits the validation errors for the current section. + */ + private getSectionServerErrorsAndSetErrorsToDisplay() { + this.subs.push( + this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + ).subscribe((validationErrors: SubmissionSectionError[]) => { + if (isNotEmpty(validationErrors)) { + validationErrors.forEach((error) => { + this.sectionService.setSectionError(this.submissionId, this.sectionData.id, error); + }); + } + })); + } + + /** + * Returns an observable of the errors for the current section that match the given pattern and index. + * @param pattern - The pattern to match against the error paths. + * @param index - The index to match against the error paths. + * @returns An observable of the errors for the current section that match the given pattern and index. + */ + public getShownSectionErrors$(pattern: string, index: number): Observable { + return this.sectionService.getShownSectionErrors(this.submissionId, this.sectionData.id, this.sectionData.sectionType) + .pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + map((validationErrors: SubmissionSectionError[]) => { + return validationErrors.filter((error) => { + const path = `${pattern}/${index}`; + return error.path.includes(path); + }); + }) + ); + } + + /** + * @returns An observable that emits a boolean indicating whether the section has any server errors or not. + */ + protected getSectionStatus(): Observable { + return this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + map((validationErrors) => isEmpty(validationErrors) + )); + } + + /** + * Unsubscribe from all subscriptions + */ + onSectionDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts new file mode 100644 index 0000000000..41ef69cd7a --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts @@ -0,0 +1,35 @@ +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; + +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { COAR_NOTIFY_WORKSPACEITEM } from './section-coar-notify-service.resource-type'; + + +/** An CoarNotify and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject { + static type = COAR_NOTIFY_WORKSPACEITEM; + + @excludeFromEquals + @autoserialize + endorsement?: number[]; + + @deserializeAs('id') + review?: number[]; + + @autoserialize + ingest?: number[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts new file mode 100644 index 0000000000..04973f80c8 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts @@ -0,0 +1,39 @@ +import { ResourceType } from '../../../core/shared/resource-type'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { autoserialize, deserialize, deserializeAs, inheritSerialization } from 'cerialize'; + +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; + + +/** A SubmissionCoarNotifyConfig and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyConfig extends CacheableObject { + static type = SUBMISSION_COAR_NOTIFY_CONFIG; + + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @deserializeAs('id') + uuid: string; + + @autoserialize + patterns: string[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 6bca8a7252..5f71d1731d 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -9,4 +9,5 @@ export enum SectionsType { SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', Collection = 'collection', + CoarNotify = 'coarnotify' } diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index cf0ab2b369..d839565f8d 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -10,7 +10,7 @@ import { SubmissionFormFooterComponent } from './form/footer/submission-form-foo import { SubmissionFormComponent } from './form/submission-form.component'; import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; import { SubmissionSectionContainerComponent } from './sections/container/section-container.component'; -import { CommonModule } from '@angular/common'; +import { CommonModule, NgOptimizedImage } from '@angular/common'; import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { submissionReducers, SubmissionState } from './submission.reducers'; @@ -67,6 +67,11 @@ import { } 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'; +import { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component'; +import { + CoarNotifyConfigDataService +} from './sections/section-coar-notify/coar-notify-config-data.service'; +import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -76,6 +81,7 @@ const ENTRY_COMPONENTS = [ SubmissionSectionCcLicensesComponent, SubmissionSectionAccessesComponent, SubmissionSectionSherpaPoliciesComponent, + SubmissionSectionCoarNotifyComponent ]; const DECLARATIONS = [ @@ -109,20 +115,22 @@ const DECLARATIONS = [ ]; @NgModule({ - imports: [ - CommonModule, - CoreModule.forRoot(), - SharedModule, - StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(submissionEffects), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - FormModule, - NgbModalModule, - NgbCollapseModule, - NgbAccordionModule, - UploadModule, - ], + imports: [ + CommonModule, + CoreModule.forRoot(), + SharedModule, + StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), + EffectsModule.forFeature(), + EffectsModule.forFeature(submissionEffects), + JournalEntitiesModule.withEntryComponents(), + ResearchEntitiesModule.withEntryComponents(), + FormModule, + NgbModalModule, + NgbCollapseModule, + NgbAccordionModule, + UploadModule, + NgOptimizedImage, + ], declarations: DECLARATIONS, exports: [ ...DECLARATIONS, @@ -135,6 +143,8 @@ const DECLARATIONS = [ SubmissionAccessesConfigDataService, SectionAccessesService, SectionFormOperationsService, + CoarNotifyConfigDataService, + LdnServicesService ] }) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 108433de21..a0a5a8734c 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1982,6 +1982,8 @@ "info.coar-notify.breadcrumbs": "Notify Support", + "submission.sections.notify.info": "The selected service is compatible with the item according to its current status. {{ service.name }}: {{ service.description }}", + "info.feedback.head": "Feedback", "info.feedback.title": "Feedback", @@ -4766,6 +4768,8 @@ "submission.sections.submit.progressbar.sherpaPolicies": "Publisher open access policy information", + "submission.sections.submit.progressbar.coarnotify": "COAR Notify", + "submission.sections.sherpa-policy.title-empty": "No publisher policy information available. If your work has an associated ISSN, please enter it above to see any related publisher open access policies.", "submission.sections.status.errors.title": "Errors", @@ -5012,6 +5016,24 @@ "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", + "submission.section.section-coar-notify.control.label": "Request {{ pattern }} at the following services", + + "submission.section.section-coar-notify.dropdown.no-data": "No data available", + + "submission.section.section-coar-notify.dropdown.select-none": "Select none", + + "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + + "submission.section.section-coar-notify.selection.description": "Selected service's description:", + + "submission.section.section-coar-notify.selection.no-description": "No further information is available", + + "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item. Please check the description for details about which record can be managed by this service.", + + "submission.section.section-coar-notify.info.no-pattern": "No patterns found in the submission.", + + "error.validation.coarnotify.invalidfilter": "Invalid filter, try to select another service or none.", + "submitter.empty": "N/A", "subscriptions.title": "Subscriptions", diff --git a/src/assets/i18n/it.json5 b/src/assets/i18n/it.json5 index 4131d0bee6..9fa58062b8 100644 --- a/src/assets/i18n/it.json5 +++ b/src/assets/i18n/it.json5 @@ -7461,6 +7461,29 @@ // "submission.workspace.generic.view-help": "Select this option to view the item's metadata.", "submission.workspace.generic.view-help": "Seleziona questa opzione per vedere i metadata dell'item.", + // "submission.section.section-coar-notify.control.label": "Request {{ pattern }} at the following services", + // TODO New key - a translation + "submission.section.section-coar-notify.control.label": "Request {{ pattern }} at the following services", + + // "submission.section.section-coar-notify.dropdown.no-data": "No data available", + // TODO New key - a translation + "submission.section.section-coar-notify.dropdown.no-data": "No data available", + + // "submission.section.section-coar-notify.dropdown.select-none": "Select none", + // TODO New key - a translation + "submission.section.section-coar-notify.dropdown.select-none": "Select none", + + // "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + // TODO New key - a translation + "submission.section.section-coar-notify.small.notification": "Select a service for {{ pattern }} of this item", + + // "submission.section.section-coar-notify.selection.description": "Selected service's description:", + // TODO New key - a translation + "submission.section.section-coar-notify.selection.description": "Selected service's description:", + + // "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.", + // TODO New key - a translation + "submission.section.section-coar-notify.notification.error": "The selected service is not suitable for the current item.Please check the description for details about which record can be managed by this service.", // "subscriptions.title": "Subscriptions", "subscriptions.title": "Sottoscrizioni",