From a80a105380bec3dcbfde1c9b671f50c9fe1f8cc3 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 28 Apr 2026 16:48:12 +0300 Subject: [PATCH 1/3] fix(resource-information): added resource information to project and registry overview metadata --- .../edit-file-metadata-dialog.component.ts | 4 +- .../file-metadata.component.spec.ts | 4 +- .../file-metadata/file-metadata.component.ts | 4 +- ...tadata-resource-information.component.html | 4 +- ...metadata-resource-information.component.ts | 20 +-- src/app/features/metadata/constants/index.ts | 1 - ...resource-information-dialog.component.html | 19 ++- ...ource-information-dialog.component.spec.ts | 89 +++++-------- .../resource-information-dialog.component.ts | 29 +--- .../metadata/store/metadata.selectors.ts | 5 + .../general-information.component.html | 12 -- .../project-overview-metadata.component.html | 72 +++++++--- ...roject-overview-metadata.component.spec.ts | 9 +- .../project-overview-metadata.component.ts | 11 ++ .../new-registration.component.scss | 1 + .../registry-overview-metadata.component.html | 126 +++++++++++------- ...gistry-overview-metadata.component.spec.ts | 8 +- .../registry-overview-metadata.component.ts | 11 ++ .../funders-list/funders-list.component.html | 15 +++ .../funders-list.component.spec.ts | 29 ++++ .../funders-list/funders-list.component.ts | 18 +++ .../project-secondary-metadata.component.ts | 4 +- src/app/shared/constants/language.const.ts | 2 +- .../resource-type-general-options.const.ts} | 2 +- src/app/shared/pipes/language-label.pipe.ts | 15 +++ .../pipes/resource-type-general-label.pipe.ts | 15 +++ src/assets/i18n/en.json | 4 + 27 files changed, 335 insertions(+), 198 deletions(-) create mode 100644 src/app/shared/components/funders-list/funders-list.component.html create mode 100644 src/app/shared/components/funders-list/funders-list.component.spec.ts create mode 100644 src/app/shared/components/funders-list/funders-list.component.ts rename src/app/{features/metadata/constants/resource-type-options.const.ts => shared/constants/resource-type-general-options.const.ts} (97%) create mode 100644 src/app/shared/pipes/language-label.pipe.ts create mode 100644 src/app/shared/pipes/resource-type-general-label.pipe.ts diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts index a47be2ff6..b1207e692 100644 --- a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts @@ -8,7 +8,7 @@ import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { languageCodes } from '@osf/shared/constants/language.const'; +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; import { resourceTypes } from '@osf/shared/constants/resource-types.const'; import { OsfFileCustomMetadata, PatchFileMetadata } from '../../models'; @@ -22,7 +22,7 @@ import { OsfFileCustomMetadata, PatchFileMetadata } from '../../models'; }) export class EditFileMetadataDialogComponent { readonly resourceTypes = resourceTypes; - readonly languages = languageCodes; + readonly languages = LANGUAGE_CODES; private readonly dialogRef = inject(DynamicDialogRef); readonly config = inject(DynamicDialogConfig); diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts index e278aa6f0..3a3452604 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts @@ -3,7 +3,7 @@ import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { languageCodes } from '@osf/shared/constants/language.const'; +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { provideOSFCore } from '@testing/osf.testing.provider'; @@ -64,7 +64,7 @@ describe('FileMetadataComponent', () => { expect(component.fileMetadata).toBeDefined(); expect(component.isLoading).toBeDefined(); expect(component.hasWriteAccess).toBeDefined(); - expect(component.languageCodes).toBe(languageCodes); + expect(component.languageCodes).toBe(LANGUAGE_CODES); expect(component.metadataFields).toBe(FileMetadataFields); }); diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.ts b/src/app/features/files/components/file-metadata/file-metadata.component.ts index 2b9f22052..3838dc86a 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -12,7 +12,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { languageCodes } from '@osf/shared/constants/language.const'; +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { LanguageCodeModel } from '@shared/models/language-code.model'; @@ -44,7 +44,7 @@ export class FileMetadataComponent { hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); - readonly languageCodes = languageCodes; + readonly languageCodes = LANGUAGE_CODES; readonly fileGuid = toSignal(this.route.params.pipe(map((params) => params['fileGuid']))); diff --git a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.html b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.html index 6f00278b7..9b2e95482 100644 --- a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.html +++ b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.html @@ -26,12 +26,12 @@

{{ 'project.overview.metadata.resourceType' | translate }}: - {{ getResourceTypeName(customItemMetadata()?.resourceTypeGeneral!) }} + {{ customItemMetadata()?.resourceTypeGeneral | resourceTypeGeneralLabel }}

{{ 'project.overview.metadata.resourceLanguage' | translate }}: - {{ getLanguageName(customItemMetadata()?.language || '') }} + {{ customItemMetadata()?.language | languageLabel }}

} @else { diff --git a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.ts b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.ts index b5a9cb0de..4659ab242 100644 --- a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.ts +++ b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.ts @@ -5,14 +5,13 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { RESOURCE_TYPE_OPTIONS } from '@osf/features/metadata/constants'; import { CustomItemMetadataRecord } from '@osf/features/metadata/models'; -import { languageCodes } from '@osf/shared/constants/language.const'; -import { LanguageCodeModel } from '@shared/models/language-code.model'; +import { LanguageLabelPipe } from '@osf/shared/pipes/language-label.pipe'; +import { ResourceTypeGeneralLabelPipe } from '@osf/shared/pipes/resource-type-general-label.pipe'; @Component({ selector: 'osf-metadata-resource-information', - imports: [Button, Card, TranslatePipe], + imports: [Button, Card, TranslatePipe, LanguageLabelPipe, ResourceTypeGeneralLabelPipe], templateUrl: './metadata-resource-information.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -22,17 +21,4 @@ export class MetadataResourceInformationComponent { customItemMetadata = input.required(); readonly = input(false); showResourceInfo = output(); - - readonly languageCodes = languageCodes; - readonly resourceTypes = RESOURCE_TYPE_OPTIONS; - - getLanguageName(languageCode: string): string { - const language = this.languageCodes.find((lang: LanguageCodeModel) => lang.code === languageCode); - return language ? language.name : languageCode; - } - - getResourceTypeName(resourceType: string): string { - const resource = this.resourceTypes.find((res) => res.value === resourceType); - return resource ? resource.label : resourceType; - } } diff --git a/src/app/features/metadata/constants/index.ts b/src/app/features/metadata/constants/index.ts index ea28ffd12..7fd5997d1 100644 --- a/src/app/features/metadata/constants/index.ts +++ b/src/app/features/metadata/constants/index.ts @@ -1,2 +1 @@ export * from './cedar-config.const'; -export * from './resource-type-options.const'; diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html index 22b3da58d..406d870f6 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.html @@ -6,14 +6,14 @@ @@ -24,22 +24,19 @@ - - {{ option.label }} - diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts index 65b895efb..284db1e25 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts @@ -12,6 +12,8 @@ import { ResourceInformationDialogComponent } from './resource-information-dialo describe('ResourceInformationDialogComponent', () => { let component: ResourceInformationDialogComponent; let fixture: ComponentFixture; + let dialogRef: DynamicDialogRef; + let config: DynamicDialogConfig; beforeEach(() => { TestBed.configureTestingModule({ @@ -21,83 +23,66 @@ describe('ResourceInformationDialogComponent', () => { fixture = TestBed.createComponent(ResourceInformationDialogComponent); component = fixture.componentInstance; + dialogRef = TestBed.inject(DynamicDialogRef); + config = TestBed.inject(DynamicDialogConfig); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should have resource type options', () => { - expect(component.resourceTypeOptions).toBeDefined(); - expect(component.resourceTypeOptions.length).toBeGreaterThan(0); - }); - - it('should have language options', () => { - expect(component.languageOptions).toBeDefined(); - expect(component.languageOptions.length).toBeGreaterThan(0); - }); + it('should patch form values on init when metadata is provided', () => { + config.data = { + customItemMetadata: { + resourceTypeGeneral: 'Dataset', + language: 'eng', + }, + }; - it('should not save when form is invalid', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = vi.spyOn(dialogRef, 'close'); + component.ngOnInit(); - component.resourceForm.patchValue({ - resourceType: 'dataset', - resourceLanguage: 'en', + expect(component.resourceForm.getRawValue()).toEqual({ + resourceType: 'Dataset', + resourceLanguage: 'eng', }); + }); - component.save(); + it('should keep default empty values on init when metadata is not provided', () => { + config.data = {}; - expect(closeSpy).toHaveBeenCalledWith({ - resourceTypeGeneral: 'dataset', - language: 'en', + component.ngOnInit(); + + expect(component.resourceForm.getRawValue()).toEqual({ + resourceType: '', + resourceLanguage: '', }); }); - it('should not save when resource type is missing', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = vi.spyOn(dialogRef, 'close'); - - component.resourceForm.patchValue({ - resourceType: '', - resourceLanguage: 'en', + it('should close dialog with mapped payload on save when form is valid', () => { + component.resourceForm.setValue({ + resourceType: 'JournalArticle', + resourceLanguage: 'deu', }); component.save(); - expect(closeSpy).toHaveBeenCalledWith({ - resourceTypeGeneral: '', - language: 'en', + expect(dialogRef.close).toHaveBeenCalledWith({ + resourceTypeGeneral: 'JournalArticle', + language: 'deu', }); }); - it('should cancel dialog', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = vi.spyOn(dialogRef, 'close'); + it('should not close dialog on save when form is invalid', () => { + component.resourceForm.setErrors({ invalid: true }); - component.cancel(); - - expect(closeSpy).toHaveBeenCalled(); - }); - - it('should validate required fields', () => { - const resourceTypeControl = component.resourceForm.get('resourceType'); - - expect(resourceTypeControl?.hasError('required')).toBe(false); - - resourceTypeControl?.setValue('dataset'); + component.save(); - expect(resourceTypeControl?.hasError('required')).toBe(false); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should handle form validation state', () => { - expect(component.resourceForm.valid).toBe(true); - - component.resourceForm.patchValue({ - resourceType: 'dataset', - resourceLanguage: 'en', - }); + it('should close dialog without payload on cancel', () => { + component.cancel(); - expect(component.resourceForm.valid).toBe(true); + expect(dialogRef.close).toHaveBeenCalledWith(); }); }); diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts index 51866076d..63278c15a 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts @@ -7,11 +7,10 @@ import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { languageCodes } from '@osf/shared/constants/language.const'; -import { LanguageCodeModel } from '@shared/models/language-code.model'; +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; +import { RESOURCE_TYPE_GENERAL_OPTIONS } from '@osf/shared/constants/resource-type-general-options.const'; -import { RESOURCE_TYPE_OPTIONS } from '../../constants'; -import { CustomItemMetadataRecord, ResourceInformationForm } from '../../models'; +import { ResourceInformationForm } from '../../models'; @Component({ selector: 'osf-resource-information-dialog', @@ -28,26 +27,12 @@ export class ResourceInformationDialogComponent implements OnInit { resourceLanguage: new FormControl(''), }); - resourceTypeOptions = RESOURCE_TYPE_OPTIONS; - languageOptions = languageCodes.map((lang: LanguageCodeModel) => ({ - label: lang.name, - value: lang.code, - })); - - get customItemMetadata(): CustomItemMetadataRecord | null { - return this.config.data?.customItemMetadata || null; - } - - get isEditMode(): boolean { - return !!this.customItemMetadata; - } - - getResourceTypeName(resourceType: string): string { - return Object.fromEntries(RESOURCE_TYPE_OPTIONS.map((item) => [item.value, item.label]))[resourceType]; - } + resourceTypeOptions = RESOURCE_TYPE_GENERAL_OPTIONS; + languageOptions = LANGUAGE_CODES; ngOnInit(): void { - const metadata = this.customItemMetadata; + const metadata = this.config.data?.customItemMetadata; + if (metadata) { this.resourceForm.patchValue({ resourceType: metadata.resourceTypeGeneral || '', diff --git a/src/app/features/metadata/store/metadata.selectors.ts b/src/app/features/metadata/store/metadata.selectors.ts index 7ceca5945..a79fa0a61 100644 --- a/src/app/features/metadata/store/metadata.selectors.ts +++ b/src/app/features/metadata/store/metadata.selectors.ts @@ -16,6 +16,11 @@ export class MetadataSelectors { return state.customMetadata?.data ?? null; } + @Selector([MetadataState]) + static isCustomItemMetadataLoading(state: MetadataStateModel) { + return state.customMetadata?.isLoading ?? false; + } + @Selector([MetadataState]) static getLoading(state: MetadataStateModel) { return state.metadata?.isLoading || state.customMetadata?.isLoading || false; diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html index fc2e0539f..26d233842 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html @@ -44,18 +44,6 @@

{{ 'preprints.details.supplementalMaterials' | translate }}

} - @if (preprintProviderValue?.assertionsEnabled) { -
-

{{ 'preprints.preprintStepper.review.sections.authorAssertions.conflictOfInterest' | translate }}

- - @if (preprintValue.hasCoi) { - {{ preprintValue.coiStatement }} - } @else { -

{{ 'preprints.preprintStepper.review.sections.authorAssertions.noCoi' | translate }}

- } -
- } - {{ 'common.labels.contributors' | translate }}

+
+

{{ 'project.overview.metadata.resourceType' | translate }}

+ +

+ {{ + customItemMetadata()?.resourceTypeGeneral + ? (customItemMetadata()?.resourceTypeGeneral | resourceTypeGeneralLabel) + : ('project.overview.metadata.noResourceType' | translate) + }} +

+
+

{{ 'common.labels.description' | translate }}

@@ -37,15 +49,6 @@

{{ 'common.labels.description' | translate }}

/>
-
-

{{ 'project.overview.metadata.supplements' | translate }}

- - -
-

{{ 'project.overview.metadata.dateCreated' | translate }}

@@ -59,18 +62,16 @@

{{ 'project.overview.metadata.dateUpdated' | translate }}

-

{{ 'common.labels.license' | translate }}

+

{{ 'common.labels.subjects' | translate }}

- +
- @if (!isAnonymous()) { -
-

{{ 'project.overview.metadata.projectDOI' | translate }}

+
+

{{ 'shared.tags.title' | translate }}

- -
- } + +
@if (!isAnonymous()) {
@@ -86,17 +87,46 @@

{{ 'common.labels.affiliatedInstitutions' | translate }}

>
-

{{ 'common.labels.subjects' | translate }}

+

{{ 'project.overview.metadata.supplements' | translate }}

- +
-

{{ 'shared.tags.title' | translate }}

+

{{ 'project.overview.metadata.funderNames' | translate }}

- + +
+ +
+

{{ 'common.labels.language' | translate }}

+ +

+ {{ + customItemMetadata()?.language + ? (customItemMetadata()?.language | languageLabel) + : ('project.overview.metadata.noLanguage' | translate) + }} +

+
+ +
+

{{ 'common.labels.license' | translate }}

+ +
+ @if (!isAnonymous()) { +
+

{{ 'project.overview.metadata.projectDOI' | translate }}

+ + +
+ } + @if (!isAnonymous()) { { ResourceLicenseComponent, SubjectsListComponent, TagsListComponent, - OverviewSupplementsComponent + OverviewSupplementsComponent, + FundersListComponent ), ], providers: [ @@ -94,6 +98,8 @@ describe('ProjectOverviewMetadataComponent', () => { { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false }, { selector: CollectionsSelectors.getCurrentProjectSubmissions, value: [] }, { selector: CollectionsSelectors.getCurrentProjectSubmissionsLoading, value: false }, + { selector: MetadataSelectors.getCustomItemMetadata, value: null }, + { selector: MetadataSelectors.isCustomItemMetadataLoading, value: false }, ], }), ], @@ -121,6 +127,7 @@ describe('ProjectOverviewMetadataComponent', () => { expect(dispatchMock).toHaveBeenCalledWith(new GetProjectPreprints('project-1')); expect(dispatchMock).toHaveBeenCalledWith(new FetchSelectedSubjects('project-1', ResourceType.Project)); expect(dispatchMock).toHaveBeenCalledWith(new GetProjectSubmissions('project-1')); + expect(dispatchMock).toHaveBeenCalledWith(new GetCustomItemMetadata('project-1')); expect(dispatchMock).toHaveBeenCalledWith(new GetProjectLicense(MOCK_PROJECT_OVERVIEW.licenseId)); }); diff --git a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.ts b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.ts index 40e0507ae..bb5d2571c 100644 --- a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.ts +++ b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.ts @@ -8,8 +8,10 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; +import { GetCustomItemMetadata, MetadataSelectors } from '@osf/features/metadata/store'; import { AffiliatedInstitutionsViewComponent } from '@osf/shared/components/affiliated-institutions-view/affiliated-institutions-view.component'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; +import { FundersListComponent } from '@osf/shared/components/funders-list/funders-list.component'; import { ResourceCitationsComponent } from '@osf/shared/components/resource-citations/resource-citations.component'; import { ResourceDoiComponent } from '@osf/shared/components/resource-doi/resource-doi.component'; import { ResourceLicenseComponent } from '@osf/shared/components/resource-license/resource-license.component'; @@ -17,6 +19,8 @@ import { SubjectsListComponent } from '@osf/shared/components/subjects-list/subj import { TagsListComponent } from '@osf/shared/components/tags-list/tags-list.component'; import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { LanguageLabelPipe } from '@osf/shared/pipes/language-label.pipe'; +import { ResourceTypeGeneralLabelPipe } from '@osf/shared/pipes/resource-type-general-label.pipe'; import { CollectionsSelectors, GetProjectSubmissions } from '@osf/shared/stores/collections'; import { ContributorsSelectors, @@ -48,11 +52,14 @@ import { OverviewSupplementsComponent } from '../overview-supplements/overview-s OverviewCollectionsComponent, AffiliatedInstitutionsViewComponent, ContributorsListComponent, + FundersListComponent, ResourceDoiComponent, ResourceLicenseComponent, SubjectsListComponent, TagsListComponent, OverviewSupplementsComponent, + LanguageLabelPipe, + ResourceTypeGeneralLabelPipe, ], templateUrl: './project-overview-metadata.component.html', styleUrl: './project-overview-metadata.component.scss', @@ -64,6 +71,8 @@ export class ProjectOverviewMetadataComponent { readonly currentProject = select(ProjectOverviewSelectors.getProject); readonly isAnonymous = select(ProjectOverviewSelectors.isProjectAnonymous); readonly canEdit = select(ProjectOverviewSelectors.hasWriteAccess); + readonly customItemMetadata = select(MetadataSelectors.getCustomItemMetadata); + readonly isCustomItemMetadataLoading = select(MetadataSelectors.isCustomItemMetadataLoading); readonly institutions = select(ProjectOverviewSelectors.getInstitutions); readonly isInstitutionsLoading = select(ProjectOverviewSelectors.isInstitutionsLoading); readonly identifiers = select(ProjectOverviewSelectors.getIdentifiers); @@ -91,6 +100,7 @@ export class ProjectOverviewMetadataComponent { setCustomCitation: SetProjectCustomCitation, getSubjects: FetchSelectedSubjects, getProjectSubmissions: GetProjectSubmissions, + getCustomItemMetadata: GetCustomItemMetadata, getBibliographicContributors: GetBibliographicContributors, loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); @@ -107,6 +117,7 @@ export class ProjectOverviewMetadataComponent { this.actions.getSubjects(project.id, ResourceType.Project); this.actions.getProjectSubmissions(project.id); this.actions.getLicense(project.licenseId); + this.actions.getCustomItemMetadata(project.id); } }); } diff --git a/src/app/features/registries/components/new-registration/new-registration.component.scss b/src/app/features/registries/components/new-registration/new-registration.component.scss index f22ca8981..ab9f13057 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.scss +++ b/src/app/features/registries/components/new-registration/new-registration.component.scss @@ -3,4 +3,5 @@ flex-direction: column; flex: 1; background-color: var(--white); + height: 100%; } diff --git a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.html b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.html index 6e9a32ebc..b28e15dc8 100644 --- a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.html +++ b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.html @@ -30,6 +30,18 @@

{{ 'common.labels.contributors' | translate }}

+
+

{{ 'project.overview.metadata.resourceType' | translate }}

+ +

+ {{ + customItemMetadata()?.resourceTypeGeneral + ? (customItemMetadata()?.resourceTypeGeneral | resourceTypeGeneralLabel) + : ('project.overview.metadata.noResourceType' | translate) + }} +

+
+

{{ 'common.labels.description' | translate }}

@@ -50,6 +62,68 @@

{{ 'registry.overview.metadata.registry' | translate }}

{{ registryProvider()?.name }}

+
+
+

{{ 'project.overview.metadata.dateCreated' | translate }}

+

{{ resource.dateCreated | date: dateFormat }}

+
+ +
+

{{ 'registry.overview.metadata.registeredDate' | translate }}

+

{{ resource.dateRegistered | date: dateFormat }}

+
+
+ +
+

{{ 'common.labels.subjects' | translate }}

+ + +
+ +
+

{{ 'shared.tags.title' | translate }}

+ + +
+ + @if (!isAnonymous()) { +
+

{{ 'common.labels.affiliatedInstitutions' | translate }}

+ + +
+ } + +
+

{{ 'project.overview.metadata.funderNames' | translate }}

+ + +
+ +
+

{{ 'common.labels.language' | translate }}

+ +

+ {{ + customItemMetadata()?.language + ? (customItemMetadata()?.language | languageLabel) + : ('project.overview.metadata.noLanguage' | translate) + }} +

+
+ @if (resource.associatedProjectId) {

{{ 'registry.overview.metadata.associatedProject' | translate }}

@@ -64,17 +138,13 @@

{{ 'registry.overview.metadata.associatedProject' | translate }}

} -
+ @if (resource.iaUrl) {
-

{{ 'project.overview.metadata.dateCreated' | translate }}

-

{{ resource.dateCreated | date: dateFormat }}

-
+

{{ 'project.overview.metadata.internetArchiveLink' | translate }}

-
-

{{ 'registry.overview.metadata.registeredDate' | translate }}

-

{{ resource.dateRegistered | date: dateFormat }}

+
{{ resource.iaUrl }}
-
+ }

{{ 'common.labels.license' | translate }}

@@ -86,14 +156,6 @@

{{ 'common.labels.license' | translate }}

>
- @if (resource.iaUrl) { -
-

{{ 'project.overview.metadata.internetArchiveLink' | translate }}

- -
{{ resource.iaUrl }}
-
- } - @if (!isAnonymous()) {

{{ 'registry.overview.metadata.doi' | translate }}

@@ -106,38 +168,6 @@

{{ 'registry.overview.metadata.doi' | translate }}

} - @if (!isAnonymous()) { -
-

{{ 'common.labels.affiliatedInstitutions' | translate }}

- - -
- } - -
-

{{ 'common.labels.subjects' | translate }}

- - -
- -
-

{{ 'shared.tags.title' | translate }}

- - -
- @if (!isAnonymous()) { { expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchSelectedSubjects)); expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetRegistryLicense)); expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetRegistryIdentifiers)); + expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetCustomItemMetadata)); }); it('should not dispatch init actions when registry is null', () => { diff --git a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.ts b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.ts index 0fa1ff01a..38a43b958 100644 --- a/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.ts +++ b/src/app/features/registry/components/registry-overview-metadata/registry-overview-metadata.component.ts @@ -9,8 +9,10 @@ import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/cor import { Router, RouterLink } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { GetCustomItemMetadata, MetadataSelectors } from '@osf/features/metadata/store'; import { AffiliatedInstitutionsViewComponent } from '@osf/shared/components/affiliated-institutions-view/affiliated-institutions-view.component'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; +import { FundersListComponent } from '@osf/shared/components/funders-list/funders-list.component'; import { ResourceCitationsComponent } from '@osf/shared/components/resource-citations/resource-citations.component'; import { ResourceDoiComponent } from '@osf/shared/components/resource-doi/resource-doi.component'; import { ResourceLicenseComponent } from '@osf/shared/components/resource-license/resource-license.component'; @@ -18,6 +20,8 @@ import { SubjectsListComponent } from '@osf/shared/components/subjects-list/subj import { TagsListComponent } from '@osf/shared/components/tags-list/tags-list.component'; import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { LanguageLabelPipe } from '@osf/shared/pipes/language-label.pipe'; +import { ResourceTypeGeneralLabelPipe } from '@osf/shared/pipes/resource-type-general-label.pipe'; import { ContributorsSelectors, LoadMoreBibliographicContributors } from '@osf/shared/stores/contributors'; import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider'; import { FetchSelectedSubjects, SubjectsSelectors } from '@osf/shared/stores/subjects'; @@ -43,8 +47,11 @@ import { ResourceLicenseComponent, AffiliatedInstitutionsViewComponent, ContributorsListComponent, + FundersListComponent, SubjectsListComponent, TagsListComponent, + LanguageLabelPipe, + ResourceTypeGeneralLabelPipe, ], templateUrl: './registry-overview-metadata.component.html', styleUrl: './registry-overview-metadata.component.scss', @@ -57,6 +64,8 @@ export class RegistryOverviewMetadataComponent { readonly registry = select(RegistrySelectors.getRegistry); readonly registryProvider = select(RegistrationProviderSelectors.getBrandedProvider); readonly isAnonymous = select(RegistrySelectors.isRegistryAnonymous); + readonly customItemMetadata = select(MetadataSelectors.getCustomItemMetadata); + readonly isCustomItemMetadataLoading = select(MetadataSelectors.isCustomItemMetadataLoading); canEdit = select(RegistrySelectors.hasWriteAccess); license = select(RegistrySelectors.getLicense); @@ -81,6 +90,7 @@ export class RegistryOverviewMetadataComponent { getInstitutions: GetRegistryInstitutions, getIdentifiers: GetRegistryIdentifiers, getLicense: GetRegistryLicense, + getCustomItemMetadata: GetCustomItemMetadata, setCustomCitation: SetRegistryCustomCitation, loadMoreBibliographicContributors: LoadMoreBibliographicContributors, }); @@ -94,6 +104,7 @@ export class RegistryOverviewMetadataComponent { this.actions.getSubjects(registry.id, ResourceType.Registration); this.actions.getLicense(registry.licenseId); this.actions.getIdentifiers(registry.id); + this.actions.getCustomItemMetadata(registry.id); } }); } diff --git a/src/app/shared/components/funders-list/funders-list.component.html b/src/app/shared/components/funders-list/funders-list.component.html new file mode 100644 index 000000000..dda761cff --- /dev/null +++ b/src/app/shared/components/funders-list/funders-list.component.html @@ -0,0 +1,15 @@ +@if (isLoading()) { +
+ +
+} @else { + @if (funders()?.length) { + + } @else { +

{{ 'project.overview.metadata.noInformation' | translate }}

+ } +} diff --git a/src/app/shared/components/funders-list/funders-list.component.spec.ts b/src/app/shared/components/funders-list/funders-list.component.spec.ts new file mode 100644 index 000000000..7c78fecbe --- /dev/null +++ b/src/app/shared/components/funders-list/funders-list.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; + +import { FundersListComponent } from './funders-list.component'; + +describe('FundersListComponent', () => { + let component: FundersListComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FundersListComponent], + providers: [provideOSFCore()], + }); + + fixture = TestBed.createComponent(FundersListComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default input values', () => { + expect(component.funders()).toBeUndefined(); + expect(component.isLoading()).toBe(false); + }); +}); diff --git a/src/app/shared/components/funders-list/funders-list.component.ts b/src/app/shared/components/funders-list/funders-list.component.ts new file mode 100644 index 000000000..30f80090e --- /dev/null +++ b/src/app/shared/components/funders-list/funders-list.component.ts @@ -0,0 +1,18 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { Funder } from '@osf/features/metadata/models'; + +@Component({ + selector: 'osf-funders-list', + imports: [Skeleton, TranslatePipe], + templateUrl: './funders-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FundersListComponent { + funders = input(undefined); + isLoading = input(false); +} diff --git a/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.ts b/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.ts index 7d3c7a568..8e62aab94 100644 --- a/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.ts +++ b/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.ts @@ -2,7 +2,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { languageCodes } from '@osf/shared/constants/language.const'; +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; import { ResourceModel } from '@shared/models/search/resource.model'; @Component({ @@ -19,6 +19,6 @@ export class ProjectSecondaryMetadataComponent { const resourceLanguage = this.resource().language; if (!resourceLanguage) return null; - return languageCodes.find((lang) => lang.code === resourceLanguage)?.name; + return LANGUAGE_CODES.find((lang) => lang.code === resourceLanguage)?.name; }); } diff --git a/src/app/shared/constants/language.const.ts b/src/app/shared/constants/language.const.ts index 30bc2c133..dc882a0df 100644 --- a/src/app/shared/constants/language.const.ts +++ b/src/app/shared/constants/language.const.ts @@ -1,4 +1,4 @@ -export const languageCodes = [ +export const LANGUAGE_CODES = [ { code: 'abk', name: 'Abkhazian', diff --git a/src/app/features/metadata/constants/resource-type-options.const.ts b/src/app/shared/constants/resource-type-general-options.const.ts similarity index 97% rename from src/app/features/metadata/constants/resource-type-options.const.ts rename to src/app/shared/constants/resource-type-general-options.const.ts index e9919c8a2..1d280087e 100644 --- a/src/app/features/metadata/constants/resource-type-options.const.ts +++ b/src/app/shared/constants/resource-type-general-options.const.ts @@ -1,6 +1,6 @@ // `value` must be from resourceTypeGeneral controlled vocab in https://schema.datacite.org/meta/kernel-4/ // see https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/resourceTypeGeneral/ -export const RESOURCE_TYPE_OPTIONS = [ +export const RESOURCE_TYPE_GENERAL_OPTIONS = [ { label: 'Audiovisual', value: 'Audiovisual' }, { label: 'Book', value: 'Book' }, { label: 'Book Chapter', value: 'BookChapter' }, diff --git a/src/app/shared/pipes/language-label.pipe.ts b/src/app/shared/pipes/language-label.pipe.ts new file mode 100644 index 000000000..405e2d0e7 --- /dev/null +++ b/src/app/shared/pipes/language-label.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { LANGUAGE_CODES } from '@osf/shared/constants/language.const'; + +const languageLabelByCode = new Map(LANGUAGE_CODES.map((item) => [item.code, item.name])); + +@Pipe({ + name: 'languageLabel', +}) +export class LanguageLabelPipe implements PipeTransform { + transform(value: string | null | undefined): string { + if (!value) return ''; + return languageLabelByCode.get(value) ?? value; + } +} diff --git a/src/app/shared/pipes/resource-type-general-label.pipe.ts b/src/app/shared/pipes/resource-type-general-label.pipe.ts new file mode 100644 index 000000000..dc8b2ea0b --- /dev/null +++ b/src/app/shared/pipes/resource-type-general-label.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { RESOURCE_TYPE_GENERAL_OPTIONS } from '@osf/shared/constants/resource-type-general-options.const'; + +const resourceTypeGeneralLabelByValue = new Map(RESOURCE_TYPE_GENERAL_OPTIONS.map((item) => [item.value, item.label])); + +@Pipe({ + name: 'resourceTypeGeneralLabel', +}) +export class ResourceTypeGeneralLabelPipe implements PipeTransform { + transform(value: string | null | undefined): string { + if (!value) return ''; + return resourceTypeGeneralLabelByValue.get(value) ?? value; + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2887a045d..92ed47482 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -511,6 +511,7 @@ "emailPlaceholder": "email@example.com", "forked": "Forked", "lastUpdated": "Last Updated", + "language": "Language", "learnMore": "Learn More", "license": "License", "makePublic": "Make Public", @@ -1984,6 +1985,7 @@ "customCitationPlaceholder": "Enter custom citation", "dateCreated": "Date Created", "dateUpdated": "Date Updated", + "funderNames": "Funder Names", "fundingSupport": "Funding/Support Information", "getMoreCitations": "Get More Citations", "internetArchiveLink": "internet archive link", @@ -1992,7 +1994,9 @@ "noInformation": "No information", "noProjectDoi": "No Project DOI", "noPublicationDoi": "No Publication DOI", + "noLanguage": "No language", "noResourceInformation": "No resource information available", + "noResourceType": "No resource type", "noSupplements": "No supplements", "noTags": "No tags", "placeholders": { From b616ba7940768e2846a0f6f60e5dc0b060be5433 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 5 May 2026 11:53:14 +0300 Subject: [PATCH 2/3] fix(funders-list): added more tests --- .../funders-list.component.spec.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/funders-list/funders-list.component.spec.ts b/src/app/shared/components/funders-list/funders-list.component.spec.ts index 7c78fecbe..49ea9362d 100644 --- a/src/app/shared/components/funders-list/funders-list.component.spec.ts +++ b/src/app/shared/components/funders-list/funders-list.component.spec.ts @@ -1,5 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Funder } from '@osf/features/metadata/models'; + import { provideOSFCore } from '@testing/osf.testing.provider'; import { FundersListComponent } from './funders-list.component'; @@ -8,6 +10,25 @@ describe('FundersListComponent', () => { let component: FundersListComponent; let fixture: ComponentFixture; + const fundersMock: Funder[] = [ + { + funderName: 'National Science Foundation', + funderIdentifier: 'https://ror.org/021nxhr62', + funderIdentifierType: 'ROR', + awardNumber: 'NSF-123', + awardUri: 'https://example.org/nsf-123', + awardTitle: 'Grant 123', + }, + { + funderName: 'National Institutes of Health', + funderIdentifier: 'https://ror.org/04zaypm56', + funderIdentifierType: 'ROR', + awardNumber: 'NIH-456', + awardUri: 'https://example.org/nih-456', + awardTitle: 'Grant 456', + }, + ]; + beforeEach(() => { TestBed.configureTestingModule({ imports: [FundersListComponent], @@ -22,8 +43,32 @@ describe('FundersListComponent', () => { expect(component).toBeTruthy(); }); - it('should have default input values', () => { - expect(component.funders()).toBeUndefined(); - expect(component.isLoading()).toBe(false); + it('should render loading skeleton', () => { + fixture.componentRef.setInput('isLoading', true); + fixture.componentRef.setInput('funders', fundersMock); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('p-skeleton')).not.toBeNull(); + expect(fixture.nativeElement.textContent).not.toContain('National Science Foundation'); + expect(fixture.nativeElement.textContent).not.toContain('project.overview.metadata.noInformation'); + }); + + it('should render funder names', () => { + fixture.componentRef.setInput('funders', fundersMock); + fixture.detectChanges(); + + const funderItems = fixture.nativeElement.querySelectorAll('li'); + + expect(funderItems).toHaveLength(2); + expect(fixture.nativeElement.textContent).toContain('National Science Foundation'); + expect(fixture.nativeElement.textContent).toContain('National Institutes of Health'); + }); + + it.each([undefined, []])('should render no information message when funders are %s', (funders) => { + fixture.componentRef.setInput('funders', funders); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ul')).toBeNull(); + expect(fixture.nativeElement.textContent).toContain('project.overview.metadata.noInformation'); }); }); From df398accbacefc02b7dee2d26aa674a659763266 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 5 May 2026 11:55:45 +0300 Subject: [PATCH 3/3] fix(resource-information): fixed comments --- package-lock.json | 4 ++-- .../resource-information-dialog.component.spec.ts | 8 -------- .../resource-information-dialog.component.ts | 12 +++++------- .../project-overview-metadata.component.html | 12 +++++++----- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfe544253..ac558ab70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "osf", - "version": "26.7.0", + "version": "26.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "osf", - "version": "26.7.0", + "version": "26.9.1", "dependencies": { "@angular/animations": "^21.2.7", "@angular/cdk": "^21.2.6", diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts index 284db1e25..5279b776a 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts @@ -72,14 +72,6 @@ describe('ResourceInformationDialogComponent', () => { }); }); - it('should not close dialog on save when form is invalid', () => { - component.resourceForm.setErrors({ invalid: true }); - - component.save(); - - expect(dialogRef.close).not.toHaveBeenCalled(); - }); - it('should close dialog without payload on cancel', () => { component.cancel(); diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts index 63278c15a..1a9acf977 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.ts @@ -42,13 +42,11 @@ export class ResourceInformationDialogComponent implements OnInit { } save(): void { - if (this.resourceForm.valid) { - const formValue = this.resourceForm.getRawValue(); - this.dialogRef.close({ - resourceTypeGeneral: formValue.resourceType, - language: formValue.resourceLanguage, - }); - } + const formValue = this.resourceForm.getRawValue(); + this.dialogRef.close({ + resourceTypeGeneral: formValue.resourceType, + language: formValue.resourceLanguage, + }); } cancel(): void { diff --git a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.html b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.html index c23d7fb78..dcb6c35d8 100644 --- a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.html +++ b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.html @@ -1,6 +1,8 @@ @let resource = currentProject(); @if (resource) { + @let customMetadata = customItemMetadata(); +