From 5c7acb40e08c7ea8021ebc076aa27979f28f117d Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 29 Jan 2024 00:49:05 +0100 Subject: [PATCH] feat(filters): add filters for dataSet and catCombo --- .../SearchableSingleSelect.tsx | 6 +- .../SearchableSingleSelect/index.ts | 3 +- .../sectionList/filters/DynamicFilters.tsx | 5 +- .../filterSelectors/CategoryComboFilter.tsx | 24 +++ .../ConstantSelectionFilter.tsx | 1 + .../filters/filterSelectors/DataSetFilter.tsx | 24 +++ .../filters/filterSelectors/ModelFilter.tsx | 177 ++++++++++++++++++ .../filterSelectors/createdFilterDataQuery.ts | 13 ++ .../filters/filterSelectors/index.ts | 2 + .../sectionList/filters/useFilterKeys.tsx | 3 +- .../sectionList/listView/useModelListView.tsx | 2 +- src/lib/sectionList/filters/index.ts | 1 + .../filters/parseFiltersToQueryParams.ts | 4 +- .../filters/useFilterQueryParams.ts | 2 +- .../listViews/sectionListViewsConfig.ts | 4 +- src/lib/sectionList/useParamsForDataQuery.ts | 11 +- 16 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx create mode 100644 src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx create mode 100644 src/components/sectionList/filters/filterSelectors/ModelFilter.tsx create mode 100644 src/components/sectionList/filters/filterSelectors/createdFilterDataQuery.ts diff --git a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx index 3a6c9013..26894a26 100644 --- a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx +++ b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx @@ -6,11 +6,10 @@ import { SingleSelectOption, } from '@dhis2/ui' import React, { forwardRef, useEffect, useState } from 'react' -// import { useDebouncedCallback } from 'use-debounce' import { useDebouncedState } from '../../lib' import classes from './SearchableSingleSelect.module.css' -interface Option { +export interface Option { value: string label: string } @@ -55,6 +54,7 @@ interface SearchableSingleSelectPropTypes { onRetryClick: () => void options: Option[] placeholder: string + prefix?: string showEndLoader: boolean loading: boolean selected?: string @@ -70,6 +70,7 @@ export const SearchableSingleSelect = ({ error, loading, placeholder, + prefix, onBlur, onChange, onEndReached, @@ -126,6 +127,7 @@ export const SearchableSingleSelect = ({ error={invalid} onChange={onChange} placeholder={placeholder} + prefix={prefix} onBlur={onBlur} onFocus={onFocus} > diff --git a/src/components/SearchableSingleSelect/index.ts b/src/components/SearchableSingleSelect/index.ts index 7f9aa7b6..889bd39d 100644 --- a/src/components/SearchableSingleSelect/index.ts +++ b/src/components/SearchableSingleSelect/index.ts @@ -1 +1,2 @@ -export { SearchableSingleSelect } from './SearchableSingleSelect' +export * from './SearchableSingleSelect' +//asf diff --git a/src/components/sectionList/filters/DynamicFilters.tsx b/src/components/sectionList/filters/DynamicFilters.tsx index 0ce82eb9..20c7e0cd 100644 --- a/src/components/sectionList/filters/DynamicFilters.tsx +++ b/src/components/sectionList/filters/DynamicFilters.tsx @@ -2,6 +2,8 @@ import React from 'react' import { ConfigurableFilterKey } from './../../../lib' import { AggregationTypeFilter, + CategoryComboFilter, + DataSetFilter, DomainTypeSelectionFilter, ValueTypeSelectionFilter, } from './filterSelectors' @@ -10,6 +12,8 @@ import { useFilterKeys } from './useFilterKeys' type FilterKeyToComponentMap = Partial> const filterKeyToComponentMap: FilterKeyToComponentMap = { + categoryCombo: CategoryComboFilter, + dataSet: DataSetFilter, domainType: DomainTypeSelectionFilter, valueType: ValueTypeSelectionFilter, aggregationType: AggregationTypeFilter, @@ -17,7 +21,6 @@ const filterKeyToComponentMap: FilterKeyToComponentMap = { export const DynamicFilters = () => { const filterKeys = useFilterKeys() - return ( <> {filterKeys.map((filterKey) => { diff --git a/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx b/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx new file mode 100644 index 00000000..67a2e91c --- /dev/null +++ b/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx @@ -0,0 +1,24 @@ +import i18n from '@dhis2/d2-i18n' +import React from 'react' +import { useSectionListFilter } from '../../../../lib' +import { createFilterDataQuery } from './createdFilterDataQuery' +import { ModelFilterSelect } from './ModelFilter' + +const query = createFilterDataQuery('categoryCombos') + +export const CategoryComboFilter = () => { + const [filter, setFilter] = useSectionListFilter('categoryCombo') + + const selected = filter?.[0] + + return ( + + setFilter(selected ? [selected] : undefined) + } + /> + ) +} diff --git a/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx b/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx index 47b90117..2c29b768 100644 --- a/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx +++ b/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx @@ -28,6 +28,7 @@ export const ConstantSelectionFilter = ({ selected={Array.isArray(filter) ? filter[0] : filter} placeholder={label} dense + prefix={label} filterable={filterable} filterPlaceholder={i18n.t('Type to filter options')} noMatchText={i18n.t('No matches')} diff --git a/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx b/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx new file mode 100644 index 00000000..c8d570b2 --- /dev/null +++ b/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx @@ -0,0 +1,24 @@ +import i18n from '@dhis2/d2-i18n' +import React from 'react' +import { useSectionListFilter } from '../../../../lib' +import { createFilterDataQuery } from './createdFilterDataQuery' +import { ModelFilterSelect } from './ModelFilter' + +const query = createFilterDataQuery('dataSets') + +export const DataSetFilter = () => { + const [filter, setFilter] = useSectionListFilter('dataSet') + + const selected = filter?.[0] + + return ( + + setFilter(selected ? [selected] : undefined) + } + /> + ) +} diff --git a/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx b/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx new file mode 100644 index 00000000..eff0b634 --- /dev/null +++ b/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx @@ -0,0 +1,177 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import React, { useCallback, useRef, useState } from 'react' +import type { Query } from '../../../../types' +import { Pager } from '../../../../types/generated' +import { Option, SearchableSingleSelect } from '../../../SearchableSingleSelect' + +function computeDisplayOptions({ + selected, + selectedOption, + options, +}: { + options: OptionResult[] + selected?: string + required?: boolean + selectedOption?: OptionResult +}): Option[] { + // This happens only when we haven't fetched the label for an initially + // selected value. Don't show anything to prevent error that an option is + // missing + if (!selectedOption && selected) { + return [] + } + + const optionsContainSelected = options?.find(({ id }) => id === selected) + + const withSelectedOption = + selectedOption && !optionsContainSelected + ? [...options, selectedOption] + : options + + return withSelectedOption.map((option) => ({ + value: option.id, + label: option.displayName, + })) +} + +type ModelQuery = { + result: Query[keyof Query] +} + +type OptionResult = { + id: string + displayName: string +} + +type OptionsResult = { + result: { + pager: Pager + } & { [key: string]: OptionResult[] } +} + +type PagedResult = { pager: Pager } & OptionsResult + +const createInitialOptionQuery = ( + resource: string, + selected?: string +): ModelQuery => ({ + result: { + resource: resource, + id: selected, + params: (params) => ({ + ...params, + fields: ['id', 'displayName'], + }), + }, +}) + +export interface ModelSingleSelectProps { + onChange: ({ selected }: { selected: string | undefined }) => void + selected?: string + placeholder: string + query: Query +} + +export const ModelFilterSelect = ({ + onChange, + selected, + query, + placeholder, +}: ModelSingleSelectProps) => { + // Using a ref because we don't want to react to changes. + // We're using this value only when imperatively calling `refetch`, + // nothing that depends on the render-cycle depends on this value + const filterRef = useRef() + const pageRef = useRef(0) + + const [initialQuery] = useState(() => + createInitialOptionQuery(query.result.resource, selected) + ) + + const initialOptionResult = useDataQuery(initialQuery, { + // run only when we have an initial selected value + lazy: initialQuery.result.id === undefined, + onComplete: (data) => { + setSelectedOption(data) + }, + }) + + // We need to persist the selected option so we can display an