diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.html b/src/app/features/collections/components/add-to-collection/add-to-collection.component.html index 41cf077d5..d76299fba 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.html +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.html @@ -48,7 +48,11 @@

{{ collectionProvider()? [targetStepValue]="AddToCollectionSteps.CollectionMetadata" [isDisabled]="isCollectionMetadataDisabled()" [primaryCollectionId]="primaryCollectionId()" + [isCedarMode]="isCedarMode()" + [cedarTemplate]="requiredMetadataTemplate()" + [existingCedarRecord]="existingCedarRecord()" (metadataSaved)="handleCollectionMetadataSaved($event)" + (cedarDataSaved)="handleCedarDataSaved($event)" (stepChange)="handleChangeStep($event)" /> diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts index 9f49bfde0..b7c9645b7 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts @@ -10,6 +10,8 @@ import { ProjectContributorsStepComponent } from '@osf/features/collections/comp import { ProjectMetadataStepComponent } from '@osf/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component'; import { SelectProjectStepComponent } from '@osf/features/collections/components/add-to-collection/select-project-step/select-project-step.component'; import { AddToCollectionSteps } from '@osf/features/collections/enums'; +import { CedarRecordDataBinding } from '@osf/features/metadata/models'; +import { MetadataSelectors } from '@osf/features/metadata/store'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -62,8 +64,10 @@ describe('AddToCollectionComponent', () => { signals: [ { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, { selector: CollectionsSelectors.getCollectionProvider, value: mockCollectionProvider }, + { selector: CollectionsSelectors.getRequiredMetadataTemplate, value: null }, { selector: ProjectsSelectors.getSelectedProject, value: MOCK_PROJECT }, { selector: UserSelectors.getCurrentUser, value: MOCK_USER }, + { selector: MetadataSelectors.getCedarRecords, value: [] }, ], }), ], @@ -123,6 +127,19 @@ describe('AddToCollectionComponent', () => { expect(component.stepperActiveValue()).toBe(AddToCollectionSteps.Complete); }); + it('should handle cedar data saved', () => { + const mockCedarData: CedarRecordDataBinding = { + data: {} as CedarRecordDataBinding['data'], + id: 'template-123', + isPublished: false, + }; + component.handleCedarDataSaved(mockCedarData); + + expect(component.pendingCedarData()).toEqual(mockCedarData); + expect(component.collectionMetadataSaved()).toBe(true); + expect(component.stepperActiveValue()).toBe(AddToCollectionSteps.Complete); + }); + it('should have actions defined', () => { expect(component.actions).toBeDefined(); expect(component.actions.getCollectionProvider).toBeDefined(); diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts index 15e4fbcbb..c90a8cee2 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts @@ -5,7 +5,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Stepper } from 'primeng/stepper'; -import { filter, map, Observable, of, switchMap } from 'rxjs'; +import { filter, finalize, map, Observable, of, switchMap } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; import { @@ -23,9 +23,18 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormGroup } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; import { UserSelectors } from '@core/store/user'; +import { CedarMetadataRecordData, CedarRecordDataBinding } from '@osf/features/metadata/models'; +import { + CreateCedarMetadataRecord, + GetCedarMetadataRecords, + MetadataSelectors, + UpdateCedarMetadataRecord, +} from '@osf/features/metadata/store'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { CanDeactivateComponent } from '@osf/shared/models/can-deactivate.interface'; import { BrandService } from '@osf/shared/services/brand.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; @@ -81,6 +90,7 @@ export class AddToCollectionComponent implements CanDeactivateComponent { private readonly headerStyleHelper = inject(HeaderStyleService); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); + private readonly environment = inject(ENVIRONMENT); readonly selectedProjectId = toSignal( this.route.params.pipe(map((params) => params['id'])) ?? of(null) @@ -92,15 +102,18 @@ export class AddToCollectionComponent implements CanDeactivateComponent { isProviderLoading = select(CollectionsSelectors.getCollectionProviderLoading); collectionProvider = select(CollectionsSelectors.getCollectionProvider); + requiredMetadataTemplate = select(CollectionsSelectors.getRequiredMetadataTemplate); selectedProject = select(ProjectsSelectors.getSelectedProject); currentUser = select(UserSelectors.getCurrentUser); currentCollectionSubmission = select(AddToCollectionSelectors.getCurrentCollectionSubmission); + cedarRecords = select(MetadataSelectors.getCedarRecords); providerId = signal(''); allowNavigation = signal(false); projectMetadataSaved = signal(false); projectContributorsSaved = signal(false); collectionMetadataSaved = signal(false); + pendingCedarData = signal(null); stepperActiveValue = signal(AddToCollectionSteps.SelectProject); primaryCollectionId = computed(() => this.collectionProvider()?.primaryCollection?.id); @@ -110,14 +123,24 @@ export class AddToCollectionComponent implements CanDeactivateComponent { isCollectionMetadataDisabled = computed( () => !this.selectedProject() || !this.projectMetadataSaved() || !this.projectContributorsSaved() ); + isCedarMode = computed(() => this.environment.collectionSubmissionWithCedar && !!this.requiredMetadataTemplate()); + existingCedarRecord = computed(() => { + const records = this.cedarRecords(); + const templateId = this.requiredMetadataTemplate()?.id; + if (!records?.length || !templateId) return null; + return records.find((r) => r.relationships?.template?.data?.id === templateId) ?? null; + }); - actions = createDispatchMap({ + readonly actions = createDispatchMap({ getCollectionProvider: GetCollectionProvider, clearAddToCollectionState: ClearAddToCollectionState, updateCollectionSubmission: UpdateCollectionSubmission, deleteCollectionSubmission: RemoveCollectionSubmission, setSelectedProject: SetSelectedProject, getCurrentCollectionSubmission: GetCurrentCollectionSubmission, + getCedarRecords: GetCedarMetadataRecords, + createCedarRecord: CreateCedarMetadataRecord, + updateCedarRecord: UpdateCedarMetadataRecord, }); showRemoveButton = computed( @@ -133,7 +156,10 @@ export class AddToCollectionComponent implements CanDeactivateComponent { } @HostListener('window:beforeunload', ['$event']) - onBeforeUnload($event: BeforeUnloadEvent): boolean { + onBeforeUnload($event: BeforeUnloadEvent): boolean | undefined { + if (this.allowNavigation() || !this.hasUnsavedChanges()) { + return undefined; + } $event.preventDefault(); return false; } @@ -171,11 +197,17 @@ export class AddToCollectionComponent implements CanDeactivateComponent { this.stepperActiveValue.set(AddToCollectionSteps.Complete); } + handleCedarDataSaved(data: CedarRecordDataBinding): void { + this.pendingCedarData.set(data); + this.collectionMetadataSaved.set(true); + this.stepperActiveValue.set(AddToCollectionSteps.Complete); + } + handleAddToCollection() { const payload = { collectionId: this.primaryCollectionId() || '', projectId: this.selectedProject()?.id || '', - collectionMetadata: this.collectionMetadataForm.value || {}, + collectionMetadata: this.isCedarMode() ? {} : this.collectionMetadataForm.value || {}, userId: this.currentUser()?.id || '', }; @@ -186,13 +218,20 @@ export class AddToCollectionComponent implements CanDeactivateComponent { this.actions .updateCollectionSubmission(payload) - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe( + switchMap(() => this.saveCedarRecordIfNeeded()), + finalize(() => this.loaderService.hide()), + takeUntilDestroyed(this.destroyRef) + ) .subscribe({ next: () => { this.toastService.showSuccess('collections.addToCollection.confirmationDialogToastMessage'); this.allowNavigation.set(true); this.router.navigate([this.selectedProject()?.id, 'overview']); }, + error: () => { + this.toastService.showError('collections.addToCollection.updateError'); + }, }); } else { this.customDialogService @@ -203,11 +242,17 @@ export class AddToCollectionComponent implements CanDeactivateComponent { }) .onClose.pipe( filter((res) => !!res), + switchMap(() => this.saveCedarRecordIfNeeded()), takeUntilDestroyed(this.destroyRef) ) - .subscribe(() => { - this.allowNavigation.set(true); - this.router.navigate([this.selectedProject()?.id, 'overview']); + .subscribe({ + next: () => { + this.allowNavigation.set(true); + this.router.navigate([this.selectedProject()?.id, 'overview']); + }, + error: () => { + this.toastService.showError('collections.addToCollection.updateError'); + }, }); } } @@ -248,6 +293,20 @@ export class AddToCollectionComponent implements CanDeactivateComponent { }); } + private saveCedarRecordIfNeeded(): Observable { + if (!this.isCedarMode()) return of(null); + + const cedarData = this.pendingCedarData(); + const projectId = this.selectedProject()?.id; + const templateId = this.requiredMetadataTemplate()?.id; + if (!cedarData || !projectId || !templateId) return of(null); + + const existingId = this.existingCedarRecord()?.id; + return existingId + ? this.actions.updateCedarRecord(cedarData, existingId, projectId, ResourceType.Project) + : this.actions.createCedarRecord(cedarData, projectId, ResourceType.Project); + } + private initializeProvider(): void { const id = this.route.snapshot.paramMap.get('providerId'); if (!id) { @@ -286,6 +345,14 @@ export class AddToCollectionComponent implements CanDeactivateComponent { this.actions.setSelectedProject(submission.project); } }); + + effect(() => { + const projectId = this.selectedProjectId(); + const isCedar = this.isCedarMode(); + if (isCedar && projectId) { + this.actions.getCedarRecords(projectId, ResourceType.Project); + } + }); } private setupCleanup() { diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html index f10094962..0b0cd6498 100644 --- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html +++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.html @@ -11,14 +11,25 @@

{{ 'collections.addToCollection.collectionMetadata' | translate }}

@if (!isDisabled() && stepperActiveValue() !== targetStepValue()) { @if (collectionMetadataSaved()) { - @for (filterEntry of availableFilterEntries(); track filterEntry.key) { -
-

{{ filterEntry.labelKey | translate }}

+ @if (isCedarMode()) { + @if (cedarTemplate()) { + + } + } @else { + @for (filterEntry of availableFilterEntries(); track filterEntry.key) { +
+

{{ filterEntry.labelKey | translate }}

-

- {{ collectionMetadataForm().get(filterEntry.key)?.value }} -

-
+

+ {{ collectionMetadataForm().get(filterEntry.key)?.value }} +

+
+ } } } @@ -35,33 +46,59 @@

{{ 'collections.addToCollection.collectionMetadata' | translate }}

-
- @for (filterEntry of availableFilterEntries(); track filterEntry.key) { -
- - + @if (isCedarMode()) { + @if (cedarTemplate()) { +
+
+ +
+ + +
+ } @else { +

{{ 'collections.addToCollection.cedarFormNotAvailable' | translate }}

} - + } @else { +
+ @for (filterEntry of availableFilterEntries(); track filterEntry.key) { +
+ + +
+ } +
-
- - -
+
+ + +
+ } 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/metadata/components/metadata-collection-item/metadata-collection-item.component.html b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html index cf05872c9..d7a384a20 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.html @@ -22,6 +22,16 @@
+@if (showCedarViewer()) { +
+ +
+} + @if (showAttributes()) {
@for (attribute of attributes(); track attribute.key) { diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts index 106fea114..1ed5237ae 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts @@ -4,32 +4,23 @@ import { provideRouter } from '@angular/router'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; +import { + MOCK_CEDAR_RECORD, + MOCK_CEDAR_SUBMISSION, + MOCK_CEDAR_TEMPLATE, +} from '@testing/data/collections/cedar-metadata.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { MetadataCollectionItemComponent } from './metadata-collection-item.component'; +const mockSubmission: CollectionSubmission = MOCK_CEDAR_SUBMISSION; +const mockCedarTemplate = MOCK_CEDAR_TEMPLATE; +const mockCedarRecord = MOCK_CEDAR_RECORD; + describe('MetadataCollectionItemComponent', () => { let component: MetadataCollectionItemComponent; let fixture: ComponentFixture; - const mockSubmission: CollectionSubmission = { - id: '1', - type: 'collection-submission', - collectionTitle: 'Test Collection', - collectionId: 'collection-123', - reviewsState: CollectionSubmissionReviewState.Pending, - collectedType: 'preprint', - status: 'pending', - volume: '1', - issue: '1', - programArea: 'Science', - schoolType: 'University', - studyDesign: 'Experimental', - dataType: 'Quantitative', - disease: 'Cancer', - gradeLevels: 'Graduate', - }; - beforeEach(() => { TestBed.configureTestingModule({ imports: [MetadataCollectionItemComponent], @@ -149,4 +140,76 @@ describe('MetadataCollectionItemComponent', () => { const attributesSection = fixture.nativeElement.querySelector('.flex.flex-column.gap-2.mt-2'); expect(attributesSection).toBeFalsy(); }); + + describe('CEDAR mode', () => { + it('should not show cedar viewer when isCedarMode is false', () => { + fixture.componentRef.setInput('isCedarMode', false); + fixture.componentRef.setInput('cedarRecord', mockCedarRecord); + fixture.componentRef.setInput('cedarTemplate', mockCedarTemplate); + fixture.detectChanges(); + + expect(component.showCedarViewer()).toBe(false); + }); + + it('should not show cedar viewer when cedarRecord is null', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecord', null); + fixture.componentRef.setInput('cedarTemplate', mockCedarTemplate); + fixture.detectChanges(); + + expect(component.showCedarViewer()).toBe(false); + }); + + it('should not show cedar viewer when cedarTemplate is null', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecord', mockCedarRecord); + fixture.componentRef.setInput('cedarTemplate', null); + fixture.detectChanges(); + + expect(component.showCedarViewer()).toBe(false); + }); + + it('should show cedar viewer when isCedarMode, record, and template are provided', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecord', mockCedarRecord); + fixture.componentRef.setInput('cedarTemplate', mockCedarTemplate); + fixture.detectChanges(); + + expect(component.showCedarViewer()).toBe(true); + }); + + it('should not show cedar viewer when submission is removed', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('cedarRecord', mockCedarRecord); + fixture.componentRef.setInput('cedarTemplate', mockCedarTemplate); + fixture.componentRef.setInput('submission', { + ...mockSubmission, + reviewsState: CollectionSubmissionReviewState.Removed, + }); + fixture.detectChanges(); + + expect(component.showCedarViewer()).toBe(false); + }); + + it('should not show attributes in cedar mode', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.detectChanges(); + + expect(component.showAttributes()).toBe(false); + }); + + it('should compute cedarMetadata from record', () => { + fixture.componentRef.setInput('cedarRecord', mockCedarRecord); + fixture.detectChanges(); + + expect(component.cedarMetadata()).toEqual({ field: 'value' }); + }); + + it('should return empty object for cedarMetadata when no record', () => { + fixture.componentRef.setInput('cedarRecord', null); + fixture.detectChanges(); + + expect(component.cedarMetadata()).toEqual({}); + }); + }); }); diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts index 1c023afd9..c9d700248 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.ts @@ -3,10 +3,19 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Tag } from 'primeng/tag'; -import { ChangeDetectionStrategy, Component, computed, 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 { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; import { KeyValueModel } from '@osf/shared/models/common/key-value.model'; @@ -18,11 +27,18 @@ import { CollectionStatusSeverityPipe } from '@osf/shared/pipes/collection-statu templateUrl: './metadata-collection-item.component.html', styleUrl: './metadata-collection-item.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + encapsulation: ViewEncapsulation.None, }) export class MetadataCollectionItemComponent { readonly CollectionSubmissionReviewState = CollectionSubmissionReviewState; submission = input.required(); + isCedarMode = input(false); + cedarRecord = input(null); + cedarTemplate = input(null); + + cedarViewerConfig = CEDAR_VIEWER_CONFIG; showSubmissionButton = computed(() => this.submission().reviewsState === CollectionSubmissionReviewState.Accepted); @@ -32,9 +48,25 @@ export class MetadataCollectionItemComponent { }); showAttributes = computed( - () => this.submission().reviewsState !== CollectionSubmissionReviewState.Removed && !!this.attributes().length + () => + !this.isCedarMode() && + this.submission().reviewsState !== CollectionSubmissionReviewState.Removed && + !!this.attributes().length + ); + + showCedarViewer = computed( + () => + this.isCedarMode() && + !!this.cedarRecord() && + !!this.cedarTemplate()?.attributes?.template && + this.submission().reviewsState !== CollectionSubmissionReviewState.Removed ); + cedarMetadata = computed(() => { + const record = this.cedarRecord(); + return record?.attributes?.metadata ? (record.attributes.metadata as Record) : {}; + }); + attributes = computed(() => { const submission = this.submission(); const attributes: KeyValueModel[] = []; diff --git a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.html b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.html index 2a135bdee..d9d0a0815 100644 --- a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.html +++ b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.html @@ -9,7 +9,12 @@

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

@if (submissions?.length) { @for (submission of submissions; track submission.id) { - + } } @else {

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

diff --git a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts index bd9568d2c..9d446b991 100644 --- a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts @@ -4,12 +4,22 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; +import { + MOCK_CEDAR_RECORD, + MOCK_CEDAR_SUBMISSION, + MOCK_CEDAR_TEMPLATE, +} from '@testing/data/collections/cedar-metadata.mock'; import { MOCK_PROJECT_COLLECTION_SUBMISSIONS } from '@testing/data/collections/collection-submissions.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { MetadataCollectionsComponent } from './metadata-collections.component'; +const mockTemplateId = MOCK_CEDAR_TEMPLATE.id; +const mockCedarTemplate = MOCK_CEDAR_TEMPLATE; +const mockCedarRecord = MOCK_CEDAR_RECORD; +const mockSubmissionsWithTemplate = [MOCK_CEDAR_SUBMISSION]; + describe('MetadataCollectionsComponent', () => { let component: MetadataCollectionsComponent; let fixture: ComponentFixture; @@ -53,4 +63,49 @@ describe('MetadataCollectionsComponent', () => { const content = fixture.nativeElement.textContent; expect(content).toContain('project.overview.metadata.noCollections'); }); + + it('should default isCedarMode to false', () => { + expect(component.isCedarMode()).toBe(false); + }); + + it('should build cedarRecordByTemplateId map from records', () => { + fixture.componentRef.setInput('cedarRecords', [mockCedarRecord]); + fixture.detectChanges(); + + const map = component.cedarRecordByTemplateId(); + expect(map.get(mockTemplateId)).toEqual(mockCedarRecord); + }); + + it('should build empty cedarRecordByTemplateId map when no records', () => { + fixture.componentRef.setInput('cedarRecords', null); + fixture.detectChanges(); + + expect(component.cedarRecordByTemplateId().size).toBe(0); + }); + + it('should build cedarTemplateById map from templates', () => { + fixture.componentRef.setInput('cedarTemplates', [mockCedarTemplate]); + fixture.detectChanges(); + + const map = component.cedarTemplateById(); + expect(map.get(mockTemplateId)).toEqual(mockCedarTemplate); + }); + + it('should build empty cedarTemplateById map when no templates', () => { + fixture.componentRef.setInput('cedarTemplates', null); + fixture.detectChanges(); + + expect(component.cedarTemplateById().size).toBe(0); + }); + + it('should pass matching cedarRecord to items in cedar mode', () => { + fixture.componentRef.setInput('isCedarMode', true); + fixture.componentRef.setInput('projectSubmissions', mockSubmissionsWithTemplate); + fixture.componentRef.setInput('cedarRecords', [mockCedarRecord]); + fixture.componentRef.setInput('cedarTemplates', [mockCedarTemplate]); + fixture.detectChanges(); + + const items = fixture.debugElement.queryAll(By.css('osf-metadata-collection-item')); + expect(items.length).toBe(1); + }); }); diff --git a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts index affc90e98..950d7e2ac 100644 --- a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts +++ b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.ts @@ -3,8 +3,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; import { MetadataCollectionItemComponent } from '../metadata-collection-item/metadata-collection-item.component'; @@ -19,4 +20,22 @@ import { MetadataCollectionItemComponent } from '../metadata-collection-item/met export class MetadataCollectionsComponent { projectSubmissions = input(null); isProjectSubmissionsLoading = input(false); + cedarRecords = input(null); + cedarTemplates = input(null); + isCedarMode = input(false); + + 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) ?? []); + }); } diff --git a/src/app/features/metadata/metadata.component.html b/src/app/features/metadata/metadata.component.html index f49bd7ff0..e49c5490b 100644 --- a/src/app/features/metadata/metadata.component.html +++ b/src/app/features/metadata/metadata.component.html @@ -71,6 +71,9 @@ }
diff --git a/src/app/features/metadata/metadata.component.ts b/src/app/features/metadata/metadata.component.ts index ad6f68623..ef00699e8 100644 --- a/src/app/features/metadata/metadata.component.ts +++ b/src/app/features/metadata/metadata.component.ts @@ -128,6 +128,8 @@ export class MetadataComponent implements OnInit, OnDestroy { private readonly environment = inject(ENVIRONMENT); private readonly signpostingService = inject(SignpostingService); + readonly collectionSubmissionWithCedar = this.environment.collectionSubmissionWithCedar; + private resourceId = ''; tabs = signal([]); diff --git a/src/app/shared/mappers/collections/collections.mapper.ts b/src/app/shared/mappers/collections/collections.mapper.ts index 680b34a04..cd7711c26 100644 --- a/src/app/shared/mappers/collections/collections.mapper.ts +++ b/src/app/shared/mappers/collections/collections.mapper.ts @@ -71,6 +71,7 @@ export class CollectionsMapper { backgroundColor: response.embeds.brand.data.attributes.background_color, } : null, + requiredMetadataTemplate: response.embeds.required_metadata_template?.data ?? null, }; } @@ -116,6 +117,8 @@ export class CollectionsMapper { gradeLevels: submission.attributes.grade_levels, collectionTitle: replaceBadEncodedChars(submission.embeds.collection.data.attributes.title), collectionId: submission.embeds.collection.data.relationships.provider.data.id, + requiredMetadataTemplateId: + submission.embeds.collection.data.relationships.required_metadata_template?.data?.id ?? null, }; } @@ -268,11 +271,15 @@ export class CollectionsMapper { } static collectionSubmissionUpdateRequest(payload: CollectionSubmissionPayload) { + const collectionsMetadata = convertToSnakeCase(payload.collectionMetadata); + return { data: { id: `${payload.projectId}-${payload.collectionId}`, type: 'collection-submissions', - attributes: {}, + attributes: { + ...collectionsMetadata, + }, relationships: {}, }, }; diff --git a/src/app/shared/models/collections/collections-json-api.model.ts b/src/app/shared/models/collections/collections-json-api.model.ts index 2ce9402af..9dce2537f 100644 --- a/src/app/shared/models/collections/collections-json-api.model.ts +++ b/src/app/shared/models/collections/collections-json-api.model.ts @@ -1,3 +1,4 @@ +import { CedarMetadataDataTemplateJsonApi } from '@osf/features/metadata/models'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; import { BrandDataJsonApi } from '../brand/brand.json-api.model'; @@ -14,6 +15,9 @@ export interface CollectionProviderResponseJsonApi { brand: { data?: BrandDataJsonApi; }; + required_metadata_template?: { + data?: CedarMetadataDataTemplateJsonApi | null; + }; }; relationships: { primary_collection: { @@ -76,6 +80,12 @@ export interface CollectionSubmissionJsonApi { id: string; }; }; + required_metadata_template?: { + data?: { + id: string; + type: string; + } | null; + }; }; }; }; diff --git a/src/app/shared/models/collections/collections.model.ts b/src/app/shared/models/collections/collections.model.ts index 6b67d7d16..ebecbbe80 100644 --- a/src/app/shared/models/collections/collections.model.ts +++ b/src/app/shared/models/collections/collections.model.ts @@ -1,3 +1,4 @@ +import { CedarMetadataDataTemplateJsonApi } from '@osf/features/metadata/models'; import { CollectionSubmissionReviewAction } from '@osf/features/moderation/models'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; @@ -19,6 +20,7 @@ export interface CollectionProvider extends BaseProviderModel { }; brand: BrandModel | null; defaultLicenseId?: string | null; + requiredMetadataTemplate?: CedarMetadataDataTemplateJsonApi | null; } export interface CollectionFilters { @@ -62,6 +64,7 @@ export interface CollectionSubmission { dataType: string; disease: string; gradeLevels: string; + requiredMetadataTemplateId?: string | null; } export interface CollectionSubmissionWithGuid { diff --git a/src/app/shared/models/environment.model.ts b/src/app/shared/models/environment.model.ts index 184fe4ce4..3a688ca4e 100644 --- a/src/app/shared/models/environment.model.ts +++ b/src/app/shared/models/environment.model.ts @@ -65,4 +65,5 @@ export interface EnvironmentModel { */ googleFilePickerAppId: number; throttleToken: string; + collectionSubmissionWithCedar: boolean; } diff --git a/src/app/shared/services/collections.service.ts b/src/app/shared/services/collections.service.ts index 8b13f253a..2fea963f7 100644 --- a/src/app/shared/services/collections.service.ts +++ b/src/app/shared/services/collections.service.ts @@ -56,7 +56,7 @@ export class CollectionsService { private actions = createDispatchMap({ setTotalSubmissions: SetTotalSubmissions }); getCollectionProvider(collectionName: string): Observable { - const url = `${this.apiUrl}/providers/collections/${collectionName}/?embed=brand`; + const url = `${this.apiUrl}/providers/collections/${collectionName}/?embed=brand,required_metadata_template`; return this.jsonApiService .get>(url) diff --git a/src/app/shared/stores/collections/collections.selectors.ts b/src/app/shared/stores/collections/collections.selectors.ts index ca076ada9..22620d7ae 100644 --- a/src/app/shared/stores/collections/collections.selectors.ts +++ b/src/app/shared/stores/collections/collections.selectors.ts @@ -21,6 +21,11 @@ export class CollectionsSelectors { return state.collectionProvider.data; } + @Selector([CollectionsState]) + static getRequiredMetadataTemplate(state: CollectionsStateModel) { + return state.collectionProvider.data?.requiredMetadataTemplate ?? null; + } + @Selector([CollectionsState]) static getCollectionDetails(state: CollectionsStateModel) { return state.collectionDetails.data; diff --git a/src/assets/config/template.json b/src/assets/config/template.json index 826cb39c4..18f954f75 100644 --- a/src/assets/config/template.json +++ b/src/assets/config/template.json @@ -27,5 +27,6 @@ "newRelicLoaderConfigTrustKey": "", "newRelicLoaderConfigAgentID": "", "newRelicLoaderConfigLicenseKey": "", - "newRelicLoaderConfigApplicationID": "" + "newRelicLoaderConfigApplicationID": "", + "collectionSubmissionWithCedar": false } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index be31ffa9a..02b338530 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1403,6 +1403,7 @@ "projectMetadataMessage": "Updates made in this section will update the project.", "projectContributors": "Project Contributors", "collectionMetadata": "Collection Metadata", + "cedarFormNotAvailable": "CEDAR metadata form is not available for this collection.", "tooltipMessage": "Complete previous step to edit this section", "contributorsTooltip": "Projects must have at least one registered administrator and one author showing in the citation at all times. A registered administrator is a user who has both confirmed their account and has administrator privileges.", "noDescription": "No description", @@ -1411,6 +1412,7 @@ "projectMetadataUpdateSuccess": "Project Metadata successfully updated.", "confirmationDialogMessage": "Once submitted to the collection, the project will be made public. It can later be made private again. A moderator will review your submission before it is included in the collection.", "confirmationDialogToastMessage": "Project has been successfully submitted to the collection", + "updateError": "Failed to submit to the collection. Please try again.", "form": { "title": "Title", "description": "Description", diff --git a/src/testing/data/collections/cedar-metadata.mock.ts b/src/testing/data/collections/cedar-metadata.mock.ts new file mode 100644 index 000000000..4fbb297c3 --- /dev/null +++ b/src/testing/data/collections/cedar-metadata.mock.ts @@ -0,0 +1,67 @@ +import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData } from '@osf/features/metadata/models'; +import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; +import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; + +export const MOCK_CEDAR_TEMPLATE: CedarMetadataDataTemplateJsonApi = { + id: 'template-1', + type: 'cedar-metadata-templates', + attributes: { + schema_name: 'Test Template', + cedar_id: 'cedar-1', + template: { + '@id': 'https://repo.metadatacenter.org/templates/1', + '@type': 'https://schema.metadatacenter.org/core/Template', + type: 'object', + title: 'Test', + description: 'Test template', + $schema: 'http://json-schema.org/draft-04/schema#', + '@context': { + pav: 'http://purl.org/pav/', + xsd: 'http://www.w3.org/2001/XMLSchema#', + bibo: 'http://purl.org/ontology/bibo/', + oslc: 'http://open-services.net/ns/core#', + schema: 'http://schema.org/', + 'schema:name': { '@type': 'xsd:string' }, + 'pav:createdBy': { '@type': '@id' }, + 'pav:createdOn': { '@type': 'xsd:dateTime' }, + 'oslc:modifiedBy': { '@type': '@id' }, + 'pav:lastUpdatedOn': { '@type': 'xsd:dateTime' }, + 'schema:description': { '@type': 'xsd:string' }, + }, + required: [], + properties: {}, + _ui: { order: [], propertyLabels: {}, propertyDescriptions: {} }, + }, + }, +}; + +export const MOCK_CEDAR_RECORD: CedarMetadataRecordData = { + id: 'record-1', + attributes: { + metadata: { field: 'value' } 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' } }, + }, +}; + +export const MOCK_CEDAR_SUBMISSION: CollectionSubmission = { + id: '1', + type: 'collection-submission', + collectionTitle: 'Test Collection', + collectionId: 'collection-123', + reviewsState: CollectionSubmissionReviewState.Pending, + collectedType: 'preprint', + status: 'pending', + volume: '1', + issue: '1', + programArea: 'Science', + schoolType: 'University', + studyDesign: 'Experimental', + dataType: 'Quantitative', + disease: 'Cancer', + gradeLevels: 'Graduate', + requiredMetadataTemplateId: 'template-1', +};