-
-
+ @if (isCedarMode()) {
+ @if (cedarTemplate()) {
+
+
+
+
+ } @else {
+
{{ 'collections.addToCollection.cedarFormNotAvailable' | translate }}
}
-
+ } @else {
+
-
+
+ }
diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts
index 96c1f74cd..f6dc67b64 100644
--- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts
+++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts
@@ -6,18 +6,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { AddToCollectionSteps } from '@osf/features/collections/enums';
+import { AddToCollectionSelectors } from '@osf/features/collections/store/add-to-collection';
+import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models';
import { CollectionsSelectors } from '@shared/stores/collections';
+import { MOCK_CEDAR_TEMPLATE } from '@testing/data/collections/cedar-metadata.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { provideMockStore } from '@testing/providers/store-provider.mock';
import { CollectionMetadataStepComponent } from './collection-metadata-step.component';
-describe.skip('CollectionMetadataStepComponent', () => {
+describe('CollectionMetadataStepComponent', () => {
let component: CollectionMetadataStepComponent;
let fixture: ComponentFixture
;
- beforeEach(() => {
+ function setup(isCedarMode = false, cedarTemplate: CedarMetadataDataTemplateJsonApi | null = null) {
TestBed.configureTestingModule({
imports: [CollectionMetadataStepComponent, MockComponents(StepPanel, Step, StepItem)],
providers: [
@@ -27,6 +30,7 @@ describe.skip('CollectionMetadataStepComponent', () => {
{ selector: CollectionsSelectors.getCollectionProvider, value: null },
{ selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
{ selector: CollectionsSelectors.getAllFiltersOptions, value: {} },
+ { selector: AddToCollectionSelectors.getCurrentCollectionSubmission, value: null },
],
}),
],
@@ -39,8 +43,16 @@ describe.skip('CollectionMetadataStepComponent', () => {
fixture.componentRef.setInput('targetStepValue', 1);
fixture.componentRef.setInput('isDisabled', false);
fixture.componentRef.setInput('primaryCollectionId', 'test-collection-id');
+ fixture.componentRef.setInput('isCedarMode', isCedarMode);
+ if (cedarTemplate) {
+ fixture.componentRef.setInput('cedarTemplate', cedarTemplate);
+ }
fixture.detectChanges();
+ }
+
+ beforeEach(() => {
+ setup();
});
it('should create', () => {
@@ -51,9 +63,10 @@ describe.skip('CollectionMetadataStepComponent', () => {
expect(component.stepperActiveValue()).toBe(0);
expect(component.targetStepValue()).toBe(1);
expect(component.isDisabled()).toBe(false);
+ expect(component.isCedarMode()).toBe(false);
});
- it('should handle save metadata', () => {
+ it('should handle save metadata in filter mode', () => {
const mockForm = new FormGroup({});
component.collectionMetadataForm.set(mockForm);
@@ -87,7 +100,7 @@ describe.skip('CollectionMetadataStepComponent', () => {
expect(navigateSpy).toHaveBeenCalledWith(component.targetStepValue());
});
- it('should handle discard changes', () => {
+ it('should handle discard changes in filter mode', () => {
const mockForm = new FormGroup({});
component.collectionMetadataForm.set(mockForm);
component.collectionMetadataSaved.set(true);
@@ -102,11 +115,6 @@ describe.skip('CollectionMetadataStepComponent', () => {
expect(component.collectionMetadataSaved()).toBe(false);
});
- it('should have actions defined', () => {
- expect(component.actions).toBeDefined();
- expect(component.actions.getCollectionDetails).toBeDefined();
- });
-
it('should handle different input values', () => {
fixture.componentRef.setInput('stepperActiveValue', 2);
fixture.componentRef.setInput('targetStepValue', 3);
@@ -117,4 +125,94 @@ describe.skip('CollectionMetadataStepComponent', () => {
expect(component.targetStepValue()).toBe(3);
expect(component.isDisabled()).toBe(true);
});
+
+ describe('CEDAR mode', () => {
+ beforeEach(() => {
+ setup(true, MOCK_CEDAR_TEMPLATE);
+ });
+
+ it('should initialize in CEDAR mode', () => {
+ expect(component.isCedarMode()).toBe(true);
+ expect(component.cedarTemplate()).toEqual(MOCK_CEDAR_TEMPLATE);
+ });
+
+ it('should handle discard changes in CEDAR mode', () => {
+ component.cedarFormData.set({ field: 'value' });
+ component.collectionMetadataSaved.set(true);
+
+ component.handleDiscardChanges();
+
+ expect(component.collectionMetadataSaved()).toBe(false);
+ expect(component.cedarFormData()).toEqual({});
+ });
+
+ it('should handle discard changes with existing record in CEDAR mode', () => {
+ const existingRecord: CedarMetadataRecordData = {
+ attributes: {
+ metadata: { field: 'original' } as unknown as CedarMetadataRecordData['attributes']['metadata'],
+ is_published: false,
+ },
+ relationships: {
+ template: { data: { type: 'cedar-metadata-templates', id: 'template-1' } },
+ target: { data: { type: 'nodes', id: 'node-1' } },
+ },
+ };
+ fixture.componentRef.setInput('existingCedarRecord', existingRecord);
+ fixture.detectChanges();
+
+ component.collectionMetadataSaved.set(true);
+ component.handleDiscardChanges();
+
+ expect(component.collectionMetadataSaved()).toBe(false);
+ });
+
+ it('should populate cedarFormData from existingCedarRecord', () => {
+ const existingRecord: CedarMetadataRecordData = {
+ attributes: {
+ metadata: { field: 'existing' } as unknown as CedarMetadataRecordData['attributes']['metadata'],
+ is_published: true,
+ },
+ relationships: {
+ template: { data: { type: 'cedar-metadata-templates', id: 'template-1' } },
+ target: { data: { type: 'nodes', id: 'node-1' } },
+ },
+ };
+ fixture.componentRef.setInput('existingCedarRecord', existingRecord);
+ fixture.detectChanges();
+
+ expect(component.cedarFormData()).toEqual({ field: 'existing' });
+ });
+
+ it('should emit cedarDataSaved when handleSaveCedarMetadata is called without editor', () => {
+ const cedarDataSavedSpy = vi.spyOn(component.cedarDataSaved, 'emit');
+ const stepChangeSpy = vi.spyOn(component.stepChange, 'emit');
+
+ component.handleSaveCedarMetadata();
+
+ expect(cedarDataSavedSpy).not.toHaveBeenCalled();
+ expect(stepChangeSpy).not.toHaveBeenCalled();
+ });
+
+ it('should handle onCedarChange event', () => {
+ const mockMetadata = { field: 'changed' };
+ const mockEditor = { currentMetadata: mockMetadata } as unknown as EventTarget;
+ const mockEvent = new CustomEvent('change');
+ Object.defineProperty(mockEvent, 'target', { value: mockEditor });
+
+ component.onCedarChange(mockEvent);
+
+ expect(component.cedarFormData()).toEqual(mockMetadata);
+ });
+
+ it('should not call handleSaveCedarMetadata without template', () => {
+ fixture.componentRef.setInput('cedarTemplate', null);
+ fixture.detectChanges();
+
+ const cedarDataSavedSpy = vi.spyOn(component.cedarDataSaved, 'emit');
+
+ component.handleSaveCedarMetadata();
+
+ expect(cedarDataSavedSpy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts
index acb6a1d0b..b4fe45f64 100644
--- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts
+++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts
@@ -7,13 +7,32 @@ import { Select } from 'primeng/select';
import { Step, StepItem, StepPanel } from 'primeng/stepper';
import { Tooltip } from 'primeng/tooltip';
-import { ChangeDetectionStrategy, Component, computed, effect, input, output, signal } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ CUSTOM_ELEMENTS_SCHEMA,
+ effect,
+ ElementRef,
+ input,
+ output,
+ signal,
+ viewChild,
+ ViewEncapsulation,
+} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { collectionFilterTypes } from '@osf/features/collections/constants';
import { AddToCollectionSteps, CollectionFilterType } from '@osf/features/collections/enums';
import { CollectionFilterEntry } from '@osf/features/collections/models/collection-filter-entry.model';
import { AddToCollectionSelectors } from '@osf/features/collections/store/add-to-collection';
+import { CEDAR_CONFIG, CEDAR_VIEWER_CONFIG } from '@osf/features/metadata/constants';
+import {
+ CedarEditorElement,
+ CedarMetadataDataTemplateJsonApi,
+ CedarMetadataRecordData,
+ CedarRecordDataBinding,
+} from '@osf/features/metadata/models';
import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/collections.model';
import { CollectionsSelectors, GetCollectionDetails } from '@osf/shared/stores/collections';
@@ -23,6 +42,8 @@ import { CollectionsSelectors, GetCollectionDetails } from '@osf/shared/stores/c
templateUrl: './collection-metadata-step.component.html',
styleUrl: './collection-metadata-step.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
+ encapsulation: ViewEncapsulation.None,
})
export class CollectionMetadataStepComponent {
private readonly filterTypes = collectionFilterTypes;
@@ -45,16 +66,27 @@ export class CollectionMetadataStepComponent {
targetStepValue = input.required();
isDisabled = input.required();
primaryCollectionId = input();
+ isCedarMode = input(false);
+ cedarTemplate = input(null);
+ existingCedarRecord = input(null);
stepChange = output();
metadataSaved = output();
+ cedarDataSaved = output();
collectionMetadataForm = signal(new FormGroup({}));
collectionMetadataSaved = signal(false);
originalFormValues = signal>({});
formPopulatedFromSubmission = signal(false);
+ cedarFormData = signal>({});
- actions = createDispatchMap({ getCollectionDetails: GetCollectionDetails });
+ cedarConfig = CEDAR_CONFIG;
+ cedarViewerConfig = CEDAR_VIEWER_CONFIG;
+
+ cedarEditor = viewChild>('cedarEditor');
+ cedarViewer = viewChild>('cedarViewer');
+
+ private readonly actions = createDispatchMap({ getCollectionDetails: GetCollectionDetails });
constructor() {
this.setupEffects();
@@ -65,6 +97,19 @@ export class CollectionMetadataStepComponent {
}
handleDiscardChanges() {
+ if (this.isCedarMode()) {
+ const record = this.existingCedarRecord();
+ this.cedarFormData.set(
+ record?.attributes?.metadata ? (record.attributes.metadata as Record) : {}
+ );
+ const editor = this.cedarEditor()?.nativeElement;
+ if (editor) {
+ editor.instanceObject = this.cedarFormData();
+ }
+ this.collectionMetadataSaved.set(false);
+ return;
+ }
+
const form = this.collectionMetadataForm();
const originalValues = this.originalFormValues();
@@ -85,6 +130,39 @@ export class CollectionMetadataStepComponent {
this.stepChange.emit(AddToCollectionSteps.Complete);
}
+ handleSaveCedarMetadata() {
+ const editor = this.cedarEditor()?.nativeElement;
+ const template = this.cedarTemplate();
+ if (!editor || !template) return;
+
+ const currentMetadata = editor.currentMetadata;
+ const isValid = !!editor.dataQualityReport?.isValid;
+
+ if (currentMetadata) {
+ this.cedarFormData.set(currentMetadata as Record);
+ }
+
+ const cedarData: CedarRecordDataBinding = {
+ data: currentMetadata as CedarRecordDataBinding['data'],
+ id: template.id,
+ isPublished: isValid,
+ };
+
+ this.collectionMetadataSaved.set(true);
+ this.cedarDataSaved.emit(cedarData);
+ this.stepChange.emit(AddToCollectionSteps.Complete);
+ }
+
+ onCedarChange(event: Event): void {
+ const customEvent = event as CustomEvent;
+ if (customEvent?.target) {
+ const editor = customEvent.target as CedarEditorElement;
+ if (editor && typeof editor.currentMetadata !== 'undefined') {
+ this.cedarFormData.set(editor.currentMetadata as Record);
+ }
+ }
+ }
+
private buildCollectionMetadataForm() {
const filterEntries = this.availableFilterEntries();
const formControls: Record = {};
@@ -115,9 +193,21 @@ export class CollectionMetadataStepComponent {
}
});
+ effect(() => {
+ const record = this.existingCedarRecord();
+ if (record?.attributes?.metadata) {
+ const metadata = record.attributes.metadata as Record;
+ this.cedarFormData.set(metadata);
+ const editor = this.cedarEditor()?.nativeElement;
+ if (editor) editor.instanceObject = metadata;
+ const viewer = this.cedarViewer()?.nativeElement;
+ if (viewer) viewer.instanceObject = metadata;
+ }
+ });
+
effect(() => {
const filterEntries = this.availableFilterEntries();
- if (filterEntries.length) {
+ if (filterEntries.length && !this.isCedarMode()) {
this.buildCollectionMetadataForm();
}
});
@@ -133,7 +223,8 @@ export class CollectionMetadataStepComponent {
form.controls &&
Object.keys(form.controls).length > 0 &&
filterEntries.length > 0 &&
- !alreadyPopulated
+ !alreadyPopulated &&
+ !this.isCedarMode()
) {
this.populateFormFromSubmission(submission.submission);
this.formPopulatedFromSubmission.set(true);
@@ -142,8 +233,10 @@ export class CollectionMetadataStepComponent {
effect(() => {
if (!this.collectionMetadataSaved() && this.stepperActiveValue() !== AddToCollectionSteps.CollectionMetadata) {
- this.collectionMetadataForm().reset();
- this.formPopulatedFromSubmission.set(false);
+ if (!this.isCedarMode()) {
+ this.collectionMetadataForm().reset();
+ this.formPopulatedFromSubmission.set(false);
+ }
}
});
}
diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.html b/src/app/features/collections/components/collections-discover/collections-discover.component.html
index 1e9261f03..1d2fe31b5 100644
--- a/src/app/features/collections/components/collections-discover/collections-discover.component.html
+++ b/src/app/features/collections/components/collections-discover/collections-discover.component.html
@@ -37,7 +37,13 @@ {{ collectionProvider()?