Skip to content

Commit

Permalink
feat(filters): add filters for dataSet and catCombo
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Jan 28, 2024
1 parent 3242a4f commit 5c7acb4
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -55,6 +54,7 @@ interface SearchableSingleSelectPropTypes {
onRetryClick: () => void
options: Option[]
placeholder: string
prefix?: string
showEndLoader: boolean
loading: boolean
selected?: string
Expand All @@ -70,6 +70,7 @@ export const SearchableSingleSelect = ({
error,
loading,
placeholder,
prefix,
onBlur,
onChange,
onEndReached,
Expand Down Expand Up @@ -126,6 +127,7 @@ export const SearchableSingleSelect = ({
error={invalid}
onChange={onChange}
placeholder={placeholder}
prefix={prefix}
onBlur={onBlur}
onFocus={onFocus}
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/SearchableSingleSelect/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { SearchableSingleSelect } from './SearchableSingleSelect'
export * from './SearchableSingleSelect'
//asf
5 changes: 4 additions & 1 deletion src/components/sectionList/filters/DynamicFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react'
import { ConfigurableFilterKey } from './../../../lib'
import {
AggregationTypeFilter,
CategoryComboFilter,
DataSetFilter,
DomainTypeSelectionFilter,
ValueTypeSelectionFilter,
} from './filterSelectors'
Expand All @@ -10,14 +12,15 @@ import { useFilterKeys } from './useFilterKeys'
type FilterKeyToComponentMap = Partial<Record<ConfigurableFilterKey, React.FC>>

const filterKeyToComponentMap: FilterKeyToComponentMap = {
categoryCombo: CategoryComboFilter,
dataSet: DataSetFilter,
domainType: DomainTypeSelectionFilter,
valueType: ValueTypeSelectionFilter,
aggregationType: AggregationTypeFilter,
}

export const DynamicFilters = () => {
const filterKeys = useFilterKeys()

return (
<>
{filterKeys.map((filterKey) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ModelFilterSelect
placeholder={i18n.t('Category combo')}
query={query}
selected={selected}
onChange={({ selected }) =>
setFilter(selected ? [selected] : undefined)
}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<ModelFilterSelect
placeholder={i18n.t('Data set')}
query={query}
selected={selected}
onChange={({ selected }) =>
setFilter(selected ? [selected] : undefined)
}
/>
)
}
177 changes: 177 additions & 0 deletions src/components/sectionList/filters/filterSelectors/ModelFilter.tsx
Original file line number Diff line number Diff line change
@@ -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<string | undefined>()
const pageRef = useRef(0)

const [initialQuery] = useState(() =>
createInitialOptionQuery(query.result.resource, selected)
)

const initialOptionResult = useDataQuery<OptionResult>(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 <Option />
// when the current list doesn't contain the selected option (e.g. when
// the page with the selected option hasn't been reached yet or when
// filtering)
const [selectedOption, setSelectedOption] = useState<OptionResult>()

const optionsQueryResult = useDataQuery<PagedResult>(query)
const { refetch, data } = optionsQueryResult

const pager = data?.result.pager
const page = pager?.page || 0
const pageCount = pager?.pageCount || 0

const refetchWithFilter = useCallback(
({ value }: { value: string }) => {
pageRef.current = 1
filterRef.current = value ? `displayName:ilike:${value}` : undefined
refetch({
page: pageRef.current,
filter: value ? filterRef.current : undefined,
})
},
[refetch]
)

const incrementPage = useCallback(() => {
pageRef.current = page + 1
refetch({ page: pageRef.current, filter: filterRef.current })
}, [refetch, page])

const loading =
optionsQueryResult.fetching ||
optionsQueryResult.loading ||
initialOptionResult.loading
const error =
optionsQueryResult.error || initialOptionResult.error
? // @TODO: Ask Joe what do do here!
'An error has occurred. Please try again'
: ''
const dataResultKey = query.result.resource
const options = data?.result[dataResultKey] || []

const displayOptions = computeDisplayOptions({
selected,
selectedOption,
options,
})

return (
<SearchableSingleSelect
placeholder={placeholder}
prefix={placeholder}
showAllOption={true}
onChange={({ selected }) => {
if (selected === selectedOption?.id) {
setSelectedOption(undefined)
} else {
const option = options.find(({ id }) => id === selected)
setSelectedOption(option)
}

onChange({ selected: selected })
}}
onEndReached={incrementPage}
options={displayOptions}
selected={selected}
showEndLoader={!loading && page < pageCount}
onFilterChange={refetchWithFilter}
loading={loading}
error={error}
onRetryClick={() => {
refetch({
page: pageRef.current,
filter: filterRef.current,
})
}}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Query } from '../../../../types'

export const createFilterDataQuery = (resource: string): Query => ({
result: {
resource: resource,
params: (params) => ({
...params,
fields: ['id', 'displayName'],
order: 'displayName:asc',
pageSize: 5,
}),
},
})
2 changes: 2 additions & 0 deletions src/components/sectionList/filters/filterSelectors/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './ConstantFilters'
export * from './DataSetFilter'
export * from './CategoryComboFilter'
3 changes: 1 addition & 2 deletions src/components/sectionList/filters/useFilterKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export type FiltersWithValue = {

export const useFilterKeys = () => {
const [filters] = useSectionListFilters()
const views = useModelListView()
const viewFilters = views.filters
const { filters: viewFilters } = useModelListView()
// combine filters and views, since filters in URL might not be selected for view
// but we should show them when they have a value
const filterKeys = useMemo(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/sectionList/listView/useModelListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const useModelListView = () => {
console.error(query.error)
}

const selectedView = query.data || getDefaultViewForSection(section.name)
const selectedView = getDefaultViewForSection(section.name)

const columns = selectedView.columns
const filters = selectedView.filters
Expand Down
1 change: 1 addition & 0 deletions src/lib/sectionList/filters/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './filterConfig'
export * from './useSectionListFilters'
export * from './useFilterQueryParams'
4 changes: 3 additions & 1 deletion src/lib/sectionList/filters/parseFiltersToQueryParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const getQueryParamForFilter = (
return `dataSetElements.dataSet.id:in:[${v.join(',')}]`
}
}
if (key === 'categoryCombo') {
return `categoryCombo.id:in:[${(value as string[]).join(',')}]`
}
return defaultFilter(key, value)
}

Expand All @@ -41,6 +44,5 @@ export const parseFiltersToQueryParams = (
.filter(
(queryFilter): queryFilter is string => queryFilter !== undefined
)

return queryFilters
}
2 changes: 1 addition & 1 deletion src/lib/sectionList/filters/useFilterQueryParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSectionListFilters } from './useSectionListFilters'
export const useFilterQueryParams = (): string[] => {
const [filters] = useSectionListFilters()
const section = useSectionHandle()

console.log({ section })
return useMemo(() => {
return parseFiltersToQueryParams(filters, section)
}, [filters, section])
Expand Down
4 changes: 2 additions & 2 deletions src/lib/sectionList/listViews/sectionListViewsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ export const modelListViewsConfig = {
{ label: i18n.t('Domain'), path: 'domainType' },
{ label: i18n.t('Value type'), path: 'valueType' },
'lastUpdated',
'sharing.public',
DESCRIPTORS.publicAccess,
],
},
filters: {
default: ['domainType', 'valueType'],
default: ['domainType', 'valueType', 'dataSet', 'categoryCombo'],
available: ['categoryCombo'],
},
},
Expand Down
Loading

0 comments on commit 5c7acb4

Please sign in to comment.