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 }}
+
+ }
+ }
+
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",