Skip to content

Commit 70a3871

Browse files
committed
feat(es2): Fix collection discover page facet
1 parent af7caca commit 70a3871

10 files changed

Lines changed: 493 additions & 20 deletions

File tree

src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,19 @@ const MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE = {
7979
$schema: 'http://json-schema.org/draft-04/schema',
8080
'@context': {} as never,
8181
required: [],
82-
properties: {},
82+
properties: {
83+
'@context': {
84+
properties: {
85+
field1: { enum: ['https://schema.metadatacenter.org/properties/test-field-uuid'] },
86+
},
87+
},
88+
field1: {
89+
'@type': 'https://schema.metadatacenter.org/core/TemplateField',
90+
_valueConstraints: {
91+
literals: [{ label: 'Option A' }, { label: 'Option B' }],
92+
},
93+
},
94+
},
8395
_ui: {
8496
order: ['field1'],
8597
propertyLabels: { field1: 'Field One' },
@@ -274,6 +286,8 @@ describe('CollectionsDiscoverComponent', () => {
274286
expect(setExtraFilters.filters).toHaveLength(1);
275287
expect(setExtraFilters.filters[0].key).toBe('field1');
276288
expect(setExtraFilters.filters[0].label).toBe('Field One');
289+
expect(setExtraFilters.filters[0].cedarPropertyIri).toBe('test-field-uuid');
290+
expect(setExtraFilters.filters[0].options).toHaveLength(2);
277291
});
278292

279293
it('should render GlobalSearchComponent when filters are initialized', () => {

src/app/features/metadata/models/cedar-metadata-template.model.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ export interface CedarMetadataDataTemplateJsonApi {
1010
};
1111
}
1212

13+
export const CEDAR_TEMPLATE_FIELD_TYPE = 'https://schema.metadatacenter.org/core/TemplateField';
14+
export const CEDAR_PROPERTIES_BASE_IRI = 'https://schema.metadatacenter.org/properties/';
15+
16+
export interface CedarTemplateField {
17+
'@type': string;
18+
_valueConstraints?: {
19+
literals?: { label: string }[];
20+
multipleChoice?: boolean;
21+
requiredValue?: boolean;
22+
};
23+
}
24+
25+
export interface CedarTemplateContextSchema {
26+
properties: Record<string, { enum?: string[] }>;
27+
}
28+
1329
export interface CedarTemplate {
1430
'@id': string;
1531
'@type': string;

src/app/shared/components/generic-filter/generic-filter.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727
(onLazyLoad)="loadMoreItems($event)"
2828
>
2929
<ng-template #item let-item>
30-
<p class="text-base">{{ item.label }} ({{ item.cardSearchResultCount }})</p>
30+
<p class="text-base">
31+
{{ item.label }}
32+
@if (item.cardSearchResultCount !== null) {
33+
({{ item.cardSearchResultCount }})
34+
}
35+
</p>
3136
</ng-template>
3237
</p-multiSelect>
3338
}

src/app/shared/components/search-filters/search-filters.component.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,40 @@ describe('SearchFiltersComponent', () => {
118118
expect(visibleFilters.length).toBe(3);
119119
});
120120

121+
it('should show CEDAR filters that have options but no resultCount', () => {
122+
const cedarFilter: DiscoverableFilter = {
123+
key: 'School Type',
124+
label: 'School Type',
125+
operator: FilterOperatorOption.AnyOf,
126+
cedarPropertyIri: 'uuid-school-type',
127+
options: [
128+
{ label: 'High School', value: 'High School', cardSearchResultCount: null },
129+
{ label: 'Middle School', value: 'Middle School', cardSearchResultCount: null },
130+
],
131+
};
132+
133+
fixture.componentRef.setInput('filters', [cedarFilter]);
134+
fixture.detectChanges();
135+
136+
expect(component.visibleFilters()).toHaveLength(1);
137+
expect(component.visibleFilters()[0].key).toBe('School Type');
138+
});
139+
140+
it('should still hide a filter with resultCount 0 and no options', () => {
141+
const zeroCountFilter: DiscoverableFilter = {
142+
key: 'emptyFilter',
143+
label: 'Empty',
144+
operator: FilterOperatorOption.AnyOf,
145+
resultCount: 0,
146+
options: [],
147+
};
148+
149+
fixture.componentRef.setInput('filters', [zeroCountFilter]);
150+
fixture.detectChanges();
151+
152+
expect(component.visibleFilters()).toHaveLength(0);
153+
});
154+
121155
it('should compute splitFilters correctly', () => {
122156
fixture.componentRef.setInput('filters', mockFilters);
123157
fixture.detectChanges();

src/app/shared/components/search-filters/search-filters.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class SearchFiltersComponent {
7575
return this.filters().filter((filter) => {
7676
if (!filter || !filter.key) return false;
7777

78-
return Boolean((filter.resultCount && filter.resultCount > 0) || (filter.options && filter.options.length > 0));
78+
return filter.resultCount === undefined || filter.resultCount > 0 || (filter.options?.length ?? 0) > 0;
7979
});
8080
});
8181

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { CEDAR_TEMPLATE_FIELD_TYPE, CedarTemplate } from '@osf/features/metadata/models';
2+
import { FilterOperatorOption } from '@osf/shared/models/search/discoverable-filter.model';
3+
4+
import { CedarTemplateFilterMapper } from './cedar-template-filter.mapper';
5+
6+
const CEDAR_BASE = 'https://schema.metadatacenter.org/properties/';
7+
8+
function makeTemplate(overrides: Partial<CedarTemplate> = {}): CedarTemplate {
9+
return {
10+
'@id': 'https://repo.metadatacenter.org/templates/test',
11+
'@type': 'https://schema.metadatacenter.org/core/Template',
12+
type: 'object',
13+
title: 'Test',
14+
description: '',
15+
$schema: 'http://json-schema.org/draft-04/schema',
16+
'@context': {} as never,
17+
required: [],
18+
properties: {
19+
'@context': {
20+
properties: {
21+
'School Type': { enum: [`${CEDAR_BASE}uuid-school-type`] },
22+
'Study Design': { enum: [`${CEDAR_BASE}uuid-study-design`] },
23+
About: { enum: [`${CEDAR_BASE}uuid-about`] },
24+
},
25+
},
26+
'School Type': {
27+
'@type': CEDAR_TEMPLATE_FIELD_TYPE,
28+
_valueConstraints: {
29+
literals: [{ label: 'High School' }, { label: 'Middle School' }],
30+
},
31+
},
32+
'Study Design': {
33+
'@type': CEDAR_TEMPLATE_FIELD_TYPE,
34+
_valueConstraints: {
35+
literals: [{ label: 'Intervention' }, { label: 'Correlational' }],
36+
},
37+
},
38+
About: {
39+
'@type': 'https://schema.metadatacenter.org/core/StaticTemplateField',
40+
_ui: { inputType: 'richtext' },
41+
},
42+
},
43+
_ui: {
44+
order: ['School Type', 'Study Design', 'About'],
45+
propertyLabels: { 'School Type': 'School Type', 'Study Design': 'Study Design', About: 'About' },
46+
propertyDescriptions: {},
47+
},
48+
...overrides,
49+
};
50+
}
51+
52+
describe('CedarTemplateFilterMapper', () => {
53+
describe('fromTemplate', () => {
54+
it('should only include TemplateField entries with literals', () => {
55+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
56+
57+
expect(filters).toHaveLength(2);
58+
expect(filters.map((f) => f.key)).toEqual(['School Type', 'Study Design']);
59+
});
60+
61+
it('should skip StaticTemplateField entries', () => {
62+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
63+
64+
expect(filters.some((f) => f.key === 'About')).toBe(false);
65+
});
66+
67+
it('should pre-populate options from _valueConstraints.literals', () => {
68+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
69+
const schoolType = filters.find((f) => f.key === 'School Type')!;
70+
71+
expect(schoolType.options).toHaveLength(2);
72+
expect(schoolType.options![0]).toEqual({
73+
label: 'High School',
74+
value: 'High School',
75+
cardSearchResultCount: null,
76+
});
77+
expect(schoolType.options![1]).toEqual({
78+
label: 'Middle School',
79+
value: 'Middle School',
80+
cardSearchResultCount: null,
81+
});
82+
});
83+
84+
it('should set cardSearchResultCount to null for all options', () => {
85+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
86+
87+
filters.forEach((f) => {
88+
f.options?.forEach((opt) => {
89+
expect(opt.cardSearchResultCount).toBeNull();
90+
});
91+
});
92+
});
93+
94+
it('should set cedarPropertyIri to the UUID from the context IRI', () => {
95+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
96+
const schoolType = filters.find((f) => f.key === 'School Type')!;
97+
const studyDesign = filters.find((f) => f.key === 'Study Design')!;
98+
99+
expect(schoolType.cedarPropertyIri).toBe('uuid-school-type');
100+
expect(studyDesign.cedarPropertyIri).toBe('uuid-study-design');
101+
});
102+
103+
it('should set operator to AnyOf', () => {
104+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
105+
106+
filters.forEach((f) => {
107+
expect(f.operator).toBe(FilterOperatorOption.AnyOf);
108+
});
109+
});
110+
111+
it('should use propertyLabels for the filter label', () => {
112+
const filters = CedarTemplateFilterMapper.fromTemplate(makeTemplate());
113+
114+
expect(filters[0].label).toBe('School Type');
115+
expect(filters[1].label).toBe('Study Design');
116+
});
117+
118+
it('should skip fields with no literals', () => {
119+
const template = makeTemplate();
120+
(template.properties['School Type'] as any)._valueConstraints = { literals: [] };
121+
122+
const filters = CedarTemplateFilterMapper.fromTemplate(template);
123+
124+
expect(filters.some((f) => f.key === 'School Type')).toBe(false);
125+
});
126+
127+
it('should skip fields with an empty label', () => {
128+
const template = makeTemplate();
129+
template._ui.propertyLabels['School Type'] = ' ';
130+
131+
const filters = CedarTemplateFilterMapper.fromTemplate(template);
132+
133+
expect(filters.some((f) => f.key === 'School Type')).toBe(false);
134+
});
135+
136+
it('should return an empty array when no filterable fields exist', () => {
137+
const template = makeTemplate({
138+
_ui: { order: ['About'], propertyLabels: { About: 'About' }, propertyDescriptions: {} },
139+
});
140+
141+
expect(CedarTemplateFilterMapper.fromTemplate(template)).toEqual([]);
142+
});
143+
});
144+
});
Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,47 @@
1-
import { CedarTemplate } from '@osf/features/metadata/models';
2-
import { DiscoverableFilter, FilterOperatorOption } from '@osf/shared/models/search/discoverable-filter.model';
1+
import {
2+
CEDAR_PROPERTIES_BASE_IRI,
3+
CEDAR_TEMPLATE_FIELD_TYPE,
4+
CedarTemplate,
5+
CedarTemplateContextSchema,
6+
CedarTemplateField,
7+
} from '@osf/features/metadata/models';
8+
import {
9+
DiscoverableFilter,
10+
FilterOperatorOption,
11+
FilterOption,
12+
} from '@osf/shared/models/search/discoverable-filter.model';
313

414
export class CedarTemplateFilterMapper {
515
static fromTemplate(template: CedarTemplate): DiscoverableFilter[] {
616
const { order, propertyLabels } = template._ui;
17+
const contextProperties = (template.properties['@context'] as CedarTemplateContextSchema)?.properties ?? {};
718

819
return order
9-
.filter((key) => propertyLabels[key]?.trim())
10-
.map((key) => ({
11-
key,
12-
label: propertyLabels[key],
13-
operator: FilterOperatorOption.AnyOf,
14-
}));
20+
.filter((key) => {
21+
const field = template.properties[key] as CedarTemplateField | undefined;
22+
return (
23+
propertyLabels[key]?.trim() &&
24+
field?.['@type'] === CEDAR_TEMPLATE_FIELD_TYPE &&
25+
(field._valueConstraints?.literals?.length ?? 0) > 0
26+
);
27+
})
28+
.map((key) => {
29+
const field = template.properties[key] as CedarTemplateField;
30+
const iri = contextProperties[key]?.enum?.[0];
31+
const cedarPropertyIri = iri?.replace(CEDAR_PROPERTIES_BASE_IRI, '');
32+
const options: FilterOption[] = (field._valueConstraints!.literals ?? []).map((literal) => ({
33+
label: literal.label,
34+
value: literal.label,
35+
cardSearchResultCount: null,
36+
}));
37+
38+
return {
39+
key,
40+
label: propertyLabels[key],
41+
operator: FilterOperatorOption.AnyOf,
42+
options,
43+
cedarPropertyIri,
44+
};
45+
});
1546
}
1647
}

src/app/shared/models/search/discoverable-filter.model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface DiscoverableFilter {
1111
isLoaded?: boolean;
1212
isPaginationLoading?: boolean;
1313
isSearchLoading?: boolean;
14+
cedarPropertyIri?: string;
1415
}
1516

1617
export enum FilterOperatorOption {
@@ -22,5 +23,5 @@ export enum FilterOperatorOption {
2223
export interface FilterOption {
2324
label: string;
2425
value: string;
25-
cardSearchResultCount: number;
26+
cardSearchResultCount: number | null;
2627
}

0 commit comments

Comments
 (0)