-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: modelSingleSelect refactor
- Loading branch information
Showing
6 changed files
with
266 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
src/components/metadataFormControls/ModelSingleSelect/BaseModelSingleSelect.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
) | ||
} |
53 changes: 53 additions & 0 deletions
53
src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
90 changes: 90 additions & 0 deletions
90
src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectRefactor.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
) | ||
} |
16 changes: 16 additions & 0 deletions
16
src/components/metadataFormControls/ModelSingleSelect/useRefreshSingleSelect.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
) | ||
} |