Skip to content

Commit

Permalink
feat: add data element group sets New and Edit view
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammer5 committed Mar 5, 2024
1 parent 3093e89 commit af119db
Show file tree
Hide file tree
Showing 29 changed files with 1,132 additions and 16 deletions.
32 changes: 22 additions & 10 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-03-05T09:22:38.399Z\n"
"PO-Revision-Date: 2024-03-05T09:22:38.399Z\n"
"POT-Creation-Date: 2024-03-05T09:22:49.833Z\n"
"PO-Revision-Date: 2024-03-05T09:22:49.833Z\n"

msgid "schemas"
msgstr "schemas"
Expand Down Expand Up @@ -111,6 +111,9 @@ msgstr "Category combo"
msgid "None"
msgstr "None"

msgid "Filter data element groups"
msgstr "Filter data element groups"

msgid "Filter data elements"
msgstr "Filter data elements"

Expand Down Expand Up @@ -654,17 +657,20 @@ msgstr "This field requires a unique value, please choose another one"
msgid "Required"
msgstr "Required"

msgid "Custom attributes"
msgstr "Custom attributes"

msgid "Exit without saving"
msgstr "Exit without saving"

msgid "Create data element group"
msgstr "Create data element group"

msgid "Selected data elements"
msgstr "Selected data elements"
msgid "Compulsory"
msgstr "Compulsory"

msgid "Data dimension"
msgstr "Data dimension"

msgid "Selected data element groups"
msgstr "Selected data element groups"

msgid "Refresh list"
msgstr "Refresh list"
Expand All @@ -684,12 +690,18 @@ msgstr "Basic information"
msgid "Set up the information for this data element group"
msgstr "Set up the information for this data element group"

msgid "Explain the purpose of this data element group."
msgstr "Explain the purpose of this data element group."

msgid "@TODO"
msgstr "@TODO"

msgid "Custom attributes"
msgstr "Custom attributes"

msgid "Selected data elements"
msgstr "Selected data elements"

msgid "Explain the purpose of this data element group."
msgstr "Explain the purpose of this data element group."

msgid "Custom fields for your DHIS2 instance"
msgstr "Custom fields for your DHIS2 instance"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import i18n from '@dhis2/d2-i18n'
import { Transfer } from '@dhis2/ui'
import React, {
ReactElement,
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
} from 'react'
import { SelectOption } from '../../../types'
import { useInitialOptionQuery } from './useInitialOptionQuery'
import { useOptionsQuery } from './useOptionsQuery'

function computeDisplayOptions({
selected,
selectedOptions,
options,
}: {
options: SelectOption[]
selected: string[]
selectedOptions: SelectOption[]
}): SelectOption[] {
// This happens only when we haven't fetched the lable for an initially
// selected value. Don't show anything to prevent error that an option is
// missing
if (!selectedOptions.length && selected.length) {
return []
}

const missingSelectedOptions = selectedOptions.filter((selectedOption) => {
return !options?.find((option) => option.value === selectedOption.value)
})

return [...options, ...missingSelectedOptions]
}

interface DataElementGroupsSelectProps {
onChange: ({ selected }: { selected: string[] }) => void
selected: string[]
rightHeader?: ReactElement
rightFooter?: ReactElement
leftFooter?: ReactElement
leftHeader?: ReactElement
}

export const DataElementGroupsTransfer = forwardRef(
function DataElementGroupsSelect(
{
onChange,
selected,
rightHeader,
rightFooter,
leftFooter,
leftHeader,
}: DataElementGroupsSelectProps,
ref
) {
// 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 [searchTerm, setSearchTerm] = useState('')
const pageRef = useRef(0)

// 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 [selectedOptions, setSelectedOptions] = useState<SelectOption[]>(
[]
)

const optionsQuery = useOptionsQuery()
const initialOptionQuery = useInitialOptionQuery({
selected,
onComplete: setSelectedOptions,
})

const { refetch, data } = optionsQuery
const pager = data?.pager
const page = pager?.page || 0
const pageCount = pager?.pageCount || 0

const loading =
optionsQuery.fetching ||
optionsQuery.loading ||
initialOptionQuery.loading
const error =
optionsQuery.error || initialOptionQuery.error
? // @TODO: Ask Joe what do do here!
'An error has occurred. Please try again'
: ''

useImperativeHandle(
ref,
() => ({
refetch: () => {
pageRef.current = 1
refetch({ page: pageRef.current, filter: searchTerm })
},
}),
[refetch, searchTerm]
)

const adjustQueryParamsWithChangedFilter = useCallback(
({ value }: { value: string | undefined }) => {
value = value ?? ''
setSearchTerm(value)
pageRef.current = 1
refetch({ page: pageRef.current, filter: value })
},
[refetch]
)

const incrementPage = useCallback(() => {
if (optionsQuery.loading || page === pageCount) {
return
}

pageRef.current = page + 1
refetch({ page: pageRef.current, filter: searchTerm })
}, [refetch, page, optionsQuery.loading, searchTerm, pageCount])

const displayOptions = computeDisplayOptions({
selected,
selectedOptions,
options: data?.result || [],
})

return (
<Transfer
filterable
filterPlaceholder={i18n.t('Filter data element groups')}
searchTerm={searchTerm}
options={displayOptions}
selected={selected}
loading={loading}
onChange={({ selected }: { selected: string[] }) => {
const nextSelectedOptions = displayOptions.filter(
({ value }) => selected.includes(value)
)
setSelectedOptions(nextSelectedOptions)
onChange({ selected })
}}
onEndReached={incrementPage}
onFilterChange={adjustQueryParamsWithChangedFilter}
rightHeader={rightHeader}
rightFooter={rightFooter}
leftHeader={leftHeader}
leftFooter={leftFooter}
/>
)
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DataElementGroupsTransfer } from './DataElementGroupsTransfer'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {
DataElementGroup,
GistCollectionResponse,
} from '../../../types/generated'

const filterFields = ['id', 'displayName'] as const //(name is translated by default in /gist)
export type FilteredDataElementGroup = Pick<
DataElementGroup,
(typeof filterFields)[number]
>
export type DataElementGroupsQueryResult =
GistCollectionResponse<FilteredDataElementGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useDataQuery } from '@dhis2/app-runtime'
import { useRef } from 'react'
import { SelectOption } from '../../../types'
import { FilteredDataElementGroup } from './types'

type InitialDataElementGroupsQueryResult = {
dataElementGroups: {
dataElementGroups: FilteredDataElementGroup[]
}
}

export function useInitialOptionQuery({
selected,
onComplete,
}: {
onComplete: (options: SelectOption[]) => void
selected: string[]
}) {
const initialSelected = useRef(selected)
const query = {
dataElementGroups: {
resource: 'dataElementGroups',
params: {
paging: false,
fields: ['id', 'displayName'],
filter: `id:in:[${initialSelected.current.join(',')}]`,
},
},
}

return useDataQuery<InitialDataElementGroupsQueryResult>(query, {
lazy: !initialSelected.current,
variables: { id: selected },
onComplete: (data) => {
const { dataElementGroups } = data.dataElementGroups
const options = dataElementGroups.map(({ id, displayName }) => ({
value: id,
label: displayName,
}))

onComplete(options)
},
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useDataQuery } from '@dhis2/app-runtime'
import { useEffect, useMemo, useState } from 'react'
import { SelectOption } from '../../../types'
import { DataElementGroup, Pager } from '../../../types/generated'

type DataElementGroupsQueryResult = {
dataElementGroups: {
pager: Pager
dataElementGroups: DataElementGroup[]
}
}

const CATEGORY_COMBOS_QUERY = {
dataElementGroups: {
resource: 'dataElementGroups',
params: (variables: Record<string, string>) => {
const params = {
page: variables.page,
pageSize: 10,
fields: ['id', 'displayName'],
order: ['displayName'],
}

if (variables.filter) {
return {
...params,
filter: `name:ilike:${variables.filter}`,
}
}

return params
},
},
}

export function useOptionsQuery() {
const [loadedOptions, setLoadedOptions] = useState<SelectOption[]>([])
const queryResult = useDataQuery<DataElementGroupsQueryResult>(
CATEGORY_COMBOS_QUERY,
{
variables: {
page: 1,
filter: '',
},
}
)
const { data } = queryResult

// Must be done in `useEffect` and not in `onComplete`, as `onComplete`
// won't get called when useDataQuery has the values in cache already
useEffect(() => {
if (data?.dataElementGroups) {
const { pager, dataElementGroups } = data.dataElementGroups
// We want to add new options to existing ones so we don't have to
// refetch existing options
setLoadedOptions((prevLoadedOptions) => [
// We only want to add when the current page is > 1
...(pager.page === 1 ? [] : prevLoadedOptions),
...(dataElementGroups.map((catCombo) => {
const { id, displayName } = catCombo
return { value: id, label: displayName }
}) || []),
])
}
}, [data])

return useMemo(
() => ({
...queryResult,
data: {
pager: queryResult.data?.dataElementGroups.pager,
result: loadedOptions,
},
}),
[loadedOptions, queryResult]
)
}
1 change: 1 addition & 0 deletions src/components/metadataFormControls/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './AggregationLevelMultiSelect'
export * from './CategoryComboSelect'
export * from './DataElementGroupsTransfer'
export * from './DataElementsTransfer'
export * from './LegendSetTransfer'
export * from './OptionSetSelect'
15 changes: 15 additions & 0 deletions src/pages/dataElementGroupSets/Edit.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.form {
background: var(--colors-white);
padding: var(--spacers-dp16);
padding-bottom: var(--spacers-dp32);
}

.formActions {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
padding: var(--spacers-dp16);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
background: var(--colors-white);
}
Loading

0 comments on commit af119db

Please sign in to comment.