diff --git a/i18n/en.pot b/i18n/en.pot index 33f4e3df..16040710 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2023-07-31T11:36:19.094Z\n" -"PO-Revision-Date: 2023-07-31T11:36:19.094Z\n" +"POT-Creation-Date: 2023-08-03T09:45:13.203Z\n" +"PO-Revision-Date: 2023-08-03T09:45:13.203Z\n" msgid "schemas" msgstr "schemas" @@ -595,6 +595,15 @@ msgstr "" msgid "Aggregation levels" msgstr "Aggregation levels" +msgid "Aggregation level(s)" +msgstr "Aggregation level(s)" + +msgid "Custom attributes" +msgstr "Custom attributes" + +msgid "Custom fields for your DHIS2 instance" +msgstr "Custom fields for your DHIS2 instance" + msgid "Selected legends" msgstr "Selected legends" diff --git a/src/pages/dataElements/Edit.module.css b/src/pages/dataElements/Edit.module.css new file mode 100644 index 00000000..de17044a --- /dev/null +++ b/src/pages/dataElements/Edit.module.css @@ -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); +} diff --git a/src/pages/dataElements/Edit.tsx b/src/pages/dataElements/Edit.tsx index 659183a6..8e0d41a8 100644 --- a/src/pages/dataElements/Edit.tsx +++ b/src/pages/dataElements/Edit.tsx @@ -1,10 +1,57 @@ +import { Button, ButtonStrip } from '@dhis2/ui' import React from 'react' -import { useParams } from 'react-router-dom' +import { Form } from 'react-final-form' +import { useNavigate } from 'react-router-dom' +import classes from './Edit.module.css' +import { DataElementForm } from './form' + +// @TODO(DataElements/edit): I suppose we want some of the initial values to be +// dynamic? In that case, we'd have to load them and add loading/error UIs. +const INITIAL_VALUES = { + legends: [], + domain: 'aggregate', + valueType: 'number', + aggregationType: 'sum', +} export const Component = () => { - const { id } = useParams() - // could do it like this instead of separate new and edit routes - const isNew = id === 'new' + const navigate = useNavigate() + const onSubmit = (values: object) => { + console.log( + '@TODO(DataElements/edit): Implement onSubmit; values:', + values + ) + } + + return ( +
+ {({ handleSubmit }) => ( + +
+ +
+ +
+ + - return
This is a form for dataElements
+ +
+
+
+ )} + + ) } diff --git a/src/pages/dataElements/New.module.css b/src/pages/dataElements/New.module.css index 62d23d2d..77ac4df4 100644 --- a/src/pages/dataElements/New.module.css +++ b/src/pages/dataElements/New.module.css @@ -1,6 +1,7 @@ -.container { - box-sizing: border-box; - height: 100%; +.form { + background: var(--colors-white); + padding: var(--spacers-dp16); + padding-bottom: var(--spacers-dp32); } .formActions { diff --git a/src/pages/dataElements/New.tsx b/src/pages/dataElements/New.tsx index 0193b162..37819e0a 100644 --- a/src/pages/dataElements/New.tsx +++ b/src/pages/dataElements/New.tsx @@ -2,19 +2,19 @@ import { Button, ButtonStrip } from '@dhis2/ui' import React from 'react' import { Form } from 'react-final-form' import { useNavigate } from 'react-router-dom' +import { StandardFormSection } from '../../components' import { DataElementForm } from './form' import classes from './New.module.css' -// @TODO(DataElements/new): I suppose we want some of the initial values to be -// dynamic? In that case, we'd have to load them and add loading/error UIs. -const INITIAL_VALUES = { - legends: [], - domain: 'aggregate', - valueType: 'number', - aggregationType: 'sum', -} - export const Component = () => { + // @TODO(DataElements/new): values dynamic or static? + const initialValues = { + legends: [], + domain: 'aggregate', + valueType: 'number', + aggregationType: 'sum', + } + const navigate = useNavigate() const onSubmit = (values: object) => { console.log( @@ -26,29 +26,29 @@ export const Component = () => { return (
{({ handleSubmit }) => ( - +
-
-
- - + + + - - + + +
)} diff --git a/src/pages/dataElements/form/CustomAttributes.tsx b/src/pages/dataElements/form/CustomAttributes.tsx new file mode 100644 index 00000000..32f5ecf6 --- /dev/null +++ b/src/pages/dataElements/form/CustomAttributes.tsx @@ -0,0 +1,67 @@ +import { InputFieldFF, TextAreaFieldFF } from '@dhis2/ui' +import * as React from 'react' +import { Field as FieldRFF } from 'react-final-form' +import { StandardFormSection } from '../../../components' + +const inputWidth = '440px' + +export function CustomAttributes({ + attributes = [], +}: { + attributes?: Array<{ + id: string, + displayFormName: string, + // @TODO(CustomAttributes): Implement all possible value types! + valueType: 'TEXT' | 'LONG_TEXT', + code: string, + mandatory: boolean, + }>, +}) { + return ( + <> + {attributes.map((attribute) => { + const { + mandatory: required, + valueType, + displayFormName, + id, + } = attribute + + // @TODO(CustomAttributes): What to use as name? + const name = `attribute.${id}` + + if (valueType === 'TEXT') { + return ( + + + + ) + } + + if (valueType === 'LONG_TEXT') { + return ( + + + + ) + } + + throw new Error( + `@TODO(CustomAttributes): Implement value type "${valueType}"!` + ) + })} + + ) +} diff --git a/src/pages/dataElements/form/DataElementForm.module.css b/src/pages/dataElements/form/DataElementForm.module.css deleted file mode 100644 index 3c5fef00..00000000 --- a/src/pages/dataElements/form/DataElementForm.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.container { - background: var(--colors-white); - padding: var(--spacers-dp16); - padding: 16px; - height: 100%; - box-sizing: border-box; -} diff --git a/src/pages/dataElements/form/DataElementForm.stories.tsx b/src/pages/dataElements/form/DataElementForm.stories.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pages/dataElements/form/DataElementForm.tsx b/src/pages/dataElements/form/DataElementForm.tsx index 563c2571..fb3f9a54 100644 --- a/src/pages/dataElements/form/DataElementForm.tsx +++ b/src/pages/dataElements/form/DataElementForm.tsx @@ -2,6 +2,7 @@ import i18n from '@dhis2/d2-i18n' import { InputFieldFF, SingleSelectFieldFF, + MultiSelectFieldFF, TextAreaFieldFF, } from '@dhis2/ui' import React, { useState } from 'react' @@ -17,270 +18,300 @@ import { DomainField, LegendsField, } from './custom-fields' -import classes from './DataElementForm.module.css' +import { CustomAttributes } from './CustomAttributes' import { EditableFieldWrapper } from './EditableFieldWrapper' import { - useCategoryCombosQuery, - useOptionSetsQuery, - useOptionSetCommentsQuery, - useLegendsQuery, - useAddLegendMutation, + useAddAggregationLevelMutation, useAddCategoryComboMutation, - useAddOptionSetMutation, + useAddLegendMutation, useAddOptionSetCommentMutation, + useAddOptionSetMutation, + useAggregationLevelsQuery, + useCategoryCombosQuery, + useCustomAttributesQuery, + useLegendsQuery, + useOptionSetCommentsQuery, + useOptionSetsQuery, } from './hooks' export function DataElementForm() { + const customAttributes = useCustomAttributesQuery() const categoryCombos = useCategoryCombosQuery() const optionSets = useOptionSetsQuery() const optionSetComments = useOptionSetCommentsQuery() const legends = useLegendsQuery() + const aggregationLevels = useAggregationLevelsQuery() const [addingLegend, setAddingLegend] = useState(false) const [addingCategoryCombo, setAddingCategoryCombo] = useState(false) const [addingOptionSet, setAddingOptionSet] = useState(false) const [addingOptionSetComment, setAddingOptionSetComment] = useState(false) + const [addingAggregationLevel, setAddingAggregationLevel] = useState(false) // @TODO(DataElementForm): Use these - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /* eslint-disable @typescript-eslint/no-unused-vars */ const [addLegend] = useAddLegendMutation() - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [addCategoryCombo] = useAddCategoryComboMutation() - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [addOptionSet] = useAddOptionSetMutation() - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [addOptionSetComment] = useAddOptionSetCommentMutation() + const [addAggregationLevel] = useAddAggregationLevelMutation() + /* eslint-enable @typescript-eslint/no-unused-vars */ - if ( + const loading = ( categoryCombos.loading || optionSets.loading || optionSetComments.loading || - legends.loading - ) { - return <>@TODO(DataElementForm): Loading - } - - if ( + legends.loading || + aggregationLevels.loading || + customAttributes.loading + ) + const error = ( categoryCombos.error || optionSets.error || optionSetComments.error || - legends.error - ) { - return <>@TODO(DataElementForm): Error + legends.error || + aggregationLevels.error || + customAttributes.error + ) + + if (loading) { + return <>@TODO(DataElementForm): Loading + } + + if (error) { + return ( + <> + @TODO(DataElementForm): Error
+ {error.toString()} + + ) } return ( <> - {addingLegend && `@TODO(DataElementForm): add Modal(?) for adding a new legend`} - {addingCategoryCombo && `@TODO(DataElementForm): add Modal(?) for adding a new category combo`} - {addingOptionSet && `@TODO(DataElementForm): add Modal(?) for adding a new option set`} - {addingOptionSetComment && `@TODO(DataElementForm): add Modal(?) for adding a new option set combo`} + + {i18n.t('Basic information')} + {i18n.t('Set up the information for this data element')} -
- - {i18n.t('Basic information')} - {i18n.t('Set up the information for this data element')} + + + - - alert('@TODO(DataElementForm): Implement me! (13)')} - /> - + + + - - alert('@TODO(DataElementForm): Implement me! (14)')} - /> - + + + - - alert('@TODO(DataElementForm): Implement me! (15)')} - /> - + + + - - - + + + - - alert('@TODO(DataElementForm): Implement me! (17)')} - /> - + + + - - - + + + - - alert('@TODO(DataElementForm): Implement me! (18)')} - /> - + + + + + + + + + + + - + + + + + + + {i18n.t('Disaggregation and Option sets')} + {i18n.t('Set up disaggregation and predefined options.')} + + + setAddingCategoryCombo(true)} + > alert('@TODO(DataElementForm): Implement me! (19)')} + label={i18n.t('Category combination (required)')} + helpText={i18n.t('Choose how this data element is disaggregated')} + options={categoryCombos.data} /> - + + - - - - - + + setAddingOptionSet(true)} + > - + + - + + setAddingOptionSetComment(true)} + > - - + + + - - {i18n.t('Disaggregation and Option sets')} - {i18n.t('Set up disaggregation and predefined options.')} + + {i18n.t('Legends')} + {i18n.t('Visualize values for this data element in Analytics app. Multiple legends can be applied.')} - - setAddingCategoryCombo(true)} - > - - - + + setAddingLegend(true)} + /> + + - - setAddingOptionSet(true)} - > - - - + + {i18n.t('Aggregation levels')} + + {` + @TODO(DataElementForm): Help text to describe the aggregation levels + functionality. It appears as if this section hasn't been + finalized yet by Joe, so I guess we'll have to talk about + this particluar part. + `} + - - setAddingOptionSetComment(true)} - > - - - - + + setAddingAggregationLevel(true)} + > + + + + + {customAttributes.data?.length && ( - {i18n.t('Legends')} - {i18n.t('Visualize values for this data element in Analytics app. Multiple legends can be applied.')} + {i18n.t('Custom attributes')} + {i18n.t('Custom fields for your DHIS2 instance')} - - setAddingLegend(true)} - /> - + + )} - - {i18n.t('Aggregation levels')} - - {` - @TODO(DataElementForm): Help text to describe the aggregation levels - functionality. It appears as if this section hasn't been - finalized yet by Joe, so I guess we'll have to talk about - this particluar part. - `} - - -
+ {addingLegend && `@TODO(DataElementForm): add Modal(?) for adding a new legend`} + {addingCategoryCombo && `@TODO(DataElementForm): add Modal(?) for adding a new category combo`} + {addingOptionSet && `@TODO(DataElementForm): add Modal(?) for adding a new option set`} + {addingOptionSetComment && `@TODO(DataElementForm): add Modal(?) for adding a new option set combo`} + {addingAggregationLevel && `@TODO(DataElementForm): add Modal(?) for adding a new aggregation level`} ) } diff --git a/src/pages/dataElements/form/hooks.ts b/src/pages/dataElements/form/hooks.ts index 85625d2c..61ad19c8 100644 --- a/src/pages/dataElements/form/hooks.ts +++ b/src/pages/dataElements/form/hooks.ts @@ -1,3 +1,6 @@ +import { useDataQuery } from '@dhis2/app-runtime' +import { useMemo } from 'react' + export function useCategoryCombosQuery() { return { loading: false, @@ -65,3 +68,61 @@ export function useAddLegendMutation() { data: null, }] } + +export function useAggregationLevelsQuery() { + return { + loading: false, + error: null, + data: [], + refetch: () => alert('@TODO(data element form hooks): Implement me!') + } +} + +export function useAddAggregationLevelMutation() { + return [() => alert('@TODO(data element form hooks): Implement me!'), { + loading: false, + error: null, + data: null, + }] +} + +const CUSTOM_ATTRIBUTES_QUERY = { + attributes: { + resource: 'attributes', + params: { + fields: ['*', 'optionSet[*]'], + paging: false, + filter: 'dataElementAttribute:eq:true', + }, + }, +} + +// @TODO(useCustomAttributesQuery): create response type +interface QueryResponse { + attributes: { + attributes: Array<{ + id: string, + displayFormName: string, + // @TODO(CustomAttributes): Implement all possible value types! + valueType: 'TEXT' | 'LONG_TEXT', + code: string, + mandatory: boolean, + }>, + } +} + +export function useCustomAttributesQuery() { + const customAttributes = useDataQuery(CUSTOM_ATTRIBUTES_QUERY) + console.log('> customAttributes', customAttributes) + + const data = useMemo(() => { + return customAttributes.data?.attributes.attributes.map(attribute => { + return attribute + }) || [] + }, [customAttributes.data]) + + return useMemo(() => ({ + ...customAttributes, + data, + }), [customAttributes, data]) +}