diff --git a/src/components/metadataFormControls/ModelTransfer/BaseModelTransfer.tsx b/src/components/metadataFormControls/ModelTransfer/BaseModelTransfer.tsx new file mode 100644 index 00000000..49a8d268 --- /dev/null +++ b/src/components/metadataFormControls/ModelTransfer/BaseModelTransfer.tsx @@ -0,0 +1,67 @@ +import { Transfer, TransferProps } from '@dhis2/ui' +import React, { useCallback, useMemo } from 'react' +import { DisplayableModel } from '../../../types/models' + +const toDisplayOption = (model: DisplayableModel) => ({ + value: model.id, + label: model.displayName, +}) + +type OwnProps = { + selected: TModel[] + available: TModel[] + onChange: ({ selected }: { selected: TModel[] }) => void +} + +export type BaseModelTransferProps = Omit< + TransferProps, + keyof OwnProps | 'options' | 'selected' +> & + OwnProps + +/* Simple wrapper component handle generic models with Transfer-component. */ +export const BaseModelTransfer = ({ + available, + selected, + onChange, + ...transferProps +}: BaseModelTransferProps) => { + const { allModelsMap, allTransferOptions } = useMemo(() => { + const allModels = selected.concat(available) + const allModelsMap = new Map(allModels.map((o) => [o.id, o])) + const allTransferOptions = allModels.map(toDisplayOption) + return { + allModelsMap, + allTransferOptions, + } + }, [available, selected]) + + const selectedTransferValues = useMemo( + () => selected.map((s) => s.id), + [selected] + ) + + const handleOnChange = useCallback( + ({ selected }: { selected: string[] }) => { + // map the selected ids to the full model + // loop through selected to keep order + const selectedModels = selected + .map((id) => allModelsMap.get(id)) + .filter((model) => !!model) + + onChange({ + selected: selectedModels, + }) + }, + [onChange, allModelsMap] + ) + + return ( + + ) +} diff --git a/src/components/metadataFormControls/ModelTransfer/ModelTransfer.tsx b/src/components/metadataFormControls/ModelTransfer/ModelTransfer.tsx index 8629c43a..ce0ec5a9 100644 --- a/src/components/metadataFormControls/ModelTransfer/ModelTransfer.tsx +++ b/src/components/metadataFormControls/ModelTransfer/ModelTransfer.tsx @@ -1,23 +1,20 @@ -import { Transfer, TransferProps } from '@dhis2/ui' -import React, { - forwardRef, - RefAttributes, - useImperativeHandle, - useMemo, - useState, -} from 'react' +import i18n from '@dhis2/d2-i18n' +import { Button, ButtonStrip } from '@dhis2/ui' +import React, { useMemo, useState } from 'react' import { useInfiniteQuery } from 'react-query' +import { useHref } from 'react-router-dom' +import { useDebouncedCallback } from 'use-debounce' +import { getSectionNewPath } from '../../../lib' import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn' import { PlainResourceQuery } from '../../../types' import { PagedResponse } from '../../../types/generated' +import { DisplayableModel } from '../../../types/models' +import { LinkButton } from '../../LinkButton' +import { BaseModelTransfer, BaseModelTransferProps } from './BaseModelTransfer' +import css from './ModelTransfer.module.css' type Response = PagedResponse -export type DisplayableModel = { - id: string - displayName: string -} - const defaultQuery = { params: { order: 'displayName:asc', @@ -25,32 +22,23 @@ const defaultQuery = { }, } -const toDisplayOption = (model: DisplayableModel) => ({ - value: model.id, - label: model.displayName, -}) - -type OwnProps = { - selected: TModel[] - onChange: ({ selected }: { selected: TModel[] }) => void - query: PlainResourceQuery +export type ModelTranferProps = Omit< + BaseModelTransferProps, + 'available' | 'filterable' +> & { + query: Omit } -type ModelTransferProps = Omit< - TransferProps, - | keyof OwnProps - | 'options' - | 'selected' - | 'filterable' - | 'onFilterChange' -> & - OwnProps -type ImperativeRef = { refetch: () => void } - -const BaseModelTransfer = ( - { selected, onChange, query, ...transferProps }: ModelTransferProps, - ref: React.Ref -) => { +export const ModelTransfer = ({ + selected, + query, + leftHeader, + rightHeader, + leftFooter, + filterPlaceholder, + filterPlaceholderPicked, + ...baseModelTransferProps +}: ModelTranferProps) => { const queryFn = useBoundResourceQueryFn() const [searchTerm, setSearchTerm] = useState('') @@ -67,6 +55,7 @@ const BaseModelTransfer = ( }, } const modelName = query.resource + const newLink = useHref(`/${getSectionNewPath(modelName)}`) const queryResult = useInfiniteQuery({ queryKey: [queryObject] as const, @@ -77,64 +66,73 @@ const BaseModelTransfer = ( getPreviousPageParam: (firstPage) => firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined, }) - - useImperativeHandle(ref, () => ({ - refetch: () => queryResult.refetch(), - })) - const allDataMap = useMemo( - () => - new Map( - queryResult.data?.pages.flatMap((page) => - page[modelName].map((d) => [d.id, d] as const) - ) - ), + () => queryResult.data?.pages.flatMap((page) => page[modelName]) ?? [], [queryResult.data, modelName] ) - const selectedOptions = selected.map(toDisplayOption) - const loadedOptions = Array.from(allDataMap.values()).map(toDisplayOption) - // always include selected options - const allOptions = selectedOptions.concat(loadedOptions || []) - - const handleOnChange = ({ selected }: { selected: string[] }) => { - // map the selected ids to the full model - // loop through selected to keep order - const selectedModels = selected - .map((id) => allDataMap.get(id)) - .filter((model): model is TModel => !!model) - - onChange({ - selected: selectedModels, - }) - } + const handleFilterChange = useDebouncedCallback(({ value }) => { + if (value != undefined) { + setSearchTerm(value) + } + }, 250) return ( - - value !== undefined && setSearchTerm(value) + available={allDataMap} + selected={selected} + onFilterChange={handleFilterChange} + filterPlaceholder={ + filterPlaceholder || i18n.t('Filter available items') + } + filterPlaceholderPicked={ + filterPlaceholderPicked || i18n.t('Filter selected items') + } + leftHeader={{leftHeader}} + rightHeader={{rightHeader}} + leftFooter={ + leftFooter ?? ( + + ) } - selected={selectedOptions.map((o) => o.value)} - onChange={handleOnChange} + {...baseModelTransferProps} /> ) } -// this is needed to support generics with ref-forwarding -interface ModelTransferWithForwardedRef - extends React.FC> { - ( - props: ModelTransferProps & RefAttributes - ): React.ReactNode +const TransferHeader = ({ children }: { children: React.ReactNode }) => { + if (typeof children === 'string') { + return
{children}
+ } + return <>{children} } -export const ModelTransfer: ModelTransferWithForwardedRef = - forwardRef(BaseModelTransfer) +const DefaultTransferLeftFooter = ({ + onRefreshClick, + newLink, +}: { + onRefreshClick: () => void + newLink: string +}) => { + return ( + + + + + {i18n.t('Add new')} + + + ) +} diff --git a/src/components/metadataFormControls/ModelTransfer/ModelTransferField.tsx b/src/components/metadataFormControls/ModelTransfer/ModelTransferField.tsx index 2a04a0a7..cd9e4640 100644 --- a/src/components/metadataFormControls/ModelTransfer/ModelTransferField.tsx +++ b/src/components/metadataFormControls/ModelTransfer/ModelTransferField.tsx @@ -1,12 +1,9 @@ -import i18n from '@dhis2/d2-i18n' -import { Button, ButtonStrip, Field, TransferProps } from '@dhis2/ui' -import React, { useRef } from 'react' +import { Field, TransferProps } from '@dhis2/ui' +import React from 'react' import { useField } from 'react-final-form' -import { useHref } from 'react-router' -import { DisplayableModel, ModelTransfer } from '../../../components' -import { getSectionNewPath } from '../../../lib' +import { ModelTransfer } from '../../../components' import { PlainResourceQuery } from '../../../types' -import { LinkButton } from '../../LinkButton' +import { DisplayableModel } from '../../../types/models' import css from './ModelTransfer.module.css' // this currently does not need a generic, because the value of the field is not passed @@ -30,25 +27,17 @@ export function ModelTransferField({ name, query, label, - rightHeader, leftHeader, - rightFooter, + rightHeader, leftFooter, + rightFooter, filterPlaceholder, filterPlaceholderPicked, }: ModelTransferFieldProps) { - const modelName = query.resource const { input, meta } = useField(name, { multiple: true, validateFields: [], }) - const newLink = useHref(`/${getSectionNewPath(modelName)}`) - - const modelTransferHandle = useRef({ - refetch: () => { - throw new Error('Not initialized') - }, - }) return ( { input.onChange(selected) input.onBlur() }} - leftHeader={{leftHeader}} - rightHeader={{rightHeader}} - leftFooter={ - leftFooter ?? ( - - ) - } + leftHeader={leftHeader} + rightHeader={rightHeader} + leftFooter={leftFooter} rightFooter={rightFooter} + filterPlaceholder={filterPlaceholder} + filterPlaceholderPicked={filterPlaceholderPicked} query={query} - height={'350px'} - optionsWidth="500px" - selectedWidth="500px" - enableOrderChange={true} /> ) } - -const TransferHeader = ({ children }: { children: React.ReactNode }) => { - if (typeof children === 'string') { - return
{children}
- } - return <>{children} -} - -const DefaultTransferLeftFooter = ({ - onRefreshClick, - newLink, -}: { - onRefreshClick: () => void - newLink: string -}) => { - return ( - - - - - {i18n.t('Add new')} - - - ) -} diff --git a/src/types/models.ts b/src/types/models.ts index 0b0f35a1..87702648 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -1,3 +1,8 @@ // generated by https://github.com/Birkbjo/dhis2-open-api-ts export type * from './generated' export * from './systemSettings' + +export type DisplayableModel = { + id: string + displayName: string +}