Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
parseFundingIds,
parseGroupBy,
parsePeopleIds,
parseProjects,
parsePublicationIds,
parseSearch,
parseSortBy,
Expand All @@ -16,6 +17,7 @@ import {
serializeEventIds,
serializeFundingIds,
serializePeopleIds,
serializeProjects,
serializePublicationIds,
serializeYears,
} from './serialization';
Expand Down Expand Up @@ -55,6 +57,7 @@ export const ResearchStore = signalStore(
const funding = createWritableStateSlice(store.fundingIds, store.setFundingIds);
const publications = createWritableStateSlice(store.publicationIds, store.setPublicationIds);
const people = createWritableStateSlice(store.peopleIds, store.setPeopleIds);
const projects = createWritableStateSlice(store.projects, store.setProjects);
const years = createWritableStateSlice(store.years, store.setYears);
const search = createWritableStateSlice(store.search, store.setSearch);
const sortBy = createWritableStateSlice(store._sortBy, store.setSortBy);
Expand Down Expand Up @@ -91,6 +94,12 @@ export const ResearchStore = signalStore(
stringify: serializePeopleIds,
...commonOptions,
});
linkedQueryParam('project', {
source: projects,
parse: parseProjects,
stringify: serializeProjects,
...commonOptions,
});
linkedQueryParam('year', {
source: years,
parse: parseYears,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CATEGORY_OPTIONS, CategoryOption, YEAR_OPTIONS, YearOption } from './with-filters.feature';
import {
CATEGORY_OPTIONS,
CategoryOption,
PROJECT_OPTIONS,
ProjectsOption,
YEAR_OPTIONS,
YearOption,
} from './with-filters.feature';
import { GroupBy, SortBy } from './with-ordering.feature';
import { View } from './with-view.feature';

Expand Down Expand Up @@ -67,6 +74,10 @@ export function parseCategories(value: unknown): CategoryOption[] | null {
return parseOptions(CATEGORY_OPTIONS, value);
}

export function parseProjects(value: unknown): ProjectsOption[] | null {
return parseOptions(PROJECT_OPTIONS, value);
}

/**
* Parses event query parameter into event options.
* @param value Raw query value
Expand Down Expand Up @@ -159,6 +170,10 @@ export function serializeCategories(options: CategoryOption[] | null): string |
return serializeOptions(options);
}

export function serializeProjects(options: ProjectsOption[] | null): string | null {
return serializeOptions(options);
}

/**
* Serializes selected events to query parameter format.
* @param ids Selected event IDs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PeopleId } from '../../../schemas/people.schema';
import { ResearchTypeId, ResearchTypeItem } from '../../../schemas/research-type.schema';
import { ResearchCategoryId, ResearchItem } from '../../../schemas/research.schema';
import { ResearchState } from './with-research.feature';
import { TagId } from '../../../schemas/tags.schema';

/** Generic search list option with a typed id */
type TypedSearchListOption<T extends string> = SearchListOption & { id: T };
Expand All @@ -33,6 +34,8 @@ export type PublicationOption = TypedSearchListOption<ResearchTypeId>;
/** Filter option for people */
export type PeopleOption = TypedSearchListOption<PeopleId>;

export type ProjectsOption = TypedSearchListOption<TagId>;

/** Year option with numeric year value */
export interface YearOption extends SearchListOption {
/** Year value */
Expand All @@ -59,6 +62,8 @@ export interface FilterProps {
countsByPublicationType: Signal<Record<string, number>>;
/** Counts by people */
countsByPeople: Signal<Record<string, number>>;
/** Counts by project */
countsByProject: Signal<Record<string, number>>;
/** Counts by year */
countsByYear: Signal<Record<string, number>>;
/** Aggregate counts array */
Expand All @@ -80,6 +85,8 @@ interface FilterState {
fundingIds: string[] | null;
/** Selected people IDs */
peopleIds: string[] | null;
/** Selected project */
projects: ProjectsOption[] | null;
/** Selected years */
years: YearOption[] | null;
/** Search text */
Expand Down Expand Up @@ -114,6 +121,15 @@ export const CATEGORY_OPTIONS: CategoryOption[] = [
{ id: 'visualization' as ResearchCategoryId, label: 'Visualizations' },
];

export const PROJECT_OPTIONS: ProjectsOption[] = [
{ id: 'amatria' as TagId, label: 'Amatria' },
{ id: 'envisioning-intelligences' as TagId, label: 'Envisioning Intelligences' },
{ id: 'hra' as TagId, label: 'Human Reference Atlas' },
{ id: 'macroscopes' as TagId, label: 'Macroscopes' },
{ id: 'maps' as TagId, label: 'Maps' },
{ id: 'whole-person-physiome' as TagId, label: 'Whole Person Physiome' },
];

/** Year filter options from 1991 to current year */
export const YEAR_OPTIONS: YearOption[] = createYearList(1991).map((year) => ({
id: year.toString(),
Expand Down Expand Up @@ -161,6 +177,14 @@ const PEOPLE_FILTER: FilterOptionCategory<PeopleOption> = {
selected: [],
};

/** Projects filter configuration */
const PROJECTS_FILTER: FilterOptionCategory<ProjectsOption> = {
id: 'project',
label: 'Project',
options: PROJECT_OPTIONS,
selected: [],
};

/** Year filter configuration */
const YEARS_FILTER: FilterOptionCategory<YearOption> = {
id: 'year',
Expand All @@ -176,6 +200,7 @@ const initialState: FilterState = {
eventIds: null,
fundingIds: null,
peopleIds: null,
projects: null,
years: null,
search: null,
};
Expand Down Expand Up @@ -337,6 +362,7 @@ export function withFilters() {
const _fundingFilter = optionsToFilter(FUNDING_FILTER, funding, _fundingOptions);
const _publicationsFilter = optionsToFilter(PUBLICATIONS_FILTER, publications, _publicationOptions);
const _peopleFilter = optionsToFilter(PEOPLE_FILTER, people, _peopleOptions);
const _projectsFilter = optionsToFilter(PROJECTS_FILTER, store.projects);
const _yearsFilter = optionsToFilter(YEARS_FILTER, store.years);

const filters = computed((): FilterOptionCategory<SearchListOption>[] => [
Expand All @@ -345,6 +371,7 @@ export function withFilters() {
_fundingFilter(),
_publicationsFilter(),
_peopleFilter(),
_projectsFilter(),
_yearsFilter(),
]);

Expand All @@ -365,8 +392,16 @@ export function withFilters() {
item.people.some((person) => selectedPeople.has(person)),
);

const _selectedProjects = optionsToSet(store.projects);
const _filteredByProject = createFilteredBy(_filteredByPeople, _selectedProjects, (item, selectedProjects) => {
if (item.projects) {
return item.projects.some((project) => selectedProjects.has(project));
}
return item.tags.some((tag) => selectedProjects.has(tag));
});

const _selectedYears = computed(() => new Set(store.years()?.map((option) => option.year) ?? []));
const _filteredByYear = createFilteredBy(_filteredByPeople, _selectedYears, (item, selectedYears) =>
const _filteredByYear = createFilteredBy(_filteredByProject, _selectedYears, (item, selectedYears) =>
selectedYears.has(item.dateStart.getFullYear()),
);

Expand Down Expand Up @@ -402,6 +437,7 @@ export function withFilters() {
(item) => item.category === 'publication',
);
const countsByPeople = countsByKey(store.researchItems, (item) => item.people);
const countsByProject = countsByKey(store.researchItems, (item) => item.projects || item.tags);
const countsByYear = countsByKey(store.researchItems, (item) => item.dateStart.getFullYear().toString());

const counts = computed(() => [
Expand All @@ -410,6 +446,7 @@ export function withFilters() {
countsByFundingType(),
countsByPublicationType(),
countsByPeople(),
countsByProject(),
countsByYear(),
]);

Expand All @@ -423,11 +460,13 @@ export function withFilters() {
countsByFundingType,
countsByPublicationType,
countsByPeople,
countsByProject,
countsByYear,
counts,
_filteredByCategory,
_filteredByType,
_filteredByPeople,
_filteredByProject,
_filteredByYear,
} satisfies FilterProps & InternalProps;
}),
Expand All @@ -449,6 +488,7 @@ export function withFilters() {
setPeople: signalMethod((people: PeopleOption[] | null) =>
patchState(store, { peopleIds: people?.map((p) => p.id) ?? null }),
),
setProjects: signalMethod((projects: ProjectsOption[] | null) => patchState(store, { projects })),
/**
* Sets selected years.
* @param years Selected year options
Expand All @@ -469,14 +509,16 @@ export function withFilters() {
const funding = filters[2]?.selected as FundingOption[];
const publications = filters[3]?.selected as PublicationOption[];
const people = filters[4]?.selected as PeopleOption[];
const years = filters[5]?.selected as YearOption[];
const projects = filters[5]?.selected as ProjectsOption[];
const years = filters[6]?.selected as YearOption[];

patchState(store, {
categories: categories.length > 0 ? categories : null,
publicationIds: publications.length > 0 ? publications.map((p) => p.id) : null,
eventIds: events.length > 0 ? events.map((e) => e.id) : null,
fundingIds: funding.length > 0 ? funding.map((f) => f.id) : null,
peopleIds: people.length > 0 ? people.map((p) => p.id) : null,
projects: projects.length > 0 ? projects : null,
years: years.length > 0 ? years : null,
});
}),
Expand Down
5 changes: 5 additions & 0 deletions apps/cns-website/src/app/schemas/research.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export const ResearchItemSchema = z
people: z.array(PeopleIdSchema),
/** Tags for categorizing the research */
tags: z.array(TagIdSchema).transform((tags) => uniqueValues(tags)),
/** Projects associated with the research (uses tag ids) */
projects: z
.array(TagIdSchema)
.transform((projects) => uniqueValues(projects))
.optional(),
/** Image source URL */
image: z.string().optional(),
})
Expand Down
65 changes: 33 additions & 32 deletions libs/design-system/filter-menu/src/lib/filter-menu.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,40 @@
<span class="title">Filter</span>
</div>
@for (filter of filtersWithCounts(); track $index) {
<hra-filter-container
cdkOverlayOrigin
[hraFeature]="filter.id | slugify"
[enableDivider]="!$last"
[action]="filter.label"
[totalCount]="filter.totalCount"
[active]="activeFilter() === filter"
[chips]="filter.selected ?? []"
(chipsChange)="updateFilterSelection(filter, $event)"
(actionClick)="toggleFilterMenu(filter)"
#filterMenuOrigin="cdkOverlayOrigin"
/>

<ng-template
cdkConnectedOverlay
cdkConnectedOverlayHasBackdrop="false"
cdkConnectedOverlayLockPosition="true"
cdkConnectedOverlayPush="true"
[cdkConnectedOverlayOpen]="activeFilterId() === filter.id"
[cdkConnectedOverlayOrigin]="filterMenuOrigin"
[cdkConnectedOverlayPositions]="filterMenuPositions"
(overlayOutsideClick)="closeFilterMenu(filter)"
(detach)="closeFilterMenu(filter)"
>
<hra-search-list
[hraFeature]="`${(filter.id | slugify)}.menu`"
[options]="filter.options"
[selected]="filter.selected ?? []"
[counts]="counts()?.[$index]"
[disableSearch]="filter.disableSearch ?? false"
(selectedChange)="updateFilterSelection(filter, $event)"
@if (!isExtraCategory(filter)) {
<hra-filter-container
cdkOverlayOrigin
[hraFeature]="filter.id | slugify"
[enableDivider]="!$last"
[action]="filter.label"
[totalCount]="filter.totalCount"
[active]="activeFilter() === filter"
[chips]="filter.selected ?? []"
(chipsChange)="updateFilterSelection(filter, $event)"
(actionClick)="toggleFilterMenu(filter)"
#filterMenuOrigin="cdkOverlayOrigin"
/>
</ng-template>
<ng-template
cdkConnectedOverlay
cdkConnectedOverlayHasBackdrop="false"
cdkConnectedOverlayLockPosition="true"
cdkConnectedOverlayPush="true"
[cdkConnectedOverlayOpen]="activeFilterId() === filter.id"
[cdkConnectedOverlayOrigin]="filterMenuOrigin"
[cdkConnectedOverlayPositions]="filterMenuPositions"
(overlayOutsideClick)="closeFilterMenu(filter)"
(detach)="closeFilterMenu(filter)"
>
<hra-search-list
[hraFeature]="`${(filter.id | slugify)}.menu`"
[options]="filter.options"
[selected]="filter.selected ?? []"
[counts]="counts()[$index]"
[disableSearch]="filter.disableSearch ?? false"
(selectedChange)="updateFilterSelection(filter, $event)"
/>
</ng-template>
}
}
</div>
</ng-scrollbar>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, computed, input, model, output, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, input, model, output, signal } from '@angular/core';
import { watchBreakpoint } from '@hra-ui/cdk/breakpoints';
import { HraCommonModule } from '@hra-ui/common';
import { ButtonsModule } from '@hra-ui/design-system/buttons';
Expand Down Expand Up @@ -67,7 +67,7 @@ export class FilterMenuComponent<T extends SearchListOption> {
readonly filters = model.required<FilterOptionCategory<T>[]>();

/** Counts for each filter option */
readonly counts = input<Record<string, number>[]>();
readonly counts = input<Record<string, number>[]>([]);

/** Emits when the form opening state is toggled */
readonly closeClick = output();
Expand All @@ -92,6 +92,10 @@ export class FilterMenuComponent<T extends SearchListOption> {
return this.filters();
});

isExtraCategory(category: FilterOptionCategory<T>): boolean {
return Object.keys(this.counts()[this.filters().indexOf(category)]).length < 2;
}

/**
* Updates filters on filter selection
* @param category Filter category to update
Expand Down
40 changes: 20 additions & 20 deletions libs/design-system/search-list/src/lib/search-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@
(selectionChange)="selectionUpdate($event.source.selectedOptions.selected)"
>
@for (option of filteredOptions(); track option.id) {
<mat-list-option
togglePosition="before"
hraClickEvent
[hraFeature]="option.label | slugify"
[value]="option"
[selected]="selected().includes(option)"
[attr.aria-label]="`Toggle ${option.label}`"
>
<mat-label class="labels-count">
<span class="option-labels">
<div class="option-primary-label">{{ option.label }}</div>
@if (option.secondaryLabel) {
<div class="option-secondary-label">{{ option.secondaryLabel }}</div>
}
</span>
@let count = getCount(option);
@if (count !== undefined) {
@let count = getCount(option);
@if (count) {
<mat-list-option
togglePosition="before"
hraClickEvent
[hraFeature]="option.label | slugify"
[value]="option"
[selected]="selected().includes(option)"
[attr.aria-label]="`Toggle ${option.label}`"
>
<mat-label class="labels-count">
<span class="option-labels">
<div class="option-primary-label">{{ option.label }}</div>
@if (option.secondaryLabel) {
<div class="option-secondary-label">{{ option.secondaryLabel }}</div>
}
</span>
<span class="option-count">{{ count | number }}</span>
}
</mat-label>
</mat-list-option>
</mat-label>
</mat-list-option>
}
}
</mat-selection-list>
</ng-scrollbar>
Loading