-
-
+ @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 8f568269e..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
@@ -7,8 +7,10 @@ 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';
@@ -18,7 +20,7 @@ describe('CollectionMetadataStepComponent', () => {
let component: CollectionMetadataStepComponent;
let fixture: ComponentFixture
;
- function setup() {
+ function setup(isCedarMode = false, cedarTemplate: CedarMetadataDataTemplateJsonApi | null = null) {
TestBed.configureTestingModule({
imports: [CollectionMetadataStepComponent, MockComponents(StepPanel, Step, StepItem)],
providers: [
@@ -41,6 +43,10 @@ describe('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();
}
@@ -57,6 +63,7 @@ describe('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 in filter mode', () => {
@@ -118,4 +125,94 @@ describe('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 5c57c30d9..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,14 +66,25 @@ 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>({});
+
+ cedarConfig = CEDAR_CONFIG;
+ cedarViewerConfig = CEDAR_VIEWER_CONFIG;
+
+ cedarEditor = viewChild>('cedarEditor');
+ cedarViewer = viewChild>('cedarViewer');
private readonly actions = createDispatchMap({ getCollectionDetails: GetCollectionDetails });
@@ -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.spec.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts
index 86f448e72..7be0470cd 100644
--- a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts
+++ b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts
@@ -4,345 +4,258 @@ import { MockComponents, MockProvider } from 'ng-mocks';
import { Mock } from 'vitest';
-import { PLATFORM_ID } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ActivatedRoute, provideRouter, Router } from '@angular/router';
+import { ActivatedRoute } from '@angular/router';
import { ENVIRONMENT } from '@core/provider/environment.provider';
-import { GlobalSearchComponent } from '@shared/components/global-search/global-search.component';
-import { LoadingSpinnerComponent } from '@shared/components/loading-spinner/loading-spinner.component';
-import { SearchInputComponent } from '@shared/components/search-input/search-input.component';
-import { CollectionDetails, CollectionProvider } from '@shared/models/collections/collections.model';
-import { EnvironmentModel } from '@shared/models/environment.model';
-import { FilterOperatorOption } from '@shared/models/search/discoverable-filter.model';
-import { BrandService } from '@shared/services/brand.service';
-import { CustomDialogService } from '@shared/services/custom-dialog.service';
-import { HeaderStyleService } from '@shared/services/header-style.service';
-import {
- CollectionsSelectors,
- GetCollectionDetails,
- GetCollectionProvider,
- SearchCollectionSubmissions,
- SetPageNumber,
- SetSearchValue,
-} from '@shared/stores/collections';
-import { ResetSearchState, SetDefaultFilterValue, SetExtraFilters } from '@shared/stores/global-search';
-
-import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock';
-import { MOCK_COLLECTIONS_EMPTY_FILTERS } from '@testing/mocks/collections-filters.mock';
+import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component';
+import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
+import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
+import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
+import { ToastService } from '@osf/shared/services/toast.service';
+import { CollectionsSelectors } from '@shared/stores/collections';
+import { SetDefaultFilterValue, SetExtraFilters } from '@shared/stores/global-search';
+
+import { MOCK_PROVIDER } from '@testing/mocks/provider.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
-import { BrandServiceMock, BrandServiceMockType } from '@testing/providers/brand-service.mock';
-import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock';
-import { EnvironmentTokenMock } from '@testing/providers/environment.token.mock';
-import { HeaderStyleServiceMock, HeaderStyleServiceMockType } from '@testing/providers/header-style-service.mock';
+import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
-import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock';
-import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock';
+import { provideMockStore } from '@testing/providers/store-provider.mock';
+import { ToastServiceMock } from '@testing/providers/toast-provider.mock';
import { CollectionsQuerySyncService } from '../../services';
-import { CollectionsHelpDialogComponent } from '../collections-help-dialog/collections-help-dialog.component';
import { CollectionsMainContentComponent } from '../collections-main-content/collections-main-content.component';
import { CollectionsDiscoverComponent } from './collections-discover.component';
-const PROVIDER_ID = 'provider-1';
-
-const mockCollectionDetails: CollectionDetails = {
- id: 'col-1',
- type: 'collections',
- title: 'Collection',
- dateCreated: '2024-01-01T00:00:00Z',
- dateModified: '2024-01-02T00:00:00Z',
- bookmarks: false,
- isPromoted: false,
- isPublic: true,
- filters: {
- collectedType: [],
- disease: [],
- dataType: [],
- gradeLevels: [],
- issue: [],
- programArea: [],
- schoolType: [],
- status: [],
- studyDesign: [],
- volume: [],
+const MOCK_COLLECTION_PROVIDER = {
+ ...MOCK_PROVIDER,
+ primaryCollection: { id: 'collection-1', type: 'collections' },
+ requiredMetadataTemplate: null,
+};
+
+const MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE = {
+ ...MOCK_COLLECTION_PROVIDER,
+ requiredMetadataTemplate: {
+ id: 'template-1',
+ type: 'cedar-metadata-templates' as const,
+ attributes: {
+ schema_name: 'Test',
+ cedar_id: 'cedar-1',
+ template: {
+ '@id': 'https://repo.metadatacenter.org/templates/test',
+ '@type': 'https://schema.metadatacenter.org/core/Template',
+ type: 'object',
+ title: 'Test',
+ description: '',
+ $schema: 'http://json-schema.org/draft-04/schema',
+ '@context': {} as never,
+ required: [],
+ properties: {},
+ _ui: {
+ order: ['field1'],
+ propertyLabels: { field1: 'Field One' },
+ propertyDescriptions: {},
+ },
+ },
+ },
},
};
-function createMockCollectionProvider(overrides: Partial = {}): CollectionProvider {
- return {
- id: PROVIDER_ID,
- type: 'collection-providers',
- name: 'Provider',
- description: '',
- domain: 'osf.io',
- advisoryBoard: '',
- allowCommenting: false,
- allowSubmissions: true,
- domainRedirectEnabled: false,
- emailSupport: null,
- example: null,
- facebookAppId: null,
- footerLinks: '',
- permissions: [],
- reviewsWorkflow: '',
- sharePublishType: '',
- shareSource: '',
- iri: 'https://api.test.osf.io/v2/collections/col-1/',
- assets: {},
- primaryCollection: { id: 'col-1', type: 'collections' },
- brand: null,
- ...overrides,
- } as CollectionProvider;
+interface SetupOptions {
+ collectionSubmissionWithCedar?: boolean;
+ provider?: typeof MOCK_COLLECTION_PROVIDER | typeof MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE;
}
-const defaultSignals: SignalOverride[] = [
- { selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
- { selector: CollectionsSelectors.getCollectionProvider, value: null },
- { selector: CollectionsSelectors.getCollectionDetails, value: null },
- { selector: CollectionsSelectors.getAllSelectedFilters, value: { ...MOCK_COLLECTIONS_EMPTY_FILTERS } },
- { selector: CollectionsSelectors.getSortBy, value: '' },
- { selector: CollectionsSelectors.getSearchText, value: '' },
- { selector: CollectionsSelectors.getPageNumber, value: '1' },
-];
+function setup(options: SetupOptions = {}) {
+ const { collectionSubmissionWithCedar = false, provider = MOCK_COLLECTION_PROVIDER } = options;
+
+ const toastServiceMock = ToastServiceMock.simple();
+ const mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();
+ const mockRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: 'provider-1' }).build();
+
+ TestBed.configureTestingModule({
+ imports: [
+ CollectionsDiscoverComponent,
+ ...MockComponents(
+ SearchInputComponent,
+ CollectionsMainContentComponent,
+ GlobalSearchComponent,
+ LoadingSpinnerComponent
+ ),
+ ],
+ providers: [
+ provideOSFCore(),
+ { provide: ENVIRONMENT, useValue: { apiDomainUrl: 'http://localhost:8000', collectionSubmissionWithCedar } },
+ MockProvider(ToastService, toastServiceMock),
+ MockProvider(CustomDialogService, mockCustomDialogService),
+ MockProvider(ActivatedRoute, mockRoute),
+ provideMockStore({
+ signals: [
+ { selector: CollectionsSelectors.getCollectionProvider, value: provider },
+ { selector: CollectionsSelectors.getCollectionDetails, value: null },
+ { selector: CollectionsSelectors.getAllSelectedFilters, value: {} },
+ { selector: CollectionsSelectors.getSortBy, value: 'date' },
+ { selector: CollectionsSelectors.getSearchText, value: '' },
+ { selector: CollectionsSelectors.getPageNumber, value: '1' },
+ { selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
+ ],
+ }),
+ ],
+ }).overrideComponent(CollectionsDiscoverComponent, {
+ set: {
+ providers: [MockProvider(CollectionsQuerySyncService)],
+ },
+ });
+
+ const fixture = TestBed.createComponent(CollectionsDiscoverComponent);
+ const component = fixture.componentInstance;
+ const store = TestBed.inject(Store);
+ fixture.detectChanges();
+
+ return { fixture, component, store };
+}
describe('CollectionsDiscoverComponent', () => {
- let component: CollectionsDiscoverComponent;
- let fixture: ComponentFixture;
- let store: Store;
- let routerMock: RouterMockType;
- let customDialogMock: CustomDialogServiceMockType;
- let querySyncMock: Partial;
- let brandServiceMock: BrandServiceMockType;
- let headerStyleServiceMock: HeaderStyleServiceMockType;
-
- function setup(
- options: {
- routeParams?: Record;
- hasParent?: boolean;
- selectorOverrides?: SignalOverride[];
- useCedarEnvironment?: boolean;
- platformId?: string;
- } = {}
- ) {
- const routeBuilder = ActivatedRouteMockBuilder.create().withParams(
- options.routeParams ?? { providerId: PROVIDER_ID }
- );
- if (options.hasParent === false) {
- routeBuilder.withNoParent();
- }
- const mockRoute = routeBuilder.build();
- routerMock = RouterMockBuilder.create().withUrl('/collections/discover').build();
- customDialogMock = CustomDialogServiceMock.simple();
- querySyncMock = {
- initializeFromUrl: vi.fn(),
- syncStoreToUrl: vi.fn(),
- };
- brandServiceMock = BrandServiceMock.simple();
- headerStyleServiceMock = HeaderStyleServiceMock.simple();
-
- const envValue = {
- ...EnvironmentTokenMock.useValue,
- collectionSubmissionWithCedar: options.useCedarEnvironment ?? false,
- } as unknown as EnvironmentModel;
-
- const signals = mergeSignalOverrides(defaultSignals, options.selectorOverrides);
-
- TestBed.configureTestingModule({
- imports: [
- CollectionsDiscoverComponent,
- ...MockComponents(
- SearchInputComponent,
- CollectionsMainContentComponent,
- GlobalSearchComponent,
- LoadingSpinnerComponent
- ),
- ],
- providers: [
- provideOSFCore(),
- provideRouter([]),
- MockProvider(ActivatedRoute, mockRoute),
- MockProvider(Router, routerMock),
- MockProvider(CustomDialogService, customDialogMock),
- MockProvider(BrandService, brandServiceMock),
- MockProvider(HeaderStyleService, headerStyleServiceMock),
- MockProvider(PLATFORM_ID, options.platformId ?? 'browser'),
- MockProvider(ENVIRONMENT, envValue),
- provideMockStore({ signals }),
- ],
- }).overrideComponent(CollectionsDiscoverComponent, {
- set: {
- providers: [MockProvider(CollectionsQuerySyncService, querySyncMock)],
- },
+ describe('legacy mode (collectionSubmissionWithCedar = false)', () => {
+ let component: CollectionsDiscoverComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ ({ fixture, component } = setup());
});
- store = TestBed.inject(Store);
- fixture = TestBed.createComponent(CollectionsDiscoverComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- }
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
- it('should create', () => {
- setup();
- expect(component).toBeTruthy();
- });
+ it('should set useShareTroveSearch to false', () => {
+ expect(component.useShareTroveSearch).toBe(false);
+ });
- it('should initialize searchControl with empty string', () => {
- setup();
- expect(component.searchControl.value).toBe('');
- });
+ it('should initialize with default values', () => {
+ expect(component.providerId()).toBe('provider-1');
+ expect(component.searchControl.value).toBe('');
+ });
- it('should navigate to not-found when providerId param is missing', () => {
- setup({ routeParams: {} });
- expect(routerMock.navigate).toHaveBeenCalledWith(['/not-found']);
- });
+ it('should have collection provider data', () => {
+ expect(component.collectionProvider()).toEqual(MOCK_COLLECTION_PROVIDER);
+ });
- it('should dispatch GetCollectionProvider when providerId is present', () => {
- setup({ routeParams: { providerId: 'my-provider' } });
- expect(store.dispatch).toHaveBeenCalledWith(new GetCollectionProvider('my-provider'));
- });
+ it('should have collection details as null', () => {
+ expect(component.collectionDetails()).toBeNull();
+ });
- it('should open help dialog with expected header', () => {
- setup();
- (store.dispatch as Mock).mockClear();
- component.openHelpDialog();
- expect(customDialogMock.open).toHaveBeenCalledWith(CollectionsHelpDialogComponent, {
- header: 'collections.helpDialog.header',
+ it('should have selected filters', () => {
+ expect(component.selectedFilters()).toEqual({});
});
- });
- it('should dispatch search and page when search is triggered in legacy mode', () => {
- setup();
- (store.dispatch as Mock).mockClear();
- component.onSearchTriggered('query');
- expect(store.dispatch).toHaveBeenCalledWith(new SetSearchValue('query'));
- expect(store.dispatch).toHaveBeenCalledWith(new SetPageNumber('1'));
- });
+ it('should have sort by value', () => {
+ expect(component.sortBy()).toBe('date');
+ });
- it('should not dispatch search actions when search is triggered in cedar mode', () => {
- setup({ useCedarEnvironment: true });
- (store.dispatch as Mock).mockClear();
- component.onSearchTriggered('query');
- expect(store.dispatch).not.toHaveBeenCalledWith(new SetSearchValue('query'));
- expect(store.dispatch).not.toHaveBeenCalledWith(new SetPageNumber('1'));
- });
+ it('should have search text', () => {
+ expect(component.searchText()).toBe('');
+ });
- it('should call query sync initialize and sync when legacy mode store fields are ready', () => {
- setup({
- selectorOverrides: [
- { selector: CollectionsSelectors.getCollectionProvider, value: createMockCollectionProvider() },
- { selector: CollectionsSelectors.getCollectionDetails, value: mockCollectionDetails },
- ],
+ it('should have page number', () => {
+ expect(component.pageNumber()).toBe('1');
});
- expect(querySyncMock.initializeFromUrl).toHaveBeenCalled();
- expect(querySyncMock.syncStoreToUrl).toHaveBeenCalledWith('', '', MOCK_COLLECTIONS_EMPTY_FILTERS, '1');
- });
- it('should dispatch search collection submissions when legacy prerequisites are met', () => {
- setup({
- selectorOverrides: [
- { selector: CollectionsSelectors.getCollectionProvider, value: createMockCollectionProvider() },
- { selector: CollectionsSelectors.getCollectionDetails, value: mockCollectionDetails },
- ],
+ it('should have loading state', () => {
+ expect(component.isProviderLoading()).toBe(false);
});
- expect(store.dispatch).toHaveBeenCalledWith(new SearchCollectionSubmissions(PROVIDER_ID, '', {}, '1', ''));
- });
- it('should apply branding when collection provider exposes brand', () => {
- const brand = {
- id: 'b1',
- name: 'B',
- heroLogoImageUrl: 'https://x/h.png',
- heroBackgroundImageUrl: 'https://x/hb.png',
- topNavLogoImageUrl: 'https://x/n.png',
- primaryColor: '#111111',
- secondaryColor: '#222222',
- backgroundColor: '#333333',
- };
- setup({
- selectorOverrides: [
- {
- selector: CollectionsSelectors.getCollectionProvider,
- value: createMockCollectionProvider({ brand }),
- },
- ],
+ it('should compute primary collection id', () => {
+ expect(component.primaryCollectionId()).toBe('collection-1');
});
- expect(brandServiceMock.applyBranding).toHaveBeenCalledWith(brand);
- expect(headerStyleServiceMock.applyHeaderStyles).toHaveBeenCalledWith('#222222', '#333333');
- });
- it('should dispatch GetCollectionDetails when primary collection id is available', () => {
- setup({
- selectorOverrides: [
- { selector: CollectionsSelectors.getCollectionProvider, value: createMockCollectionProvider() },
- ],
+ it('should handle search control value changes', () => {
+ component.searchControl.setValue('new search value');
+ expect(component.searchControl.value).toBe('new search value');
});
- expect(store.dispatch).toHaveBeenCalledWith(new GetCollectionDetails('col-1'));
- });
- it('should dispatch cedar default filters and extra filters when provider and template load', () => {
- const provider = createMockCollectionProvider({
- requiredMetadataTemplate: CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK,
+ it('should not initialize default search filters', () => {
+ expect(component.defaultSearchFiltersInitialized()).toBe(false);
});
- setup({
- useCedarEnvironment: true,
- selectorOverrides: [{ selector: CollectionsSelectors.getCollectionProvider, value: provider }],
+
+ it('should render CollectionsMainContentComponent', () => {
+ const el = fixture.nativeElement as HTMLElement;
+ expect(el.querySelector('osf-collections-main-content')).toBeTruthy();
+ expect(el.querySelector('osf-global-search')).toBeNull();
});
- expect(store.dispatch).toHaveBeenCalledWith(
- new SetDefaultFilterValue('isContainedBy', 'https://api.test.osf.io/v2/collections/col-1/')
- );
- expect(store.dispatch).toHaveBeenCalledWith(
- new SetExtraFilters([
- {
- key: 'Project Name',
- label: 'Project Name',
- operator: FilterOperatorOption.AnyOf,
- },
- ])
- );
- });
- it('should dispatch ResetSearchState on destroy in cedar mode', () => {
- setup({ useCedarEnvironment: true });
- (store.dispatch as Mock).mockClear();
- fixture.destroy();
- expect(store.dispatch).toHaveBeenCalledWith(expect.any(ResetSearchState));
- });
+ it('should dispatch setSearchValue and setPageNumber on search triggered', () => {
+ const { component: localComponent, store: localStore } = setup();
+ (localStore.dispatch as Mock).mockClear();
- it('should reset branding and header on destroy in browser', () => {
- setup();
- fixture.destroy();
- expect(headerStyleServiceMock.resetToDefaults).toHaveBeenCalled();
- expect(brandServiceMock.resetBranding).toHaveBeenCalled();
- });
+ localComponent.onSearchTriggered('my query');
- it('should not dispatch clear actions or reset services on destroy when not in browser', () => {
- setup({ platformId: 'server' });
- (store.dispatch as Mock).mockClear();
- brandServiceMock.resetBranding.mockClear();
- headerStyleServiceMock.resetToDefaults.mockClear();
- fixture.destroy();
- expect(store.dispatch).not.toHaveBeenCalled();
- expect(brandServiceMock.resetBranding).not.toHaveBeenCalled();
- expect(headerStyleServiceMock.resetToDefaults).not.toHaveBeenCalled();
+ const calls = (localStore.dispatch as Mock).mock.calls.flat();
+ expect(calls.some((c: unknown) => c instanceof SetDefaultFilterValue)).toBe(false);
+ });
});
- it('should debounce search control changes and dispatch trimmed search value', () => {
- vi.useFakeTimers();
- try {
- setup({
- selectorOverrides: [
- { selector: CollectionsSelectors.getCollectionProvider, value: createMockCollectionProvider() },
- { selector: CollectionsSelectors.getCollectionDetails, value: mockCollectionDetails },
- ],
+ describe('shtrove mode (collectionSubmissionWithCedar = true)', () => {
+ it('should set useShareTroveSearch to true', () => {
+ const { component } = setup({ collectionSubmissionWithCedar: true });
+ expect(component.useShareTroveSearch).toBe(true);
+ });
+
+ it('should initialize default search filters', () => {
+ const { component } = setup({ collectionSubmissionWithCedar: true });
+ expect(component.defaultSearchFiltersInitialized()).toBe(true);
+ });
+
+ it('should dispatch SetDefaultFilterValue with collection IRI', () => {
+ const { store } = setup({ collectionSubmissionWithCedar: true });
+ const dispatched = (store.dispatch as Mock).mock.calls.flat();
+ const setDefaultFilter = dispatched.find(
+ (c: unknown) => c instanceof SetDefaultFilterValue
+ ) as SetDefaultFilterValue;
+
+ expect(setDefaultFilter).toBeDefined();
+ expect(setDefaultFilter.filterKey).toBe('isContainedBy');
+ expect(setDefaultFilter.value).toBe('http://localhost:8000/v2/collections/collection-1/');
+ });
+
+ it('should not dispatch SetExtraFilters when provider has no requiredMetadataTemplate', () => {
+ const { store } = setup({ collectionSubmissionWithCedar: true });
+ const dispatched = (store.dispatch as Mock).mock.calls.flat();
+
+ expect(dispatched.some((c: unknown) => c instanceof SetExtraFilters)).toBe(false);
+ });
+
+ it('should dispatch SetExtraFilters when provider has a requiredMetadataTemplate', () => {
+ const { store } = setup({
+ collectionSubmissionWithCedar: true,
+ provider: MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE,
});
+
+ const dispatched = (store.dispatch as Mock).mock.calls.flat();
+ const setExtraFilters = dispatched.find((c: unknown) => c instanceof SetExtraFilters) as SetExtraFilters;
+
+ expect(setExtraFilters).toBeDefined();
+ expect(setExtraFilters.filters).toHaveLength(1);
+ expect(setExtraFilters.filters[0].key).toBe('field1');
+ expect(setExtraFilters.filters[0].label).toBe('Field One');
+ });
+
+ it('should render GlobalSearchComponent when filters are initialized', () => {
+ const { fixture } = setup({ collectionSubmissionWithCedar: true });
+ const el = fixture.nativeElement as HTMLElement;
+
+ expect(el.querySelector('osf-global-search')).toBeTruthy();
+ expect(el.querySelector('osf-collections-main-content')).toBeNull();
+ });
+
+ it('should not dispatch any action on onSearchTriggered in shtrove mode', () => {
+ const { component, store } = setup({ collectionSubmissionWithCedar: true });
(store.dispatch as Mock).mockClear();
- component.searchControl.setValue(' trimmed ');
- vi.advanceTimersByTime(300);
- expect(store.dispatch).toHaveBeenCalledWith(new SetSearchValue('trimmed'));
- } finally {
- vi.useRealTimers();
- }
+
+ component.onSearchTriggered('query');
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
});
});
diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.ts
index 0455286aa..af6994b7e 100644
--- a/src/app/features/collections/components/collections-discover/collections-discover.component.ts
+++ b/src/app/features/collections/components/collections-discover/collections-discover.component.ts
@@ -164,10 +164,12 @@ export class CollectionsDiscoverComponent {
private setupShareTroveSearchEffect(): void {
effect(() => {
const provider = this.collectionProvider();
+ const collectionId = this.primaryCollectionId();
- if (!provider || !provider.iri || this.defaultSearchFiltersInitialized()) return;
+ if (!provider || !collectionId || this.defaultSearchFiltersInitialized()) return;
- this.actions.setDefaultFilterValue('isContainedBy', provider.iri);
+ const collectionIri = `${this.environment.apiDomainUrl}/v2/collections/${collectionId}/`;
+ this.actions.setDefaultFilterValue('isContainedBy', collectionIri);
if (provider.requiredMetadataTemplate?.attributes?.template) {
const extraFilters = CedarTemplateFilterMapper.fromTemplate(
diff --git a/src/app/features/collections/store/add-to-collection/add-to-collection.state.ts b/src/app/features/collections/store/add-to-collection/add-to-collection.state.ts
index 718041d1e..04a1848c0 100644
--- a/src/app/features/collections/store/add-to-collection/add-to-collection.state.ts
+++ b/src/app/features/collections/store/add-to-collection/add-to-collection.state.ts
@@ -56,8 +56,8 @@ export class AddToCollectionState {
getCurrentCollectionSubmission(ctx: StateContext, action: GetCurrentCollectionSubmission) {
const state = ctx.getState();
ctx.patchState({
- currentProjectSubmission: {
- ...state.currentProjectSubmission,
+ collectionLicenses: {
+ ...state.collectionLicenses,
isLoading: true,
},
});
diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts
index a128bab01..032e378f3 100644
--- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts
+++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts
@@ -176,10 +176,7 @@ export class CedarTemplateFormComponent {
onSubmit() {
const editor = this.cedarEditor()?.nativeElement;
if (editor && typeof editor.currentMetadata !== 'undefined') {
- const cleanedData = CedarMetadataHelper.cleanMetadataForSubmission(
- editor.currentMetadata as Record
- );
- const finalData = { data: cleanedData, id: this.template().id, isPublished: this.isValid };
+ const finalData = { data: editor.currentMetadata, id: this.template().id, isPublished: this.isValid };
this.formData.set(finalData);
this.emitData.emit(finalData as CedarRecordDataBinding);
}
diff --git a/src/app/features/metadata/helpers/cedar-metadata.helper.spec.ts b/src/app/features/metadata/helpers/cedar-metadata.helper.spec.ts
deleted file mode 100644
index d5739c98f..000000000
--- a/src/app/features/metadata/helpers/cedar-metadata.helper.spec.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { CedarTemplate } from '../models';
-
-import { CedarMetadataHelper } from './cedar-metadata.helper';
-
-const MOCK_TEMPLATE: CedarTemplate = {
- '@id': 'https://repo.metadatacenter.org/templates/test-id',
- '@type': 'https://schema.metadatacenter.org/core/Template',
- type: 'object',
- title: 'Test Template',
- description: 'Test',
- $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: {} },
-};
-
-describe('CedarMetadataHelper', () => {
- describe('ensureProperStructure', () => {
- it('should return an empty array for non-array input', () => {
- expect(CedarMetadataHelper.ensureProperStructure(null)).toEqual([]);
- expect(CedarMetadataHelper.ensureProperStructure('string')).toEqual([]);
- expect(CedarMetadataHelper.ensureProperStructure({})).toEqual([]);
- });
-
- it('should normalize array items to have @id, @type, rdfs:label', () => {
- const input = [{ '@id': 'id1', '@type': 'type1', 'rdfs:label': 'label1' }];
- expect(CedarMetadataHelper.ensureProperStructure(input)).toEqual([
- { '@id': 'id1', '@type': 'type1', 'rdfs:label': 'label1' },
- ]);
- });
-
- it('should fill missing properties with defaults', () => {
- const input = [{}];
- expect(CedarMetadataHelper.ensureProperStructure(input)).toEqual([
- { '@id': '', '@type': '', 'rdfs:label': null },
- ]);
- });
- });
-
- describe('buildCedarSystemMetadata', () => {
- beforeEach(() => {
- vi.useFakeTimers();
- vi.setSystemTime(new Date('2025-01-15T10:00:00.000Z'));
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- it('should set @id to empty string', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['@id']).toBe('');
- });
-
- it('should set schema:isBasedOn to the template @id', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['schema:isBasedOn']).toBe('https://repo.metadatacenter.org/templates/test-id');
- });
-
- it('should set schema:name and schema:description to empty strings', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['schema:name']).toBe('');
- expect(result['schema:description']).toBe('');
- });
-
- it('should set pav:createdBy and oslc:modifiedBy to empty strings', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['pav:createdBy']).toBe('');
- expect(result['oslc:modifiedBy']).toBe('');
- });
-
- it('should set pav:createdOn and pav:lastUpdatedOn to the current timestamp', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['pav:createdOn']).toBe('2025-01-15T10:00:00.000Z');
- expect(result['pav:lastUpdatedOn']).toBe('2025-01-15T10:00:00.000Z');
- });
-
- it('should copy @context from the template', () => {
- const result = CedarMetadataHelper.buildCedarSystemMetadata(MOCK_TEMPLATE);
- expect(result['@context']).toEqual(MOCK_TEMPLATE['@context']);
- });
-
- it('should use empty object for @context when template has none', () => {
- const templateWithoutContext = { ...MOCK_TEMPLATE, '@context': undefined } as unknown as CedarTemplate;
- const result = CedarMetadataHelper.buildCedarSystemMetadata(templateWithoutContext);
- expect(result['@context']).toEqual({});
- });
-
- it('should use empty string for schema:isBasedOn when template @id is missing', () => {
- const templateWithoutId = { ...MOCK_TEMPLATE, '@id': undefined } as unknown as CedarTemplate;
- const result = CedarMetadataHelper.buildCedarSystemMetadata(templateWithoutId);
- expect(result['schema:isBasedOn']).toBe('');
- });
- });
-
- describe('buildEmptyMetadata', () => {
- it('should return an object with @context and LDbase-specific empty arrays', () => {
- const result = CedarMetadataHelper.buildEmptyMetadata();
- expect(result['@context']).toEqual({});
- expect(result['Constructs']).toEqual([]);
- expect(result['Assessments']).toEqual([]);
- });
- });
-
- describe('buildStructuredMetadata', () => {
- it('should return metadata as-is for keys not in the fix list', () => {
- const metadata = { customField: 'value' };
- expect(CedarMetadataHelper.buildStructuredMetadata(metadata)).toEqual({ customField: 'value' });
- });
-
- it('should normalize array fields in the fix list', () => {
- const metadata = { Constructs: [{ '@id': 'id1' }] };
- const result = CedarMetadataHelper.buildStructuredMetadata(metadata);
- expect(result['Constructs']).toEqual([{ '@id': 'id1', '@type': '', 'rdfs:label': null }]);
- });
- });
-
- describe('cleanMetadataForSubmission', () => {
- it('should pass through non-UUID top-level keys unchanged', () => {
- const metadata = { '@id': '', 'schema:name': '', 'School Type': { '@value': 'High School' } };
- expect(CedarMetadataHelper.cleanMetadataForSubmission(metadata)).toEqual(metadata);
- });
-
- it('should remove UUID-format top-level keys', () => {
- const metadata = {
- '@id': '',
- '052a3bf4-2003-42e4-bb38-a63e5e0fc0d3': { '@id': 'https://example.com' },
- 'School Type': { '@value': 'High School' },
- };
- const result = CedarMetadataHelper.cleanMetadataForSubmission(metadata);
- expect(result['052a3bf4-2003-42e4-bb38-a63e5e0fc0d3']).toBeUndefined();
- expect(result['@id']).toBe('');
- expect(result['School Type']).toEqual({ '@value': 'High School' });
- });
-
- it('should remove UUID-format keys from @context', () => {
- const metadata = {
- '@context': {
- pav: 'http://purl.org/pav/',
- 'schema:name': { '@type': 'xsd:string' },
- '052a3bf4-2003-42e4-bb38-a63e5e0fc0d3': 'https://repo.metadatacenter.org/template-fields/3de6ff2c',
- 'School Type': 'https://schema.metadatacenter.org/properties/abc',
- },
- '@id': '',
- };
- const result = CedarMetadataHelper.cleanMetadataForSubmission(metadata);
- const ctx = result['@context'] as Record;
- expect(ctx['052a3bf4-2003-42e4-bb38-a63e5e0fc0d3']).toBeUndefined();
- expect(ctx['pav']).toBe('http://purl.org/pav/');
- expect(ctx['School Type']).toBe('https://schema.metadatacenter.org/properties/abc');
- });
-
- it('should handle missing or null @context gracefully', () => {
- const metadata = { '@id': '', 'schema:name': '' };
- expect(() => CedarMetadataHelper.cleanMetadataForSubmission(metadata)).not.toThrow();
- });
- });
-});
diff --git a/src/app/features/metadata/helpers/cedar-metadata.helper.ts b/src/app/features/metadata/helpers/cedar-metadata.helper.ts
index b5bce0cd4..9ee0ecc35 100644
--- a/src/app/features/metadata/helpers/cedar-metadata.helper.ts
+++ b/src/app/features/metadata/helpers/cedar-metadata.helper.ts
@@ -1,21 +1,4 @@
-import { CedarTemplate } from '../models';
-
export class CedarMetadataHelper {
- static buildCedarSystemMetadata(template: CedarTemplate): Record {
- const now = new Date().toISOString();
- return {
- '@id': '',
- '@context': template['@context'] ?? {},
- 'schema:isBasedOn': template['@id'] ?? '',
- 'schema:name': '',
- 'schema:description': '',
- 'pav:createdBy': '',
- 'oslc:modifiedBy': '',
- 'pav:createdOn': now,
- 'pav:lastUpdatedOn': now,
- };
- }
-
static ensureProperStructure(items: unknown): Record[] {
if (!Array.isArray(items)) return [];
@@ -67,22 +50,4 @@ export class CedarMetadataHelper {
LDbaseInvestigatorORCID: this.ensureProperStructure([]),
};
}
-
- static cleanMetadataForSubmission(metadata: Record): Record {
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
- const cleaned: Record = {};
-
- for (const [key, value] of Object.entries(metadata)) {
- if (uuidRegex.test(key)) continue;
- if (key === '@context' && value && typeof value === 'object') {
- cleaned[key] = Object.fromEntries(
- Object.entries(value as Record).filter(([k]) => !uuidRegex.test(k))
- );
- } else {
- cleaned[key] = value;
- }
- }
-
- return cleaned;
- }
}
diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts
index 847f824d9..612a93311 100644
--- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts
+++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts
@@ -8,7 +8,6 @@ import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/col
import { CollectionsSelectors } from '@osf/shared/stores/collections';
import { DateAgoPipe } from '@shared/pipes/date-ago.pipe';
-import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock';
import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock';
import { provideOSFCore } from '@testing/osf.testing.provider';
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
@@ -143,59 +142,4 @@ describe('CollectionSubmissionItemComponent', () => {
const currentAction = component.currentReviewAction();
expect(currentAction).toBeNull();
});
-
- it('should open a new tab with serialized URL on handleNavigation', () => {
- const windowOpenSpy = vi.spyOn(window, 'open').mockReturnValue(null);
- fixture.componentRef.setInput('submission', mockSubmission);
- fixture.detectChanges();
-
- component.handleNavigation();
-
- expect(mockRouter.createUrlTree).toHaveBeenCalledWith(
- ['../', mockSubmission.nodeId],
- expect.objectContaining({ queryParams: { status: 'pending', mode: 'moderation' } })
- );
- expect(windowOpenSpy).toHaveBeenCalledWith('/', '_blank');
- });
-
- it('should emit loadContributors on handleOpen', () => {
- fixture.componentRef.setInput('submission', mockSubmission);
- fixture.detectChanges();
-
- const outputSpy = vi.fn();
- component.loadContributors.subscribe(outputSpy);
-
- component.handleOpen();
-
- expect(outputSpy).toHaveBeenCalled();
- });
-
- it('should return true for hasMoreContributors when loaded count is less than total', () => {
- fixture.componentRef.setInput('submission', {
- ...mockSubmission,
- contributors: [MOCK_CONTRIBUTOR],
- totalContributors: 3,
- });
- fixture.detectChanges();
-
- expect(component.hasMoreContributors()).toBe(true);
- });
-
- it('should return false for hasMoreContributors when all contributors are loaded', () => {
- fixture.componentRef.setInput('submission', {
- ...mockSubmission,
- contributors: [MOCK_CONTRIBUTOR],
- totalContributors: 1,
- });
- fixture.detectChanges();
-
- expect(component.hasMoreContributors()).toBe(false);
- });
-
- it('should return false for hasMoreContributors when contributors are not set', () => {
- fixture.componentRef.setInput('submission', mockSubmission);
- fixture.detectChanges();
-
- expect(component.hasMoreContributors()).toBe(false);
- });
});
diff --git a/src/app/shared/mappers/collections/collections.mapper.ts b/src/app/shared/mappers/collections/collections.mapper.ts
index d18c2ee96..cd7711c26 100644
--- a/src/app/shared/mappers/collections/collections.mapper.ts
+++ b/src/app/shared/mappers/collections/collections.mapper.ts
@@ -31,7 +31,6 @@ export class CollectionsMapper {
return {
id: response.id,
type: response.type,
- iri: response.links.iri,
name: replaceBadEncodedChars(response.attributes.name),
description: replaceBadEncodedChars(response.attributes.description),
advisoryBoard: response.attributes.advisory_board,
@@ -72,7 +71,7 @@ export class CollectionsMapper {
backgroundColor: response.embeds.brand.data.attributes.background_color,
}
: null,
- requiredMetadataTemplate: null,
+ requiredMetadataTemplate: response.embeds.required_metadata_template?.data ?? null,
};
}
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 fc6ce11b3..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';
@@ -9,15 +10,14 @@ import { UserDataErrorResponseJsonApi } from '../user/user-json-api.model';
export interface CollectionProviderResponseJsonApi {
id: string;
type: string;
- links: {
- iri: string;
- self: string;
- };
attributes: CollectionsProviderAttributesJsonApi;
embeds: {
brand: {
data?: BrandDataJsonApi;
};
+ required_metadata_template?: {
+ data?: CedarMetadataDataTemplateJsonApi | null;
+ };
};
relationships: {
primary_collection: {
@@ -26,12 +26,6 @@ export interface CollectionProviderResponseJsonApi {
type: 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 71c197222..ebecbbe80 100644
--- a/src/app/shared/models/collections/collections.model.ts
+++ b/src/app/shared/models/collections/collections.model.ts
@@ -8,7 +8,6 @@ import { ProjectModel } from '../projects/projects.model';
import { BaseProviderModel } from '../provider/provider.model';
export interface CollectionProvider extends BaseProviderModel {
- iri?: string;
assets: {
style?: string;
squareColorTransparent?: string;
diff --git a/src/app/shared/services/collections.service.ts b/src/app/shared/services/collections.service.ts
index ba97b566e..2f2bc8256 100644
--- a/src/app/shared/services/collections.service.ts
+++ b/src/app/shared/services/collections.service.ts
@@ -41,7 +41,6 @@ import { ReviewActionPayloadJsonApi } from '../models/review-action/review-actio
import { SetTotalSubmissions } from '../stores/collections/collections.actions';
import { JsonApiService } from './json-api.service';
-import { MetadataService } from './metadata.service';
@Injectable({
providedIn: 'root',
@@ -49,7 +48,6 @@ import { MetadataService } from './metadata.service';
export class CollectionsService {
private readonly jsonApiService = inject(JsonApiService);
private readonly environment = inject(ENVIRONMENT);
- private readonly metadataService = inject(MetadataService);
get apiUrl() {
return `${this.environment.apiDomainUrl}/v2`;
@@ -58,22 +56,11 @@ 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).pipe(
- switchMap((response) => {
- const provider = CollectionsMapper.fromGetCollectionProviderResponse(response.data);
- const templateId = response.data.relationships.required_metadata_template?.data?.id;
-
- if (!templateId) {
- return of(provider);
- }
-
- return this.metadataService
- .getCedarMetadataTemplateDetail(templateId)
- .pipe(map((template) => ({ ...provider, requiredMetadataTemplate: template })));
- })
- );
+ return this.jsonApiService
+ .get>(url)
+ .pipe(map((response) => CollectionsMapper.fromGetCollectionProviderResponse(response.data)));
}
getCollectionDetails(collectionId: string): Observable {
diff --git a/src/app/shared/services/metadata.service.ts b/src/app/shared/services/metadata.service.ts
index 6488cf11d..82c1bd357 100644
--- a/src/app/shared/services/metadata.service.ts
+++ b/src/app/shared/services/metadata.service.ts
@@ -6,7 +6,6 @@ import { inject, Injectable } from '@angular/core';
import { ENVIRONMENT } from '@core/provider/environment.provider';
import { CedarRecordsMapper, MetadataMapper, RorMapper } from '@osf/features/metadata/mappers';
import {
- CedarMetadataDataTemplateJsonApi,
CedarMetadataRecord,
CedarMetadataRecordJsonApi,
CedarMetadataTemplateJsonApi,
@@ -22,7 +21,6 @@ import {
} from '@osf/features/metadata/models';
import { ResourceType } from '../enums/resource-type.enum';
-import { JsonApiResponse } from '../models/common/json-api.model';
import { IdentifierModel } from '../models/identifiers/identifier.model';
import { LicenseOptions } from '../models/license/license.model';
import { BaseNodeAttributesJsonApi } from '../models/nodes/base-node-attributes-json-api.model';
@@ -104,14 +102,6 @@ export class MetadataService {
);
}
- getCedarMetadataTemplateDetail(templateId: string): Observable {
- return this.jsonApiService
- .get<
- JsonApiResponse
- >(`${this.apiDomainUrl}/_/cedar_metadata_templates/${templateId}/`)
- .pipe(map((response) => response.data));
- }
-
getMetadataCedarRecords(
resourceId: string,
resourceType: ResourceType,
diff --git a/src/app/shared/stores/global-search/global-search.state.ts b/src/app/shared/stores/global-search/global-search.state.ts
index bb94a2461..b20d061b4 100644
--- a/src/app/shared/stores/global-search/global-search.state.ts
+++ b/src/app/shared/stores/global-search/global-search.state.ts
@@ -276,15 +276,8 @@ export class GlobalSearchState {
private updateResourcesState(ctx: StateContext, response: ResourcesData) {
const { extraFilters } = ctx.getState();
- const seenKeys = new Set(response.filters.map((f) => f.key));
- const merged = [
- ...response.filters,
- ...extraFilters.filter((f) => {
- if (seenKeys.has(f.key)) return false;
- seenKeys.add(f.key);
- return true;
- }),
- ];
+ const apiFilterKeys = new Set(response.filters.map((f) => f.key));
+ const merged = [...response.filters, ...extraFilters.filter((f) => !apiFilterKeys.has(f.key))];
ctx.patchState({
resources: { data: response.resources, isLoading: false, error: null },