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 a3ca35636..0d590b06f 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 @@ -79,7 +79,19 @@ const MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE = { $schema: 'http://json-schema.org/draft-04/schema', '@context': {} as never, required: [], - properties: {}, + properties: { + '@context': { + properties: { + field1: { enum: ['https://schema.metadatacenter.org/properties/test-field-uuid'] }, + }, + }, + field1: { + '@type': 'https://schema.metadatacenter.org/core/TemplateField', + _valueConstraints: { + literals: [{ label: 'Option A' }, { label: 'Option B' }], + }, + }, + }, _ui: { order: ['field1'], propertyLabels: { field1: 'Field One' }, @@ -274,6 +286,8 @@ describe('CollectionsDiscoverComponent', () => { expect(setExtraFilters.filters).toHaveLength(1); expect(setExtraFilters.filters[0].key).toBe('field1'); expect(setExtraFilters.filters[0].label).toBe('Field One'); + expect(setExtraFilters.filters[0].cedarPropertyIri).toBe('test-field-uuid'); + expect(setExtraFilters.filters[0].options).toHaveLength(2); }); it('should render GlobalSearchComponent when filters are initialized', () => { diff --git a/src/app/features/metadata/models/cedar-metadata-template.model.ts b/src/app/features/metadata/models/cedar-metadata-template.model.ts index e75886006..fb2969382 100644 --- a/src/app/features/metadata/models/cedar-metadata-template.model.ts +++ b/src/app/features/metadata/models/cedar-metadata-template.model.ts @@ -10,6 +10,22 @@ export interface CedarMetadataDataTemplateJsonApi { }; } +export const CEDAR_TEMPLATE_FIELD_TYPE = 'https://schema.metadatacenter.org/core/TemplateField'; +export const CEDAR_PROPERTIES_BASE_IRI = 'https://schema.metadatacenter.org/properties/'; + +export interface CedarTemplateField { + '@type': string; + _valueConstraints?: { + literals?: { label: string }[]; + multipleChoice?: boolean; + requiredValue?: boolean; + }; +} + +export interface CedarTemplateContextSchema { + properties: Record; +} + export interface CedarTemplate { '@id': string; '@type': string; diff --git a/src/app/shared/components/generic-filter/generic-filter.component.html b/src/app/shared/components/generic-filter/generic-filter.component.html index 4288d45f2..f88ac85c8 100644 --- a/src/app/shared/components/generic-filter/generic-filter.component.html +++ b/src/app/shared/components/generic-filter/generic-filter.component.html @@ -27,7 +27,12 @@ (onLazyLoad)="loadMoreItems($event)" > -

{{ item.label }} ({{ item.cardSearchResultCount }})

+

+ {{ item.label }} + @if (item.cardSearchResultCount !== null) { + ({{ item.cardSearchResultCount }}) + } +

} diff --git a/src/app/shared/components/search-filters/search-filters.component.spec.ts b/src/app/shared/components/search-filters/search-filters.component.spec.ts index e28d4bdca..96ee5e76a 100644 --- a/src/app/shared/components/search-filters/search-filters.component.spec.ts +++ b/src/app/shared/components/search-filters/search-filters.component.spec.ts @@ -118,6 +118,40 @@ describe('SearchFiltersComponent', () => { expect(visibleFilters.length).toBe(3); }); + it('should show CEDAR filters that have options but no resultCount', () => { + const cedarFilter: DiscoverableFilter = { + key: 'School Type', + label: 'School Type', + operator: FilterOperatorOption.AnyOf, + cedarPropertyIri: 'uuid-school-type', + options: [ + { label: 'High School', value: 'High School', cardSearchResultCount: null }, + { label: 'Middle School', value: 'Middle School', cardSearchResultCount: null }, + ], + }; + + fixture.componentRef.setInput('filters', [cedarFilter]); + fixture.detectChanges(); + + expect(component.visibleFilters()).toHaveLength(1); + expect(component.visibleFilters()[0].key).toBe('School Type'); + }); + + it('should still hide a filter with resultCount 0 and no options', () => { + const zeroCountFilter: DiscoverableFilter = { + key: 'emptyFilter', + label: 'Empty', + operator: FilterOperatorOption.AnyOf, + resultCount: 0, + options: [], + }; + + fixture.componentRef.setInput('filters', [zeroCountFilter]); + fixture.detectChanges(); + + expect(component.visibleFilters()).toHaveLength(0); + }); + it('should compute splitFilters correctly', () => { fixture.componentRef.setInput('filters', mockFilters); fixture.detectChanges(); diff --git a/src/app/shared/components/search-filters/search-filters.component.ts b/src/app/shared/components/search-filters/search-filters.component.ts index c87370fc3..f7f8991f4 100644 --- a/src/app/shared/components/search-filters/search-filters.component.ts +++ b/src/app/shared/components/search-filters/search-filters.component.ts @@ -75,7 +75,7 @@ export class SearchFiltersComponent { return this.filters().filter((filter) => { if (!filter || !filter.key) return false; - return Boolean((filter.resultCount && filter.resultCount > 0) || (filter.options && filter.options.length > 0)); + return filter.resultCount === undefined || filter.resultCount > 0 || (filter.options?.length ?? 0) > 0; }); }); diff --git a/src/app/shared/mappers/filters/cedar-template-filter.mapper.spec.ts b/src/app/shared/mappers/filters/cedar-template-filter.mapper.spec.ts new file mode 100644 index 000000000..b37663180 --- /dev/null +++ b/src/app/shared/mappers/filters/cedar-template-filter.mapper.spec.ts @@ -0,0 +1,144 @@ +import { CEDAR_TEMPLATE_FIELD_TYPE, CedarTemplate } from '@osf/features/metadata/models'; +import { FilterOperatorOption } from '@osf/shared/models/search/discoverable-filter.model'; + +import { CedarTemplateFilterMapper } from './cedar-template-filter.mapper'; + +const CEDAR_BASE = 'https://schema.metadatacenter.org/properties/'; + +function makeTemplate(overrides: Partial = {}): CedarTemplate { + return { + '@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: { + '@context': { + properties: { + 'School Type': { enum: [`${CEDAR_BASE}uuid-school-type`] }, + 'Study Design': { enum: [`${CEDAR_BASE}uuid-study-design`] }, + About: { enum: [`${CEDAR_BASE}uuid-about`] }, + }, + }, + 'School Type': { + '@type': CEDAR_TEMPLATE_FIELD_TYPE, + _valueConstraints: { + literals: [{ label: 'High School' }, { label: 'Middle School' }], + }, + }, + 'Study Design': { + '@type': CEDAR_TEMPLATE_FIELD_TYPE, + _valueConstraints: { + literals: [{ label: 'Intervention' }, { label: 'Correlational' }], + }, + }, + About: { + '@type': 'https://schema.metadatacenter.org/core/StaticTemplateField', + _ui: { inputType: 'richtext' }, + }, + }, + _ui: { + order: ['School Type', 'Study Design', 'About'], + propertyLabels: { 'School Type': 'School Type', 'Study Design': 'Study Design', About: 'About' }, + propertyDescriptions: {}, + }, + ...overrides, + }; +} + +describe('CedarTemplateFilterMapper', () => { + describe('fromTemplate', () => { + it('should only include TemplateField entries with literals', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + + expect(filters).toHaveLength(2); + expect(filters.map((f) => f.key)).toEqual(['School Type', 'Study Design']); + }); + + it('should skip StaticTemplateField entries', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + + expect(filters.some((f) => f.key === 'About')).toBe(false); + }); + + it('should pre-populate options from _valueConstraints.literals', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + const schoolType = filters.find((f) => f.key === 'School Type')!; + + expect(schoolType.options).toHaveLength(2); + expect(schoolType.options![0]).toEqual({ + label: 'High School', + value: 'High School', + cardSearchResultCount: null, + }); + expect(schoolType.options![1]).toEqual({ + label: 'Middle School', + value: 'Middle School', + cardSearchResultCount: null, + }); + }); + + it('should set cardSearchResultCount to null for all options', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + + filters.forEach((f) => { + f.options?.forEach((opt) => { + expect(opt.cardSearchResultCount).toBeNull(); + }); + }); + }); + + it('should set cedarPropertyIri to the UUID from the context IRI', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + const schoolType = filters.find((f) => f.key === 'School Type')!; + const studyDesign = filters.find((f) => f.key === 'Study Design')!; + + expect(schoolType.cedarPropertyIri).toBe('uuid-school-type'); + expect(studyDesign.cedarPropertyIri).toBe('uuid-study-design'); + }); + + it('should set operator to AnyOf', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + + filters.forEach((f) => { + expect(f.operator).toBe(FilterOperatorOption.AnyOf); + }); + }); + + it('should use propertyLabels for the filter label', () => { + const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate()); + + expect(filters[0].label).toBe('School Type'); + expect(filters[1].label).toBe('Study Design'); + }); + + it('should skip fields with no literals', () => { + const template = makeTemplate(); + (template.properties['School Type'] as any)._valueConstraints = { literals: [] }; + + const filters = CedarTemplateFilterMapper.fromTemplate(template); + + expect(filters.some((f) => f.key === 'School Type')).toBe(false); + }); + + it('should skip fields with an empty label', () => { + const template = makeTemplate(); + template._ui.propertyLabels['School Type'] = ' '; + + const filters = CedarTemplateFilterMapper.fromTemplate(template); + + expect(filters.some((f) => f.key === 'School Type')).toBe(false); + }); + + it('should return an empty array when no filterable fields exist', () => { + const template = makeTemplate({ + _ui: { order: ['About'], propertyLabels: { About: 'About' }, propertyDescriptions: {} }, + }); + + expect(CedarTemplateFilterMapper.fromTemplate(template)).toEqual([]); + }); + }); +}); diff --git a/src/app/shared/mappers/filters/cedar-template-filter.mapper.ts b/src/app/shared/mappers/filters/cedar-template-filter.mapper.ts index 56a632e09..de0d473f3 100644 --- a/src/app/shared/mappers/filters/cedar-template-filter.mapper.ts +++ b/src/app/shared/mappers/filters/cedar-template-filter.mapper.ts @@ -1,16 +1,47 @@ -import { CedarTemplate } from '@osf/features/metadata/models'; -import { DiscoverableFilter, FilterOperatorOption } from '@osf/shared/models/search/discoverable-filter.model'; +import { + CEDAR_PROPERTIES_BASE_IRI, + CEDAR_TEMPLATE_FIELD_TYPE, + CedarTemplate, + CedarTemplateContextSchema, + CedarTemplateField, +} from '@osf/features/metadata/models'; +import { + DiscoverableFilter, + FilterOperatorOption, + FilterOption, +} from '@osf/shared/models/search/discoverable-filter.model'; export class CedarTemplateFilterMapper { static fromTemplate(template: CedarTemplate): DiscoverableFilter[] { const { order, propertyLabels } = template._ui; + const contextProperties = (template.properties['@context'] as CedarTemplateContextSchema)?.properties ?? {}; return order - .filter((key) => propertyLabels[key]?.trim()) - .map((key) => ({ - key, - label: propertyLabels[key], - operator: FilterOperatorOption.AnyOf, - })); + .filter((key) => { + const field = template.properties[key] as CedarTemplateField | undefined; + return ( + propertyLabels[key]?.trim() && + field?.['@type'] === CEDAR_TEMPLATE_FIELD_TYPE && + (field._valueConstraints?.literals?.length ?? 0) > 0 + ); + }) + .map((key) => { + const field = template.properties[key] as CedarTemplateField; + const iri = contextProperties[key]?.enum?.[0]; + const cedarPropertyIri = iri?.replace(CEDAR_PROPERTIES_BASE_IRI, ''); + const options: FilterOption[] = (field._valueConstraints!.literals ?? []).map((literal) => ({ + label: literal.label, + value: literal.label, + cardSearchResultCount: null, + })); + + return { + key, + label: propertyLabels[key], + operator: FilterOperatorOption.AnyOf, + options, + cedarPropertyIri, + }; + }); } } diff --git a/src/app/shared/models/search/discoverable-filter.model.ts b/src/app/shared/models/search/discoverable-filter.model.ts index fb313ee49..005f9b1dc 100644 --- a/src/app/shared/models/search/discoverable-filter.model.ts +++ b/src/app/shared/models/search/discoverable-filter.model.ts @@ -11,6 +11,7 @@ export interface DiscoverableFilter { isLoaded?: boolean; isPaginationLoading?: boolean; isSearchLoading?: boolean; + cedarPropertyIri?: string; } export enum FilterOperatorOption { @@ -22,5 +23,5 @@ export enum FilterOperatorOption { export interface FilterOption { label: string; value: string; - cardSearchResultCount: number; + cardSearchResultCount: number | null; } diff --git a/src/app/shared/stores/global-search/global-search.state.spec.ts b/src/app/shared/stores/global-search/global-search.state.spec.ts new file mode 100644 index 000000000..b43908615 --- /dev/null +++ b/src/app/shared/stores/global-search/global-search.state.spec.ts @@ -0,0 +1,242 @@ +import { provideStore, Store } from '@ngxs/store'; + +import { EMPTY, of } from 'rxjs'; + +import { vi } from 'vitest'; + +import { TestBed } from '@angular/core/testing'; + +import { + DiscoverableFilter, + FilterOperatorOption, + FilterOption, +} from '@osf/shared/models/search/discoverable-filter.model'; +import { GlobalSearchService } from '@osf/shared/services/global-search.service'; +import { ResourcesData } from '@shared/models/search/resource.model'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; + +import { + FetchResources, + LoadFilterOptions, + LoadFilterOptionsAndSetValues, + SetExtraFilters, +} from './global-search.actions'; +import { GlobalSearchSelectors } from './global-search.selectors'; +import { GlobalSearchState } from './global-search.state'; + +const MOCK_RESOURCES_DATA: ResourcesData = { + resources: [], + filters: [], + count: 0, + self: '', + first: null, + next: null, + previous: null, +}; + +const CEDAR_FILTER: DiscoverableFilter = { + key: 'School Type', + label: 'School Type', + operator: FilterOperatorOption.AnyOf, + cedarPropertyIri: 'uuid-school-type', + options: [ + { label: 'High School', value: 'High School', cardSearchResultCount: null }, + { label: 'Middle School', value: 'Middle School', cardSearchResultCount: null }, + ], +}; + +const REGULAR_FILTER: DiscoverableFilter = { + key: 'subject', + label: 'Subject', + operator: FilterOperatorOption.AnyOf, + resultCount: 10, +}; + +function setup() { + const mockGetResources = vi.fn().mockReturnValue(of(MOCK_RESOURCES_DATA)); + const mockGetFilterOptions = vi.fn().mockReturnValue(of({ options: [], nextUrl: undefined })); + + TestBed.configureTestingModule({ + providers: [ + provideOSFCore(), + provideStore([GlobalSearchState]), + { + provide: GlobalSearchService, + useValue: { + getResources: mockGetResources, + getFilterOptions: mockGetFilterOptions, + getResourcesByLink: vi.fn().mockReturnValue(EMPTY), + getFilterOptionsFromPaginationUrl: vi.fn().mockReturnValue(EMPTY), + }, + }, + ], + }); + + return { + store: TestBed.inject(Store), + mockGetResources, + mockGetFilterOptions, + }; +} + +describe('GlobalSearchState', () => { + describe('LoadFilterOptions', () => { + it('should skip the API call for a CEDAR filter (cedarPropertyIri set)', () => { + const { store, mockGetFilterOptions } = setup(); + + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + store.dispatch(new FetchResources()); + store.dispatch(new LoadFilterOptions(CEDAR_FILTER.key)); + + expect(mockGetFilterOptions).not.toHaveBeenCalled(); + }); + + it('should set isLoaded to true for a CEDAR filter when short-circuiting', () => { + const { store } = setup(); + + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + store.dispatch(new FetchResources()); + store.dispatch(new LoadFilterOptions(CEDAR_FILTER.key)); + + const filters = store.selectSnapshot(GlobalSearchSelectors.getFilters); + const cedarFilterState = filters.find((f) => f.key === CEDAR_FILTER.key); + expect(cedarFilterState?.isLoaded).toBe(true); + }); + + it('should call the API for a regular filter', () => { + const { store, mockGetFilterOptions } = setup(); + + store.dispatch(new FetchResources()); + store.dispatch(new LoadFilterOptions(REGULAR_FILTER.key)); + + expect(mockGetFilterOptions).toHaveBeenCalled(); + const params = mockGetFilterOptions.mock.calls[0][0]; + expect(params['valueSearchPropertyPath']).toBe(REGULAR_FILTER.key); + }); + }); + + describe('LoadFilterOptionsAndSetValues', () => { + it('should not call the API for CEDAR filter keys', () => { + const { store, mockGetFilterOptions } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + + store.dispatch( + new LoadFilterOptionsAndSetValues({ + [CEDAR_FILTER.key]: [{ label: 'High School', value: 'High School', cardSearchResultCount: null }], + }) + ); + + expect(mockGetFilterOptions).not.toHaveBeenCalled(); + }); + + it('should still set selectedFilterOptions for CEDAR keys', () => { + const { store } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + + const selectedOption: FilterOption = { label: 'High School', value: 'High School', cardSearchResultCount: null }; + store.dispatch(new LoadFilterOptionsAndSetValues({ [CEDAR_FILTER.key]: [selectedOption] })); + + const selected = store.selectSnapshot(GlobalSearchSelectors.getSelectedOptions); + expect(selected[CEDAR_FILTER.key]).toEqual([selectedOption]); + }); + + it('should only call the API for non-CEDAR keys in a mixed payload', () => { + const { store, mockGetFilterOptions } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + + store.dispatch( + new LoadFilterOptionsAndSetValues({ + [CEDAR_FILTER.key]: [{ label: 'High School', value: 'High School', cardSearchResultCount: null }], + [REGULAR_FILTER.key]: [{ label: 'Biology', value: 'biology', cardSearchResultCount: 5 }], + }) + ); + + expect(mockGetFilterOptions).toHaveBeenCalledTimes(1); + const params = mockGetFilterOptions.mock.calls[0][0]; + expect(params['valueSearchPropertyPath']).toBe(REGULAR_FILTER.key); + }); + }); + + describe('FetchResources (CEDAR filter params)', () => { + it('should add iriShorthand[cedar] when extraFilters are present', () => { + const { store, mockGetResources } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + + store.dispatch(new FetchResources()); + + const params = mockGetResources.mock.calls[0][0]; + expect(params['iriShorthand[cedar]']).toBe('https://schema.metadatacenter.org/properties/'); + }); + + it('should not add iriShorthand[cedar] when no extraFilters are present', () => { + const { store, mockGetResources } = setup(); + + store.dispatch(new FetchResources()); + + const params = mockGetResources.mock.calls[0][0]; + expect(params['iriShorthand[cedar]']).toBeUndefined(); + }); + + it('should use cardSearchText for a selected CEDAR filter value', () => { + const { store, mockGetResources } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + store.dispatch(new FetchResources()); // populates state.filters via updateResourcesState + mockGetResources.mockClear(); + + store.dispatch( + new LoadFilterOptionsAndSetValues({ + [CEDAR_FILTER.key]: [{ label: 'High School', value: 'High School', cardSearchResultCount: null }], + }) + ); + store.dispatch(new FetchResources()); + + const params = mockGetResources.mock.calls[0][0]; + expect(params[`cardSearchText[osf:hasCedarRecord.cedar:${CEDAR_FILTER.cedarPropertyIri}][]`]).toEqual([ + '"High School"', + ]); + expect(params[`cardSearchFilter[${CEDAR_FILTER.key}][]`]).toBeUndefined(); + }); + + it('should include all selected values for a CEDAR filter', () => { + const { store, mockGetResources } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + store.dispatch(new FetchResources()); + mockGetResources.mockClear(); + + store.dispatch( + new LoadFilterOptionsAndSetValues({ + [CEDAR_FILTER.key]: [ + { label: 'High School', value: 'High School', cardSearchResultCount: null }, + { label: 'Middle School', value: 'Middle School', cardSearchResultCount: null }, + ], + }) + ); + store.dispatch(new FetchResources()); + + const params = mockGetResources.mock.calls[0][0]; + expect(params[`cardSearchText[osf:hasCedarRecord.cedar:${CEDAR_FILTER.cedarPropertyIri}][]`]).toEqual([ + '"High School"', + '"Middle School"', + ]); + }); + + it('should use extraFilters as fallback for CEDAR lookup before state.filters is populated', () => { + const { store, mockGetResources } = setup(); + store.dispatch(new SetExtraFilters([CEDAR_FILTER])); + + store.dispatch( + new LoadFilterOptionsAndSetValues({ + [CEDAR_FILTER.key]: [{ label: 'High School', value: 'High School', cardSearchResultCount: null }], + }) + ); + // First FetchResources — state.filters is still empty at this point + store.dispatch(new FetchResources()); + + const params = mockGetResources.mock.calls[0][0]; + expect(params[`cardSearchText[osf:hasCedarRecord.cedar:${CEDAR_FILTER.cedarPropertyIri}][]`]).toEqual([ + '"High School"', + ]); + }); + }); +}); 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 b20d061b4..5e80a3541 100644 --- a/src/app/shared/stores/global-search/global-search.state.ts +++ b/src/app/shared/stores/global-search/global-search.state.ts @@ -62,6 +62,15 @@ export class GlobalSearchState { loadFilterOptions(ctx: StateContext, action: LoadFilterOptions) { const state = ctx.getState(); const filterKey = action.filterKey; + + const filter = state.filters.find((f) => f.key === filterKey); + if (filter?.cedarPropertyIri) { + ctx.patchState({ + filters: state.filters.map((f) => (f.key === filterKey ? { ...f, isLoaded: true } : f)), + }); + return EMPTY; + } + const cachedOptions = state.filterOptionsCache[filterKey]; if (cachedOptions?.length) { const updatedFilters = state.filters.map((f) => @@ -204,7 +213,12 @@ export class GlobalSearchState { ctx.patchState({ filters: loadingFilters }); ctx.patchState({ selectedFilterOptions: filterValues }); - const observables = filterKeys.map((key) => + const cedarKeys = new Set(ctx.getState().extraFilters.map((f) => f.key)); + const nonCedarKeys = filterKeys.filter((key) => !cedarKeys.has(key)); + + if (!nonCedarKeys.length) return; + + const observables = nonCedarKeys.map((key) => this.searchService.getFilterOptions(this.buildParamsForIndexValueSearch(ctx.getState(), key)).pipe( tap((response) => { const options = response.options; @@ -310,20 +324,34 @@ export class GlobalSearchState { Object.entries(state.defaultFilterOptions).forEach(([key, value]) => { filtersParams[`cardSearchFilter[${key}][]`] = value; }); + let hasCedarFilters = state.extraFilters.length > 0; + Object.entries(state.selectedFilterOptions).forEach(([key, options]) => { - const filter = state.filters.find((f) => f.key === key); + const filter = state.filters.find((f) => f.key === key) ?? state.extraFilters.find((f) => f.key === key); - const firstOptionValue = options[0]?.value; - const isOptionValueBoolean = firstOptionValue === 'true' || firstOptionValue === 'false'; - if (filter?.operator === FilterOperatorOption.IsPresent || isOptionValueBoolean) { - if (firstOptionValue) { - filtersParams[`cardSearchFilter[${key}][is-present]`] = firstOptionValue; + if (filter?.cedarPropertyIri) { + hasCedarFilters = true; + const values = options.map((o) => `"${o.value}"`); + if (values.length) { + filtersParams[`cardSearchText[osf:hasCedarRecord.cedar:${filter.cedarPropertyIri}][]`] = values; } } else { - filtersParams[`cardSearchFilter[${key}][]`] = options.map((option) => option.value); + const firstOptionValue = options[0]?.value; + const isOptionValueBoolean = firstOptionValue === 'true' || firstOptionValue === 'false'; + if (filter?.operator === FilterOperatorOption.IsPresent || isOptionValueBoolean) { + if (firstOptionValue) { + filtersParams[`cardSearchFilter[${key}][is-present]`] = firstOptionValue; + } + } else { + filtersParams[`cardSearchFilter[${key}][]`] = options.map((option) => option.value); + } } }); + if (hasCedarFilters) { + filtersParams['iriShorthand[cedar]'] = 'https://schema.metadatacenter.org/properties/'; + } + filtersParams['cardSearchFilter[resourceType]'] = getResourceTypeStringFromEnum(state.resourceType); filtersParams['cardSearchFilter[accessService]'] = `${this.environment.webUrl}/`; filtersParams['cardSearchText[*,creator.name,isContainedBy.creator.name]'] = state.searchText