Skip to content

Commit

Permalink
refactor: modelSingleSelect refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Oct 31, 2024
1 parent ac43be0 commit 1c14ae7
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 34 deletions.
74 changes: 42 additions & 32 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-10-23T11:07:53.225Z\n"
"PO-Revision-Date: 2024-10-23T11:07:53.225Z\n"
"POT-Creation-Date: 2024-10-31T16:21:47.811Z\n"
"PO-Revision-Date: 2024-10-31T16:21:47.811Z\n"

msgid "schemas"
msgstr "schemas"
Expand Down Expand Up @@ -123,21 +123,6 @@ msgstr "Failed to load {{label}}"
msgid "Failed to load"
msgstr "Failed to load"

msgid "Download"
msgstr "Download"

msgid "Merge"
msgstr "Merge"

msgid "Delete source data element values"
msgstr "Delete source data element values"

msgid "Last updated"
msgstr "Last updated"

msgid "Discard"
msgstr "Discard"

msgid "Aggregation level(s)"
msgstr "Aggregation level(s)"

Expand Down Expand Up @@ -237,6 +222,9 @@ msgstr "Created"
msgid "Last updated by"
msgstr "Last updated by"

msgid "Last updated"
msgstr "Last updated"

msgid "Id"
msgstr "Id"

Expand All @@ -258,6 +246,9 @@ msgstr "Details"
msgid "Failed to load details"
msgstr "Failed to load details"

msgid "Download"
msgstr "Download"

msgid "Download {{section}}"
msgstr "Download {{section}}"

Expand Down Expand Up @@ -297,6 +288,9 @@ msgstr "Clear all filters"
msgid "Category"
msgstr "Category"

msgid "Category option"
msgstr "Category option"

msgid "Category option group"
msgstr "Category option group"

Expand Down Expand Up @@ -435,9 +429,6 @@ msgstr "Search for a user or group"
msgid "Categories"
msgstr "Categories"

msgid "Category option"
msgstr "Category option"

msgid "Category options"
msgstr "Category options"

Expand Down Expand Up @@ -855,9 +846,15 @@ msgstr "Zero is significant"
msgid "Data dimension type"
msgstr "Data dimension type"

msgid "Ignore data approval"
msgstr "Ignore data approval"

msgid "This field requires a unique value, please choose another one"
msgstr "This field requires a unique value, please choose another one"

msgid "{{label}} (required)"
msgstr "{{label}} (required)"

msgid "No changes to be saved"
msgstr "No changes to be saved"

Expand All @@ -879,23 +876,27 @@ msgstr "Basic information"
msgid "Set up the basic information for this category."
msgstr "Set up the basic information for this category."

msgid "Explain the purpose of this category."
msgstr "Explain the purpose of this category."
msgid "Explain the purpose of this category option group."
msgstr "Explain the purpose of this category option group."

msgid "Data configuration"
msgstr "Data configuration"

msgid "Choose how this category will be used to capture and analyze"
msgstr "Choose how this category will be used to capture and analyze"
msgid "Choose how this category option group will be used to capture and analyze"
msgstr "Choose how this category option group will be used to capture and analyze"

msgid "Use as data dimension"
msgstr "Use as data dimension"

msgid "Category will be available to the analytics as another dimension"
msgstr "Category will be available to the analytics as another dimension"
msgid ""
"Category option group will be available to the analytics as another "
"dimension"
msgstr ""
"Category option group will be available to the analytics as another "
"dimension"

msgid "Choose the category options to include in this category."
msgstr "Choose the category options to include in this category."
msgid "Choose the category options to include in this category option group."
msgstr "Choose the category options to include in this category option group."

msgid "Available category options"
msgstr "Available category options"
Expand Down Expand Up @@ -933,6 +934,15 @@ msgstr "Filter selected categories"
msgid "At least one category is required"
msgstr "At least one category is required"

msgid "Set up the basic information for this category option group."
msgstr "Set up the basic information for this category option group."

msgid "Choose how this category option will be used to capture and analyze"
msgstr "Choose how this category option will be used to capture and analyze"

msgid "Choose the category options to include in this category."
msgstr "Choose the category options to include in this category."

msgid "Set up the basic information for this category option."
msgstr "Set up the basic information for this category option."

Expand Down Expand Up @@ -1136,11 +1146,11 @@ msgstr "Latitude"
msgid "Longitude"
msgstr "Longitude"

msgid "Reference assignments"
msgstr "Reference assignments"
msgid "Reference assignment"
msgstr "Reference assignment"

msgid "Assign the organisation unit to related models."
msgstr "Assign the organisation unit to related models."
msgid "Assign the organisation unit to related objects."
msgstr "Assign the organisation unit to related objects."

msgid "Available data sets"
msgstr "Available data sets"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ function Error({

type OnChange = ({ selected }: { selected: string }) => void
type OnFilterChange = ({ value }: { value: string }) => void
interface SearchableSingleSelectPropTypes {
export interface SearchableSingleSelectPropTypes {
onChange: OnChange
onFilterChange: OnFilterChange
onEndReached?: () => void
onRetryClick: () => void
dense?: boolean
options: Option[]
placeholder: string
placeholder?: string
prefix?: string
showEndLoader: boolean
loading: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useCallback, useMemo } from 'react'
import { DisplayableModel } from '../../../types/models'
import {
SearchableSingleSelect,
SearchableSingleSelectPropTypes,
} from '../../SearchableSingleSelect'

const toDisplayOption = (model: DisplayableModel) => ({
value: model.id,
label: model.displayName,
})

type OwnProps<TModel> = {
selected: TModel | undefined
available: TModel[]
onChange: ({ selected }: { selected: TModel | undefined }) => void
}

export type BaseModelSingleSelectProps<TModel extends DisplayableModel> = Omit<
SearchableSingleSelectPropTypes,
keyof OwnProps<TModel> | 'options' | 'selected'
> &
OwnProps<TModel>

/* Simple wrapper component handle generic models with SingleSelect-component. */
export const BaseModelSingleSelect = <TModel extends DisplayableModel>({
available,
selected,
onChange,
...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)

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

const handleOnChange: SearchableSingleSelectPropTypes['onChange'] =
useCallback(
({ selected }) => {
// map the selected ids to the full model
const fullSelectedModel = allModelsMap.get(selected)
onChange({
selected: fullSelectedModel,
})
},
[onChange, allModelsMap]
)

return (
<SearchableSingleSelect
{...searchableSingleSelectProps}
selected={selected?.id}
options={allSingleSelectOptions}
onChange={handleOnChange}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Field } from '@dhis2/ui'
import React from 'react'
import { useField } from 'react-final-form'
import { PlainResourceQuery } from '../../../types'
import { DisplayableModel } from '../../../types/models'
import {
ModelSingleSelectProps,
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 = {
name: string
query: PlainResourceQuery
label?: string
placeholder?: string
helpText?: string
} & ModelSingleSelectProps<DisplayableModel>

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

return (
<Field
dataTest="formfields-modelsingleselect"
error={meta.invalid}
validationText={(meta.touched && meta.error?.toString()) || ''}
name={name}
label={label}
helpText={helpText}
>
<ModelSingleSelect
{...modelSingleSelectProps}
selected={input.value}
onChange={({ selected }) => {
input.onChange(selected)
input.onBlur()
}}
query={query}
/>
</Field>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useMemo, useState } from 'react'
import { useInfiniteQuery } from 'react-query'
import { useDebouncedCallback } from 'use-debounce'
import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn'
import { PlainResourceQuery } from '../../../types'
import { PagedResponse } from '../../../types/generated'
import { DisplayableModel } from '../../../types/models'
import {
BaseModelSingleSelect,
BaseModelSingleSelectProps,
} from './BaseModelSingleSelect'

type Response<Model> = PagedResponse<Model, string>

const defaultQuery = {
params: {
order: 'displayName:asc',
fields: ['id', 'displayName'],
},
}

export type ModelSingleSelectProps<TModel extends DisplayableModel> = Omit<
BaseModelSingleSelectProps<TModel>,
| 'available'
| 'onFilterChange'
| 'onRetryClick'
| 'showEndLoader'
| 'loading'
| 'error'
> & {
query: Omit<PlainResourceQuery, 'id'>
onFilterChange?: (value: string) => void
}

export const ModelSingleSelect = <TModel extends DisplayableModel>({
selected,
query,
...baseModelSingleSelectProps
}: ModelSingleSelectProps<TModel>) => {
const queryFn = useBoundResourceQueryFn()
const [searchTerm, setSearchTerm] = useState('')

const searchFilter = `identifiable:token:${searchTerm}`
const filter: string[] = searchTerm ? [searchFilter] : []
const params = query.params

const queryObject = {
...query,
params: {
...defaultQuery.params,
...params,
filter: filter.concat(params?.filter || []),
},
}
const modelName = query.resource

const queryResult = useInfiniteQuery({
queryKey: [queryObject] as const,
queryFn: queryFn<Response<TModel>>,
keepPreviousData: true,
getNextPageParam: (lastPage) =>
lastPage.pager.nextPage ? lastPage.pager.page + 1 : undefined,
getPreviousPageParam: (firstPage) =>
firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined,
})
const allDataMap = useMemo(
() => queryResult.data?.pages.flatMap((page) => page[modelName]) ?? [],
[queryResult.data, modelName]
)

const handleFilterChange = useDebouncedCallback(({ value }) => {
if (value != undefined) {
setSearchTerm(value)
}
baseModelSingleSelectProps.onFilterChange?.(value)
}, 250)

return (
<BaseModelSingleSelect
{...baseModelSingleSelectProps}
selected={selected}
available={allDataMap}
onFilterChange={handleFilterChange}
onRetryClick={queryResult.refetch}
showEndLoader={!!queryResult.hasNextPage}
loading={queryResult.isLoading}
error={queryResult.error as string | undefined}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useCallback } from 'react'
import { useQueryClient, InvalidateQueryFilters } from 'react-query'
import { PlainResourceQuery } from '../../../types'

export const useRefreshModelSingleSelect = (
query: Omit<PlainResourceQuery, 'id'>
) => {
const queryClient = useQueryClient()

return useCallback(
(invalidateFilters?: InvalidateQueryFilters) => {
queryClient.invalidateQueries([query], invalidateFilters)
},
[queryClient, query]
)
}

0 comments on commit 1c14ae7

Please sign in to comment.