diff --git a/src/app/features/metadata/services/index.ts b/src/app/features/metadata/services/index.ts deleted file mode 100644 index 92c69e450..000000000 --- a/src/app/features/metadata/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './metadata.service'; diff --git a/src/app/features/metadata/store/metadata.state.ts b/src/app/features/metadata/store/metadata.state.ts index 245895fd9..af839233a 100644 --- a/src/app/features/metadata/store/metadata.state.ts +++ b/src/app/features/metadata/store/metadata.state.ts @@ -5,9 +5,9 @@ import { catchError, finalize, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; +import { MetadataService } from '@osf/shared/services/metadata.service'; import { CedarMetadataRecord, CedarMetadataRecordJsonApi, MetadataModel } from '../models'; -import { MetadataService } from '../services'; import { AddCedarMetadataRecordToState, diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html index 77f120b97..7275ce5ce 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html @@ -39,7 +39,7 @@ [submission]="item" [status]="selectedReviewOption()" (selected)="navigateToRegistration(item)" - (loadContributors)="loadContributors(item)" + (loadAdditionalData)="loadAdditionalData(item)" (loadMoreContributors)="loadMoreContributors(item)" > diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts index ac0f00e76..26b08f29c 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts @@ -26,6 +26,7 @@ import { RegistrySort, SubmissionReviewStatus } from '../../enums'; import { RegistryModeration } from '../../models'; import { GetRegistrySubmissionContributors, + GetRegistrySubmissionFunders, GetRegistrySubmissions, LoadMoreRegistrySubmissionContributors, RegistryModerationSelectors, @@ -63,6 +64,7 @@ export class RegistryPendingSubmissionsComponent implements OnInit { getRegistrySubmissions: GetRegistrySubmissions, getRegistrySubmissionContributors: GetRegistrySubmissionContributors, loadMoreRegistrySubmissionContributors: LoadMoreRegistrySubmissionContributors, + getRegistrySubmissionFunders: GetRegistrySubmissionFunders, }); readonly submissions = select(RegistryModerationSelectors.getRegistrySubmissions); @@ -129,6 +131,11 @@ export class RegistryPendingSubmissionsComponent implements OnInit { this.actions.loadMoreRegistrySubmissionContributors(item.id); } + loadAdditionalData(item: RegistryModeration) { + this.actions.getRegistrySubmissionContributors(item.id); + this.actions.getRegistrySubmissionFunders(item.id); + } + private getStatusFromQueryParams() { const queryParams = this.route.snapshot.queryParams; const statusValues = Object.values(SubmissionReviewStatus); diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html index 006ce75fc..8bc4d4a6e 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -64,12 +64,19 @@

{{ submission().title }}

+
+ +
diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts index 770db64d4..93b331b09 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts @@ -10,6 +10,7 @@ import { ContributorsListComponent } from '@osf/shared/components/contributors-l import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; import { DateAgoPipe } from '@osf/shared/pipes/date-ago.pipe'; +import { FunderAwardsListComponent } from '@shared/funder-awards-list/funder-awards-list.component'; import { REGISTRY_ACTION_LABEL, ReviewStatusIcon } from '../../constants'; import { ActionStatus, SubmissionReviewStatus } from '../../enums'; @@ -29,6 +30,7 @@ import { RegistryModeration } from '../../models'; AccordionHeader, AccordionContent, ContributorsListComponent, + FunderAwardsListComponent, ], templateUrl: './registry-submission-item.component.html', styleUrl: './registry-submission-item.component.scss', @@ -37,9 +39,8 @@ import { RegistryModeration } from '../../models'; export class RegistrySubmissionItemComponent { status = input.required(); submission = input.required(); - loadContributors = output(); loadMoreContributors = output(); - + loadAdditionalData = output(); selected = output(); readonly reviewStatusIcon = ReviewStatusIcon; @@ -67,6 +68,6 @@ export class RegistrySubmissionItemComponent { }); handleOpen() { - this.loadContributors.emit(); + this.loadAdditionalData.emit(); } } diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html index 5066d15f7..73386a8f1 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html @@ -39,7 +39,7 @@ [submission]="item" [status]="selectedReviewOption()" (selected)="navigateToRegistration(item)" - (loadContributors)="loadContributors(item)" + (loadAdditionalData)="loadAdditionalData(item)" (loadMoreContributors)="loadMoreContributors(item)" > diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts index e664daacc..3271d565f 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts @@ -26,6 +26,7 @@ import { RegistrySort, SubmissionReviewStatus } from '../../enums'; import { RegistryModeration } from '../../models'; import { GetRegistrySubmissionContributors, + GetRegistrySubmissionFunders, GetRegistrySubmissions, LoadMoreRegistrySubmissionContributors, RegistryModerationSelectors, @@ -63,6 +64,7 @@ export class RegistrySubmissionsComponent implements OnInit { getRegistrySubmissions: GetRegistrySubmissions, getRegistrySubmissionContributors: GetRegistrySubmissionContributors, loadMoreRegistrySubmissionContributors: LoadMoreRegistrySubmissionContributors, + getRegistrySubmissionFunders: GetRegistrySubmissionFunders, }); readonly submissions = select(RegistryModerationSelectors.getRegistrySubmissions); @@ -129,6 +131,11 @@ export class RegistrySubmissionsComponent implements OnInit { this.actions.loadMoreRegistrySubmissionContributors(item.id); } + loadAdditionalData(item: RegistryModeration) { + this.actions.getRegistrySubmissionContributors(item.id); + this.actions.getRegistrySubmissionFunders(item.id); + } + private getStatusFromQueryParams() { const queryParams = this.route.snapshot.queryParams; const statusValues = Object.values(SubmissionReviewStatus); diff --git a/src/app/features/moderation/models/registry-moderation.model.ts b/src/app/features/moderation/models/registry-moderation.model.ts index 2d59b2681..31da4f051 100644 --- a/src/app/features/moderation/models/registry-moderation.model.ts +++ b/src/app/features/moderation/models/registry-moderation.model.ts @@ -1,3 +1,4 @@ +import { Funder } from '@osf/features/metadata/models'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; import { ContributorModel } from '@shared/models/contributors/contributor.model'; @@ -18,4 +19,6 @@ export interface RegistryModeration { contributors?: ContributorModel[]; totalContributors?: number; contributorsPage?: number; + funders?: Funder[]; + fundersLoading?: boolean; } diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts index 3e350142c..eafd2f856 100644 --- a/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts @@ -27,3 +27,9 @@ export class LoadMoreRegistrySubmissionContributors { constructor(public registryId: string) {} } + +export class GetRegistrySubmissionFunders { + static readonly type = `${ACTION_SCOPE} Get Registry Submission Funders`; + + constructor(public registryId: string) {} +} diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts index 52b068e89..5c4715b76 100644 --- a/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts @@ -7,6 +7,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { PaginatedData } from '@osf/shared/models/paginated-data.model'; +import { MetadataService } from '@osf/shared/services/metadata.service'; import { DEFAULT_TABLE_PARAMS } from '@shared/constants/default-table-params.constants'; import { ResourceType } from '@shared/enums/resource-type.enum'; import { ContributorsService } from '@shared/services/contributors.service'; @@ -16,6 +17,7 @@ import { RegistryModerationService } from '../../services'; import { GetRegistrySubmissionContributors, + GetRegistrySubmissionFunders, GetRegistrySubmissions, LoadMoreRegistrySubmissionContributors, } from './registry-moderation.actions'; @@ -29,7 +31,7 @@ import { REGISTRY_MODERATION_STATE_DEFAULTS, RegistryModerationStateModel } from export class RegistryModerationState { private readonly registryModerationService = inject(RegistryModerationService); private readonly contributorsService = inject(ContributorsService); - + private readonly metadataService = inject(MetadataService); @Action(GetRegistrySubmissionContributors) getRegistrySubmissionContributors( ctx: StateContext, @@ -151,4 +153,59 @@ export class RegistryModerationState { catchError((error) => handleSectionError(ctx, 'submissions', error)) ); } + + @Action(GetRegistrySubmissionFunders) + getRegistrySubmissionFunders( + ctx: StateContext, + { registryId }: GetRegistrySubmissionFunders + ) { + const state = ctx.getState(); + const submission = state.submissions.data.find((s) => s.id === registryId); + + if (submission?.funders && submission.funders.length > 0) { + return; + } + + ctx.setState( + patch({ + submissions: patch({ + data: updateItem( + (submission) => submission.id === registryId, + patch({ fundersLoading: true }) + ), + }), + }) + ); + + return this.metadataService.getCustomItemMetadata(registryId).pipe( + tap((res) => { + ctx.setState( + patch({ + submissions: patch({ + data: updateItem( + (submission) => submission.id === registryId, + patch({ + funders: res.funders, + fundersLoading: false, + }) + ), + }), + }) + ); + }), + catchError((error) => { + ctx.setState( + patch({ + submissions: patch({ + data: updateItem( + (submission) => submission.id === registryId, + patch({ fundersLoading: false }) + ), + }), + }) + ); + return handleSectionError(ctx, 'submissions', error); + }) + ); + } } diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.html b/src/app/shared/funder-awards-list/funder-awards-list.component.html new file mode 100644 index 000000000..95fcdad0b --- /dev/null +++ b/src/app/shared/funder-awards-list/funder-awards-list.component.html @@ -0,0 +1,24 @@ +
+ @if (isLoading()) { +
+ +
+ } @else { + @if (funders().length) { +

{{ 'resourceCard.labels.funderAwards' | translate }}

+
+ @for (funder of funders(); track $index) { + +
{{ funder.funderName }}
+ +
{{ funder.awardNumber }}
+
+ } +
+ } + } +
diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.scss b/src/app/shared/funder-awards-list/funder-awards-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts b/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts new file mode 100644 index 000000000..9614e930c --- /dev/null +++ b/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts @@ -0,0 +1,85 @@ +import { TranslateModule } from '@ngx-translate/core'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { provideRouter } from '@angular/router'; + +import { Funder } from '@osf/features/metadata/models'; + +import { FunderAwardsListComponent } from './funder-awards-list.component'; + +describe('FunderAwardsListComponent', () => { + let component: FunderAwardsListComponent; + let fixture: ComponentFixture; + + const MOCK_REGISTRY_ID = 'test-registry-123'; + const MOCK_FUNDERS: Funder[] = [ + { + funderName: 'National Science Foundation', + awardNumber: 'NSF-2024-X', + funderIdentifier: '10.13039/100000001', + funderIdentifierType: 'Crossref Funder ID', + awardUri: 'https://nsf.gov/award/1', + awardTitle: 'Advanced Research Initiative', + }, + { + funderName: 'European Research Council', + awardNumber: 'ERC-888', + funderIdentifier: '10.13039/501100000781', + funderIdentifierType: 'Crossref Funder ID', + awardUri: 'https://erc.europa.eu/award/2', + awardTitle: 'Future Fellowship', + }, + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FunderAwardsListComponent, TranslateModule.forRoot()], + providers: [provideRouter([])], + }).compileComponents(); + + fixture = TestBed.createComponent(FunderAwardsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should not render the list or label if funders array is empty', () => { + fixture.componentRef.setInput('funders', []); + fixture.detectChanges(); + const label = fixture.debugElement.query(By.css('p')); + const links = fixture.debugElement.queryAll(By.css('a')); + expect(label).toBeNull(); + expect(links.length).toBe(0); + }); + + it('should render a list of funders when data is provided', () => { + fixture.componentRef.setInput('funders', MOCK_FUNDERS); + fixture.componentRef.setInput('registryId', MOCK_REGISTRY_ID); + fixture.detectChanges(); + const links = fixture.debugElement.queryAll(By.css('a')); + expect(links.length).toBe(2); + const firstItemText = links[0].nativeElement.textContent; + expect(firstItemText).toContain('National Science Foundation'); + expect(firstItemText).toContain('NSF-2024-X'); + }); + + it('should generate the correct router link', () => { + fixture.componentRef.setInput('funders', MOCK_FUNDERS); + fixture.componentRef.setInput('registryId', MOCK_REGISTRY_ID); + fixture.detectChanges(); + const linkDebugEl = fixture.debugElement.query(By.css('a')); + const href = linkDebugEl.nativeElement.getAttribute('href'); + expect(href).toContain(`/${MOCK_REGISTRY_ID}/metadata/osf`); + }); + + it('should open links in a new tab', () => { + fixture.componentRef.setInput('funders', MOCK_FUNDERS); + fixture.detectChanges(); + const linkDebugEl = fixture.debugElement.query(By.css('a')); + expect(linkDebugEl.attributes['target']).toBe('_blank'); + }); +}); diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.ts b/src/app/shared/funder-awards-list/funder-awards-list.component.ts new file mode 100644 index 000000000..969bf1053 --- /dev/null +++ b/src/app/shared/funder-awards-list/funder-awards-list.component.ts @@ -0,0 +1,21 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { Funder } from '@osf/features/metadata/models'; + +@Component({ + selector: 'osf-funder-awards-list', + imports: [RouterLink, TranslatePipe, Skeleton], + templateUrl: './funder-awards-list.component.html', + styleUrl: './funder-awards-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FunderAwardsListComponent { + funders = input([]); + registryId = input(null); + isLoading = input(false); +} diff --git a/src/app/features/metadata/services/metadata.service.ts b/src/app/shared/services/metadata.service.ts similarity index 97% rename from src/app/features/metadata/services/metadata.service.ts rename to src/app/shared/services/metadata.service.ts index 75ae0c86b..b74d82b64 100644 --- a/src/app/features/metadata/services/metadata.service.ts +++ b/src/app/shared/services/metadata.service.ts @@ -4,24 +4,25 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { IdentifierModel } from '@osf/shared/models/identifiers/identifier.model'; -import { LicenseOptions } from '@osf/shared/models/license/license.model'; -import { BaseNodeAttributesJsonApi } from '@osf/shared/models/nodes/base-node-attributes-json-api.model'; -import { JsonApiService } from '@osf/shared/services/json-api.service'; - -import { CedarRecordsMapper, MetadataMapper } from '../mappers'; +import { CedarRecordsMapper, MetadataMapper } from '@osf/features/metadata/mappers'; import { CedarMetadataRecord, CedarMetadataRecordJsonApi, CedarMetadataTemplateJsonApi, CedarRecordDataBinding, + CrossRefFundersResponse, + CustomItemMetadataRecord, CustomMetadataJsonApi, CustomMetadataJsonApiResponse, MetadataJsonApi, MetadataJsonApiResponse, -} from '../models'; -import { CrossRefFundersResponse, CustomItemMetadataRecord, MetadataModel } from '../models/metadata.model'; + MetadataModel, +} from '@osf/features/metadata/models'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { IdentifierModel } from '@osf/shared/models/identifiers/identifier.model'; +import { LicenseOptions } from '@osf/shared/models/license/license.model'; +import { BaseNodeAttributesJsonApi } from '@osf/shared/models/nodes/base-node-attributes-json-api.model'; +import { JsonApiService } from '@osf/shared/services/json-api.service'; @Injectable({ providedIn: 'root', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 13b5ec8f8..3e1af97b6 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2778,6 +2778,7 @@ "withdrawn": "Withdrawn", "from": "From:", "funder": "Funder:", + "funderAwards": "Funder awards:", "resourceNature": "Resource type:", "dateCreated": "Date created", "dateModified": "Date modified",