From dc90c66ff2fa4dfcfbbb4ef4ffcf6928e2850973 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 12 Jun 2026 14:53:55 +0200 Subject: [PATCH] fix(ENG-11358): show cedar metadata in collection accordion on project overview --- .../overview-collections.component.html | 30 ++++++-- .../overview-collections.component.spec.ts | 73 +++++++++++++++++++ .../overview-collections.component.ts | 33 ++++++++- .../project-overview-metadata.component.html | 3 + ...roject-overview-metadata.component.spec.ts | 7 ++ .../project-overview-metadata.component.ts | 14 +++- 6 files changed, 150 insertions(+), 10 deletions(-) diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.html b/src/app/features/project/overview/components/overview-collections/overview-collections.component.html index 5a18e2d72..2b7e65d86 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.html +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.html @@ -29,16 +29,30 @@

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

- @let attributes = getSubmissionAttributes(submission); + @let templateId = submission.requiredMetadataTemplateId ?? ''; + @let cedarRecord = cedarRecordByTemplateId().get(templateId) ?? null; + @let cedarTemplate = cedarTemplateById().get(templateId) ?? null; - @if (attributes.length) { -
- @for (attribute of attributes; track attribute.key) { -

- {{ attribute.label }}: {{ attribute.value }} -

- } + @if (isCedarMode() && cedarRecord && cedarTemplate?.attributes?.template) { +
+
+ } @else { + @let attributes = getSubmissionAttributes(submission); + + @if (attributes.length) { +
+ @for (attribute of attributes; track attribute.key) { +

+ {{ attribute.label }}: {{ attribute.value }} +

+ } +
+ } } diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts index 020aee7b6..c279a7aef 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts @@ -2,8 +2,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; import { collectionFilterNames } from '@osf/features/collections/constants'; +import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock'; +import { MOCK_CEDAR_METADATA_RECORD_DATA } from '@testing/mocks/cedar-metadata-record.mock'; import { MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, MOCK_COLLECTION_SUBMISSION_SINGLE_FILTER, @@ -92,4 +95,74 @@ describe('OverviewCollectionsComponent', () => { expect(statusAttr?.value).toBe('1'); expect(typeof statusAttr?.value).toBe('string'); }); + + it('should render cedar-artifact-viewer when isCedarMode is true with matching record and template', async () => { + const cedarSubmission: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_EMPTY_FILTERS, + requiredMetadataTemplateId: 'template-1', + }; + const cedarRecord: CedarMetadataRecordData = MOCK_CEDAR_METADATA_RECORD_DATA; + const cedarTemplate: CedarMetadataDataTemplateJsonApi = + CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK as CedarMetadataDataTemplateJsonApi; + + fixture.componentRef.setInput('projectSubmissions', [cedarSubmission]); + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecords', [cedarRecord]); + fixture.componentRef.setInput('cedarTemplates', [cedarTemplate]); + fixture.detectChanges(); + await fixture.whenStable(); + + const viewer = fixture.nativeElement.querySelector('cedar-artifact-viewer'); + expect(viewer).toBeTruthy(); + }); + + it('should not render cedar-artifact-viewer when isCedarMode is false', async () => { + const cedarSubmission: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_WITH_FILTERS, + requiredMetadataTemplateId: 'template-1', + }; + const cedarRecord: CedarMetadataRecordData = MOCK_CEDAR_METADATA_RECORD_DATA; + const cedarTemplate: CedarMetadataDataTemplateJsonApi = + CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK as CedarMetadataDataTemplateJsonApi; + + fixture.componentRef.setInput('projectSubmissions', [cedarSubmission]); + fixture.componentRef.setInput('isCedarMode', false); + fixture.componentRef.setInput('cedarRecords', [cedarRecord]); + fixture.componentRef.setInput('cedarTemplates', [cedarTemplate]); + fixture.detectChanges(); + await fixture.whenStable(); + + const viewer = fixture.nativeElement.querySelector('cedar-artifact-viewer'); + expect(viewer).toBeNull(); + }); + + it('should show traditional attributes when isCedarMode is true but no matching record', async () => { + const cedarSubmission: CollectionSubmission = { + ...MOCK_COLLECTION_SUBMISSION_WITH_FILTERS, + requiredMetadataTemplateId: 'non-existent-template', + }; + + fixture.componentRef.setInput('projectSubmissions', [cedarSubmission]); + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecords', []); + fixture.componentRef.setInput('cedarTemplates', []); + fixture.detectChanges(); + await fixture.whenStable(); + + const viewer = fixture.nativeElement.querySelector('cedar-artifact-viewer'); + expect(viewer).toBeNull(); + expect(component.getSubmissionAttributes(cedarSubmission).length).toBeGreaterThan(0); + }); + + it('should compute empty cedarRecordByTemplateId map when cedarRecords is null', () => { + fixture.componentRef.setInput('cedarRecords', null); + fixture.detectChanges(); + expect(component.cedarRecordByTemplateId().size).toBe(0); + }); + + it('should compute empty cedarTemplateById map when cedarTemplates is null', () => { + fixture.componentRef.setInput('cedarTemplates', null); + fixture.detectChanges(); + expect(component.cedarTemplateById().size).toBe(0); + }); }); diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts index 168a3530b..a9c51aa97 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.ts @@ -5,10 +5,19 @@ import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; import { Tag } from 'primeng/tag'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + CUSTOM_ELEMENTS_SCHEMA, + input, + ViewEncapsulation, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { collectionFilterNames } from '@osf/features/collections/constants'; +import { CEDAR_VIEWER_CONFIG } from '@osf/features/metadata/constants'; +import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; import { KeyValueModel } from '@osf/shared/models/common/key-value.model'; @@ -32,10 +41,32 @@ import { CollectionStatusSeverityPipe } from '@osf/shared/pipes/collection-statu templateUrl: './overview-collections.component.html', styleUrl: './overview-collections.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + encapsulation: ViewEncapsulation.None, }) export class OverviewCollectionsComponent { projectSubmissions = input(null); isProjectSubmissionsLoading = input(false); + isCedarMode = input(false); + cedarRecords = input(null); + cedarTemplates = input(null); + + cedarViewerConfig = CEDAR_VIEWER_CONFIG; + + cedarRecordByTemplateId = computed(() => { + const records = this.cedarRecords(); + return new Map( + records?.flatMap((record) => { + const templateId = record.relationships?.template?.data?.id; + return templateId ? [[templateId, record] as const] : []; + }) ?? [] + ); + }); + + cedarTemplateById = computed(() => { + const templates = this.cedarTemplates(); + return new Map(templates?.map((t) => [t.id, t] as const) ?? []); + }); getSubmissionAttributes(submission: CollectionSubmission): KeyValueModel[] { const attributes: KeyValueModel[] = []; 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 8c9b34ff6..e918fdafe 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 @@ -83,6 +83,9 @@

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

diff --git a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts index f896c2108..13202745e 100644 --- a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts +++ b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts @@ -7,6 +7,8 @@ import { Mock } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; +import { UserSelectors } from '@core/store/user'; +import { GetCedarMetadataRecords, GetCedarMetadataTemplates, 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 { ResourceCitationsComponent } from '@osf/shared/components/resource-citations/resource-citations.component'; @@ -94,6 +96,9 @@ describe('ProjectOverviewMetadataComponent', () => { { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false }, { selector: CollectionsSelectors.getCurrentProjectSubmissions, value: [] }, { selector: CollectionsSelectors.getCurrentProjectSubmissionsLoading, value: false }, + { selector: UserSelectors.getActiveFlags, value: [] }, + { selector: MetadataSelectors.getCedarRecords, value: [] }, + { selector: MetadataSelectors.getCedarTemplates, value: null }, ], }), ], @@ -122,6 +127,8 @@ describe('ProjectOverviewMetadataComponent', () => { expect(dispatchMock).toHaveBeenCalledWith(new FetchSelectedSubjects('project-1', ResourceType.Project)); expect(dispatchMock).toHaveBeenCalledWith(new GetProjectSubmissions('project-1')); expect(dispatchMock).toHaveBeenCalledWith(new GetProjectLicense(MOCK_PROJECT_OVERVIEW.licenseId)); + expect(dispatchMock).toHaveBeenCalledWith(new GetCedarMetadataRecords('project-1', ResourceType.Project)); + expect(dispatchMock).toHaveBeenCalledWith(new GetCedarMetadataTemplates()); }); it('should not dispatch init actions when project is null', () => { 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..55b3e571a 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 @@ -5,9 +5,11 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core'; import { Router, RouterLink } from '@angular/router'; +import { UserSelectors } from '@core/store/user'; +import { GetCedarMetadataRecords, GetCedarMetadataTemplates, 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 { ResourceCitationsComponent } from '@osf/shared/components/resource-citations/resource-citations.component'; @@ -24,6 +26,7 @@ import { LoadMoreBibliographicContributors, } from '@osf/shared/stores/contributors'; import { FetchSelectedSubjects, SubjectsSelectors } from '@osf/shared/stores/subjects'; +import { COLLECTION_SUBMISSION_WITH_CEDAR } from '@shared/constants/feature-flags.const'; import { GetProjectIdentifiers, @@ -79,6 +82,11 @@ export class ProjectOverviewMetadataComponent { readonly hasMoreBibliographicContributors = select(ContributorsSelectors.hasMoreBibliographicContributors); readonly projectSubmissions = select(CollectionsSelectors.getCurrentProjectSubmissions); readonly isProjectSubmissionsLoading = select(CollectionsSelectors.getCurrentProjectSubmissionsLoading); + readonly activeFlags = select(UserSelectors.getActiveFlags); + readonly cedarRecords = select(MetadataSelectors.getCedarRecords); + private readonly cedarTemplatesResponse = select(MetadataSelectors.getCedarTemplates); + readonly cedarTemplates = computed(() => this.cedarTemplatesResponse()?.data ?? null); + readonly isCedarMode = computed(() => this.activeFlags().includes(COLLECTION_SUBMISSION_WITH_CEDAR)); readonly resourceType = CurrentResourceType.Projects; readonly dateFormat = 'MMM d, y, h:mm a'; @@ -93,6 +101,8 @@ export class ProjectOverviewMetadataComponent { getProjectSubmissions: GetProjectSubmissions, getBibliographicContributors: GetBibliographicContributors, loadMoreBibliographicContributors: LoadMoreBibliographicContributors, + getCedarRecords: GetCedarMetadataRecords, + getCedarTemplates: GetCedarMetadataTemplates, }); constructor() { @@ -107,6 +117,8 @@ export class ProjectOverviewMetadataComponent { this.actions.getSubjects(project.id, ResourceType.Project); this.actions.getProjectSubmissions(project.id); this.actions.getLicense(project.licenseId); + this.actions.getCedarRecords(project.id, ResourceType.Project); + this.actions.getCedarTemplates(); } }); }