Skip to content

Commit

Permalink
fix(ModelSingleSelect): more refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Dec 10, 2024
1 parent 56c209a commit 7bb5852
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import i18n from '@dhis2/d2-i18n'
import React, { forwardRef } from 'react'
import { ModelSingleSelect } from '../ModelSingleSelect'
import type { ModelSingleSelectProps } from '../ModelSingleSelect'
import { ModelSingleSelect } from '../ModelSingleSelect/ModelSingleSelectRefactor'
import type { ModelSingleSelectProps } from '../ModelSingleSelect/ModelSingleSelectRefactor'
import { useInitialOptionQuery } from './useInitialOptionQuery'
import { useOptionsQuery } from './useOptionsQuery'
import { DisplayableModel } from '../../../types/models'

type CategoryComboSelectProps = Omit<
ModelSingleSelectProps,
'useInitialOptionQuery' | 'useOptionsQuery'
>

type CategoryComboSelectProps = ModelSingleSelectProps<DisplayableModel> & {
required?: boolean
}
export const CategoryComboSelect = forwardRef(function CategoryComboSelect(
{
onChange,
Expand All @@ -26,7 +25,6 @@ export const CategoryComboSelect = forwardRef(function CategoryComboSelect(
) {
return (
<ModelSingleSelect
ref={ref}
required={required}
invalid={invalid}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const toDisplayOption = (model: DisplayableModel) => ({
type OwnProps<TModel> = {
selected: TModel | undefined
available: TModel[]
onChange: ({ selected }: { selected: TModel | undefined }) => void
onChange: (selected: TModel | undefined) => void
showNoValueOption?: boolean
}

export type BaseModelSingleSelectProps<TModel extends DisplayableModel> = Omit<
Expand All @@ -27,27 +28,34 @@ export const BaseModelSingleSelect = <TModel extends DisplayableModel>({
available,
selected,
onChange,
showNoValueOption,
...searchableSingleSelectProps
}: BaseModelSingleSelectProps<TModel>) => {
const { allModelsMap, allSingleSelectOptions } = useMemo(() => {
const allModels = selected ? [selected].concat(available) : available
const allModelsMap = new Map(allModels.map((o) => [o.id, o]))
const allSingleSelectOptions = allModels.map(toDisplayOption)
const allModelsMap = new Map(available.map((o) => [o.id, o]))
// due to pagination, the selected model might not be in the available list, so add it
if (selected && !allModelsMap.get(selected.id)) {
allModelsMap.set(selected.id, selected)
}
const allSingleSelectOptions = Array.from(allModelsMap).map(
([, value]) => toDisplayOption(value)
)
if (showNoValueOption) {
allSingleSelectOptions.unshift({ value: '', label: '' })
}

return {
allModelsMap,
allSingleSelectOptions,
}
}, [available, selected])
}, [available, selected, showNoValueOption])

const handleOnChange: SearchableSingleSelectPropTypes['onChange'] =
useCallback(
({ selected }) => {
// map the selected ids to the full model
const fullSelectedModel = allModelsMap.get(selected)
onChange({
selected: fullSelectedModel,
})
onChange(fullSelectedModel)
},
[onChange, allModelsMap]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,52 @@ import {
ModelSingleSelect,
} from './ModelSingleSelectRefactor'

// this currently does not need a generic, because the value of the field is not passed
// or available from props. However if it's made available,
// the generic of <TModel extends DisplayableModel> should be added.
type ModelSingleSelectFieldProps = {
type OwnProps<TModel extends DisplayableModel> = {
name: string
query: PlainResourceQuery
label?: string
placeholder?: string
helpText?: string
} & ModelSingleSelectProps<DisplayableModel>
required?: boolean
onChange?: ModelSingleSelectProps<TModel>['onChange']
}

type ModelSingleSelectFieldProps<TModel extends DisplayableModel> = Omit<
ModelSingleSelectProps<TModel>,
'selected' | 'onChange'
> &
OwnProps<TModel>

export function ModelSingleSelectField({
export function ModelSingleSelectField<TModel extends DisplayableModel>({
name,
query,
label,
helpText,
required,
onChange,
...modelSingleSelectProps
}: ModelSingleSelectFieldProps) {
const { input, meta } = useField<DisplayableModel | undefined>(name, {
}: ModelSingleSelectFieldProps<TModel>) {
const { input, meta } = useField<TModel | undefined>(name, {
validateFields: [],
})

return (
<Field
dataTest="formfields-modelsingleselect"
dataTest={`formfields-modelsingleselect-${name}`}
error={meta.invalid}
validationText={(meta.touched && meta.error?.toString()) || ''}
name={name}
label={label}
helpText={helpText}
required={required}
>
<ModelSingleSelect
<ModelSingleSelect<TModel>
{...modelSingleSelectProps}
selected={input.value}
onChange={({ selected }) => {
onChange={(selected) => {
input.onChange(selected)
input.onBlur()
onChange?.(selected)
}}
query={query}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'
import React, { useMemo, useRef, useState } from 'react'
import { useInfiniteQuery } from 'react-query'
import { useDebouncedCallback } from 'use-debounce'
import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn'
Expand All @@ -16,8 +16,9 @@ const defaultQuery = {
params: {
order: 'displayName:asc',
fields: ['id', 'displayName'],
pageSize: 10,
},
}
} satisfies Omit<PlainResourceQuery, 'resource'>

export type ModelSingleSelectProps<TModel extends DisplayableModel> = Omit<
BaseModelSingleSelectProps<TModel>,
Expand All @@ -30,14 +31,18 @@ export type ModelSingleSelectProps<TModel extends DisplayableModel> = Omit<
> & {
query: Omit<PlainResourceQuery, 'id'>
onFilterChange?: (value: string) => void
select?: (value: TModel[]) => TModel[]
}

export const ModelSingleSelect = <TModel extends DisplayableModel>({
selected,
query,
select,
...baseModelSingleSelectProps
}: ModelSingleSelectProps<TModel>) => {
const queryFn = useBoundResourceQueryFn()
// keep select in ref, so we dont recompute for inline selects
const selectRef = useRef(select)
const [searchTerm, setSearchTerm] = useState('')

const searchFilter = `identifiable:token:${searchTerm}`
Expand All @@ -62,11 +67,18 @@ export const ModelSingleSelect = <TModel extends DisplayableModel>({
lastPage.pager.nextPage ? lastPage.pager.page + 1 : undefined,
getPreviousPageParam: (firstPage) =>
firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined,
// select: select
// ? (data) => select(data.pages.flatMap((p) => p[modelName]))
// : undefined,
})
const allDataMap = useMemo(
() => queryResult.data?.pages.flatMap((page) => page[modelName]) ?? [],
[queryResult.data, modelName]
)
const allDataMap = useMemo(() => {
const flatData =
queryResult.data?.pages.flatMap((page) => page[modelName]) ?? []
if (selectRef.current) {
return selectRef.current(flatData)
}
return flatData
}, [queryResult.data, modelName])

const handleFilterChange = useDebouncedCallback(({ value }) => {
if (value != undefined) {
Expand All @@ -83,6 +95,7 @@ export const ModelSingleSelect = <TModel extends DisplayableModel>({
onFilterChange={handleFilterChange}
onRetryClick={queryResult.refetch}
showEndLoader={!!queryResult.hasNextPage}
onEndReached={queryResult.fetchNextPage}
loading={queryResult.isLoading}
error={queryResult.error as string | undefined}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export const useRefreshModelSingleSelect = (

return useCallback(
(invalidateFilters?: InvalidateQueryFilters) => {
queryClient.invalidateQueries([query], invalidateFilters)
queryClient.invalidateQueries({
queryKey: [query],
...invalidateFilters,
})
},
[queryClient, query]
)
Expand Down
73 changes: 24 additions & 49 deletions src/pages/dataElements/fields/CategoryComboField.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import i18n from '@dhis2/d2-i18n'
import { Field } from '@dhis2/ui'
import React, { useEffect, useRef } from 'react'
import { useField, useForm, useFormState } from 'react-final-form'
import { useForm, useFormState } from 'react-final-form'
import { useHref } from 'react-router'
import { CategoryComboSelect, EditableFieldWrapper } from '../../../components'
import { useDefaultCategoryComboQuery } from '../../../lib'
import { EditableFieldWrapper } from '../../../components'
import { ModelSingleSelectField } from '../../../components/metadataFormControls/ModelSingleSelect/ModelSingleSelectField'
import { useRefreshModelSingleSelect } from '../../../components/metadataFormControls/ModelSingleSelect/useRefreshSingleSelect'
import {
DEFAULT_CATEGORY_COMBO,
useDefaultCategoryComboQuery,
} from '../../../lib'
import { PlainResourceQuery } from '../../../types'
import classes from './CategoryComboField.module.css'

const required = (value: { id: string }) => {
if (!value.id) {
return i18n.t('Required')
}
}
const query = {
resource: 'categoryCombos',
params: {
filter: 'isDefault:eq:false',
},
} satisfies PlainResourceQuery

/*
* @TODO: Verify that the api ignores the category combo when it's disabled.
Expand All @@ -23,24 +29,13 @@ const required = (value: { id: string }) => {
* domainType is Tracker
*/
export function CategoryComboField() {
const refresh = useRefreshModelSingleSelect(query)
const defaultCategoryComboQuery = useDefaultCategoryComboQuery()
const { change } = useForm()
const { values } = useFormState({ subscription: { values: true } })
const domainTypeIsTracker = values.domainType === 'TRACKER'
const disabled = domainTypeIsTracker
const validate = disabled ? undefined : required
const newCategoryComboLink = useHref('/categoryCombos/new')
const { input, meta } = useField('categoryCombo', {
validateFields: [],
validate,
format: (categoryCombo) => categoryCombo.id,
parse: (id) => ({ id }),
})
const categoryComboHandle = useRef({
refetch: () => {
throw new Error('Not initialized')
},
})

useEffect(() => {
if (defaultCategoryComboQuery.data?.id && domainTypeIsTracker) {
Expand All @@ -51,38 +46,18 @@ export function CategoryComboField() {
return (
<EditableFieldWrapper
dataTest="formfields-categorycombo"
onRefresh={() => categoryComboHandle.current.refetch()}
onRefresh={() => refresh}
onAddNew={() => window.open(newCategoryComboLink, '_blank')}
>
<div className={classes.categoryComboSelect}>
<Field
<ModelSingleSelectField
query={query}
name="categoryCombo"
label={i18n.t('Category combo')}
required
name="categoryCombo.id"
label={i18n.t('{{fieldLabel}} (required)', {
fieldLabel: i18n.t('Category combination'),
})}
helpText={i18n.t(
'Choose how this data element is disaggregated'
)}
error={meta.touched && !!meta.error}
validationText={meta.touched ? meta.error : undefined}
dataTest="formfields-categorycombo"
>
<CategoryComboSelect
required
placeholder=""
disabled={disabled}
invalid={meta.touched && !!meta.error}
ref={categoryComboHandle}
selected={input.value}
onChange={({ selected }) => {
input.onChange(selected)
input.onBlur()
}}
onBlur={input.onBlur}
onFocus={input.onFocus}
/>
</Field>
select={(value) => [DEFAULT_CATEGORY_COMBO, ...value]}
disabled={disabled}
/>
</div>
</EditableFieldWrapper>
)
Expand Down

0 comments on commit 7bb5852

Please sign in to comment.