From 099993eccff60fd3c958aa35433d4d422cd6faaf Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 27 Sep 2023 12:11:27 +0200 Subject: [PATCH] feat: generic selectionlist filter --- .../SearchableSingleSelect.tsx | 5 +- .../CategoryComboSelect.tsx | 2 + .../filters/GenericSelectionFilter.tsx | 117 ++++++++++++++++++ .../filters/useSectionListFilter.ts | 3 + src/constants/sectionListViews.ts | 2 +- src/pages/dataElements/List.tsx | 7 ++ src/types/generated/utility.ts | 15 ++- 7 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/components/sectionList/filters/GenericSelectionFilter.tsx diff --git a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx index 3b1aaeb9..d0d749c5 100644 --- a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx +++ b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx @@ -58,11 +58,13 @@ interface SearchableSingleSelectPropTypes { selected?: string error?: string showAllOption?: boolean + label: string } export const SearchableSingleSelect = ({ showAllOption, error, + label, loading, onChange, onFilterChange, @@ -96,7 +98,6 @@ export const SearchableSingleSelect = ({ const observer = new IntersectionObserver( (entries) => { const [{ isIntersecting }] = entries - if (isIntersecting) { onEndReached() } @@ -124,7 +125,7 @@ export const SearchableSingleSelect = ({ // any value to the "selected" prop, as otherwise an error will be thrown selected={hasSelectedInOptionList ? selected : ''} onChange={onChange} - placeholder={i18n.t('Category combo')} + placeholder={label} >
diff --git a/src/components/metadataSelects/CategoryComboSelect/CategoryComboSelect.tsx b/src/components/metadataSelects/CategoryComboSelect/CategoryComboSelect.tsx index be222eec..0e79a2db 100644 --- a/src/components/metadataSelects/CategoryComboSelect/CategoryComboSelect.tsx +++ b/src/components/metadataSelects/CategoryComboSelect/CategoryComboSelect.tsx @@ -1,3 +1,4 @@ +import i18n from '@dhis2/d2-i18n' import React, { useCallback, useRef, useState } from 'react' import { SelectOption } from '../../../types' import { SearchableSingleSelect } from '../../SearchableSingleSelect' @@ -92,6 +93,7 @@ export function CategoryComboSelect({ return ( { if (selected === selectedOption?.value) { diff --git a/src/components/sectionList/filters/GenericSelectionFilter.tsx b/src/components/sectionList/filters/GenericSelectionFilter.tsx new file mode 100644 index 00000000..b21c3c92 --- /dev/null +++ b/src/components/sectionList/filters/GenericSelectionFilter.tsx @@ -0,0 +1,117 @@ +import { useDataEngine } from '@dhis2/app-runtime' +import React, { useState } from 'react' +import { useInfiniteQuery, QueryFunctionContext } from 'react-query' +import { useModelSectionHandleOrThrow } from '../../../lib' +import { Query } from '../../../types' +import { + BaseIdentifiableObject, + IdentifiableObject, + ModelCollectionResponse, +} from '../../../types/generated' +import { SearchableSingleSelect } from '../../SearchableSingleSelect' +import { useSectionListFilter } from './useSectionListFilter' + +type SimpleQuery = { + resource: string + fields?: string[] + order?: string[] +} + +type GenericSelectionFilterProps = { + label: string + filterKey: string + query: SimpleQuery +} +type WrapInData = { data: T } + +const infiniteQueryFn = + (dataEngine: ReturnType) => + ({ + queryKey: [query], + pageParam = 1, + signal, + }: QueryFunctionContext<[Query], number>) => { + const pagedQuery = { + data: { + ...query.data, + params: { + ...query.data.params, + page: pageParam, + }, + }, + } + return dataEngine.query(pagedQuery, { + signal, + }) as Promise< + WrapInData> + > + } + +export const GenericSelectionFilter = ({ + filterKey, + label, + query, +}: GenericSelectionFilterProps) => { + const dataEngine = useDataEngine() + const [filter, setFilter] = useSectionListFilter(filterKey) + const [searchValue, setSearchValue] = useState('') + + const handleChange = ({ selected }: { selected: string }) => { + setFilter(selected) + } + + const optionsQuery = { + data: { + resource: query.resource, + params: { + pageSize: 20, + fields: + query.fields?.length && query.fields?.length > 0 + ? query.fields + : ['id', 'displayName'], + filter: searchValue + ? `displayName:ilike:${searchValue}` + : undefined, + }, + }, + } + + const res = useInfiniteQuery({ + queryKey: [optionsQuery], + queryFn: infiniteQueryFn(dataEngine), + getNextPageParam: (lastPage) => { + const pager = lastPage?.data?.pager + return pager.nextPage ? pager.page + 1 : undefined + }, + getPreviousPageParam: (lastPage) => { + const pager = lastPage?.data?.pager + return pager.nextPage ? pager.page - 1 : undefined + }, + }) + + const displayOptions = + res.data?.pages.flatMap((p) => + p.data[query.resource].map(({ id, displayName }) => ({ + value: id, + label: displayName, + })) + ) ?? [] + + return ( + setSearchValue(value)} + loading={false} + label={label} + error={res.error?.toString()} + onRetryClick={() => { + res.refetch() + }} + /> + ) +} diff --git a/src/components/sectionList/filters/useSectionListFilter.ts b/src/components/sectionList/filters/useSectionListFilter.ts index 535e9e98..8da8a8cd 100644 --- a/src/components/sectionList/filters/useSectionListFilter.ts +++ b/src/components/sectionList/filters/useSectionListFilter.ts @@ -126,6 +126,9 @@ const parseToGistQueryFilter = (filters: Filters): string[] => { restFilterGroup = 1 } Object.entries(restFilters).forEach(([key, value]) => { + if (key === 'uid') { + key = 'id' + } const group = restFilterGroup ? `${restFilterGroup++}:` : '' queryFilters.push(`${group}${key}:eq:${value}`) }) diff --git a/src/constants/sectionListViews.ts b/src/constants/sectionListViews.ts index ad3643ef..542a263e 100644 --- a/src/constants/sectionListViews.ts +++ b/src/constants/sectionListViews.ts @@ -1,6 +1,6 @@ import i18n from '@dhis2/d2-i18n' import { PublicAccessValue } from '../components/sectionList/modelValue/PublicAccess' -import { uniqueBy } from '../lib' +import { uniqueBy } from '../lib/utils/uniqueBy' import { SectionName } from './sections' import { getTranslatedProperty } from './translatedModelProperties' diff --git a/src/pages/dataElements/List.tsx b/src/pages/dataElements/List.tsx index d9b3fe48..faa50393 100644 --- a/src/pages/dataElements/List.tsx +++ b/src/pages/dataElements/List.tsx @@ -5,6 +5,7 @@ import { ValueTypeSelectionFilter, useQueryParamsForModelGist, } from '../../components' +import { GenericSelectionFilter } from '../../components/sectionList/filters/GenericSelectionFilter' import { useModelListView } from '../../components/sectionList/listView' import { getFieldFilterFromPath, useModelGist } from '../../lib/' import { DataElement, GistCollectionResponse } from '../../types/models' @@ -27,6 +28,7 @@ type DataElements = GistCollectionResponse export const Component = () => { const { columns, query: listViewQuery } = useModelListView() const initialParams = useQueryParamsForModelGist() + const { refetch, error, data } = useModelGist( 'dataElements/gist', { @@ -52,6 +54,11 @@ export const Component = () => { return (
+ diff --git a/src/types/generated/utility.ts b/src/types/generated/utility.ts index 76206d0b..8a91db6d 100644 --- a/src/types/generated/utility.ts +++ b/src/types/generated/utility.ts @@ -1,10 +1,23 @@ /* GENERATED BY https://github.com/Birkbjo/dhis2-open-api-ts */ -import { IdentifiableObject, GistPager } from './' +import { IdentifiableObject, GistPager, Pager } from './' // import { CategoryCombo, DataElement } from "../generated"; type ModelReferenceCollection = Array type ModelReference = IdentifiableObject | ModelReferenceCollection +export type ModelCollectionPart< + T extends IdentifiableObject, + PagedListName extends string = 'result' +> = { + [K in PagedListName]: T[] +} +export type ModelCollectionResponse< + T extends IdentifiableObject = IdentifiableObject, + PagedListName extends string = 'result' +> = { + pager: Pager +} & ModelCollectionPart + type BaseGist = IdentifiableObject & { apiEndpoints: GistApiEndpoints }