From cc66cbdae710a434fafdfb1f2efe78413ba83a9c Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 4 Oct 2023 10:47:28 +0200 Subject: [PATCH 01/10] fix(edit de): allow removing nested values --- src/pages/dataElements/New.tsx | 6 ++++-- src/pages/dataElements/edit/createJsonPatchOperations.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/dataElements/New.tsx b/src/pages/dataElements/New.tsx index 0a352f2f..35251698 100644 --- a/src/pages/dataElements/New.tsx +++ b/src/pages/dataElements/New.tsx @@ -26,8 +26,10 @@ function computeInitialValues(customAttributes: Attribute[]) { code: '', description: '', url: '', - color: '', - icon: '', + style: { + color: '', + icon: '', + }, fieldMask: '', domainType: 'AGGREGATE', formName: '', diff --git a/src/pages/dataElements/edit/createJsonPatchOperations.ts b/src/pages/dataElements/edit/createJsonPatchOperations.ts index 11feec85..c8937c37 100644 --- a/src/pages/dataElements/edit/createJsonPatchOperations.ts +++ b/src/pages/dataElements/edit/createJsonPatchOperations.ts @@ -46,6 +46,6 @@ export function createJsonPatchOperations({ return adjustedDirtyFieldsKeys.map((name) => ({ op: get(name, dataElement) ? 'replace' : 'add', path: `/${name.replace(/[.]/g, '/')}`, - value: get(name, values), + value: get(name, values) || '', })) } From f557dd0f392cf6cafbab0a199f7d1c138a1c6ff1 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 4 Oct 2023 12:57:19 +0200 Subject: [PATCH 02/10] fix(searchable single selects): add posibility to deselect --- .../CategoryComboSelect.tsx | 15 ++++++------ .../CategoryComboSelect/useOptionsQuery.ts | 6 ++++- .../ModelSingleSelect/ModelSingleSelect.tsx | 23 ++++++++++++------- .../ModelSingleSelect/index.ts | 2 +- .../OptionSetSelect/OptionSetSelect.tsx | 15 ++++++------ src/pages/dataElements/New.tsx | 1 - .../dataElements/form/CustomAttributes.tsx | 5 ++-- src/pages/dataElements/form/customFields.tsx | 1 + 8 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/components/metadataFormControls/CategoryComboSelect/CategoryComboSelect.tsx b/src/components/metadataFormControls/CategoryComboSelect/CategoryComboSelect.tsx index c2d1a050..869f7e2a 100644 --- a/src/components/metadataFormControls/CategoryComboSelect/CategoryComboSelect.tsx +++ b/src/components/metadataFormControls/CategoryComboSelect/CategoryComboSelect.tsx @@ -1,22 +1,20 @@ import i18n from '@dhis2/d2-i18n' import React, { forwardRef } from 'react' import { ModelSingleSelect } from '../ModelSingleSelect' +import type { ModelSingleSelectProps } from '../ModelSingleSelect' import { useInitialOptionQuery } from './useInitialOptionQuery' import { useOptionsQuery } from './useOptionsQuery' -interface CategoryComboSelectProps { - onChange: ({ selected }: { selected: string }) => void - placeholder?: string - selected?: string - showAllOption?: boolean - onBlur?: () => void - onFocus?: () => void -} +type CategoryComboSelectProps = Omit< + ModelSingleSelectProps, + 'useInitialOptionQuery' | 'useOptionsQuery' +> export const CategoryComboSelect = forwardRef(function CategoryComboSelect( { onChange, placeholder = i18n.t('Category combo'), + required, selected, showAllOption, onBlur, @@ -27,6 +25,7 @@ export const CategoryComboSelect = forwardRef(function CategoryComboSelect( return ( value === selected ) - if (selectedOption && !optionsContainSelected) { - return [...options, selectedOption] + const withSelectedOption = + selectedOption && !optionsContainSelected + ? [...options, selectedOption] + : options + + if (!required) { + return [{ value: '', label: i18n.t('None') }, ...withSelectedOption] } - return options + return withSelectedOption } type UseInitialOptionQuery = ({ @@ -44,8 +52,9 @@ type UseInitialOptionQuery = ({ selected?: string }) => QueryResponse -interface ModelSingleSelectProps { +export interface ModelSingleSelectProps { onChange: ({ selected }: { selected: string }) => void + required?: boolean placeholder?: string selected?: string showAllOption?: boolean @@ -55,14 +64,11 @@ interface ModelSingleSelectProps { useOptionsQuery: () => QueryResponse } -export interface ModelSingleSelectHandle { - refetch: () => void -} - export const ModelSingleSelect = forwardRef(function ModelSingleSelect( { onChange, placeholder = '', + required, selected, showAllOption, onBlur, @@ -134,6 +140,7 @@ export const ModelSingleSelect = forwardRef(function ModelSingleSelect( const displayOptions = computeDisplayOptions({ selected, selectedOption, + required, options: result, }) diff --git a/src/components/metadataFormControls/ModelSingleSelect/index.ts b/src/components/metadataFormControls/ModelSingleSelect/index.ts index 077b733f..d5dbc231 100644 --- a/src/components/metadataFormControls/ModelSingleSelect/index.ts +++ b/src/components/metadataFormControls/ModelSingleSelect/index.ts @@ -1 +1 @@ -export { ModelSingleSelect } from './ModelSingleSelect' +export * from './ModelSingleSelect' diff --git a/src/components/metadataFormControls/OptionSetSelect/OptionSetSelect.tsx b/src/components/metadataFormControls/OptionSetSelect/OptionSetSelect.tsx index a84bc158..4adbd951 100644 --- a/src/components/metadataFormControls/OptionSetSelect/OptionSetSelect.tsx +++ b/src/components/metadataFormControls/OptionSetSelect/OptionSetSelect.tsx @@ -1,22 +1,20 @@ import i18n from '@dhis2/d2-i18n' import React, { forwardRef } from 'react' import { ModelSingleSelect } from '../ModelSingleSelect' +import type { ModelSingleSelectProps } from '../ModelSingleSelect' import { useInitialOptionQuery } from './useInitialOptionQuery' import { useOptionsQuery } from './useOptionsQuery' -interface OptionSetSelectProps { - onChange: ({ selected }: { selected: string }) => void - placeholder?: string - selected?: string - showAllOption?: boolean - onBlur?: () => void - onFocus?: () => void -} +type OptionSetSelectProps = Omit< + ModelSingleSelectProps, + 'useInitialOptionQuery' | 'useOptionsQuery' +> export const OptionSetSelect = forwardRef(function OptionSetSelect( { onChange, placeholder = i18n.t('Option set'), + required, selected, showAllOption, onBlur, @@ -27,6 +25,7 @@ export const OptionSetSelect = forwardRef(function OptionSetSelect( return ( { variables: payload, }) } catch (e) { - console.log('> e', e) return { [FORM_ERROR]: (e as Error | string).toString() } } diff --git a/src/pages/dataElements/form/CustomAttributes.tsx b/src/pages/dataElements/form/CustomAttributes.tsx index 2eaf1bd9..ce0b0326 100644 --- a/src/pages/dataElements/form/CustomAttributes.tsx +++ b/src/pages/dataElements/form/CustomAttributes.tsx @@ -64,9 +64,8 @@ function CustomAttribute({ attribute, index }: CustomAttributeProps) { ) } - throw new Error( - `@TODO(CustomAttributes): Implement value type "${attribute.valueType}"!` - ) + // @TODO: Verify that all value types have been covered! + throw new Error(`Implement value type "${attribute.valueType}"!`) } export function CustomAttributes({ diff --git a/src/pages/dataElements/form/customFields.tsx b/src/pages/dataElements/form/customFields.tsx index 757850e2..2bce4ada 100644 --- a/src/pages/dataElements/form/customFields.tsx +++ b/src/pages/dataElements/form/customFields.tsx @@ -251,6 +251,7 @@ export function CategoryComboField() { validationText={meta.error} > Date: Wed, 4 Oct 2023 13:05:04 +0200 Subject: [PATCH 03/10] fix(color and icon picker): add remove icon btn & adjust styles according to specs --- .../ColorAndIconPicker/ColorAndIconPicker.tsx | 1 - .../ColorAndIconPicker/ColorPicker.module.css | 5 +- .../ColorAndIconPicker/ColorPicker.tsx | 57 ++----------------- .../ColorAndIconPicker/IconPicker.module.css | 14 +++-- .../ColorAndIconPicker/IconPicker.tsx | 4 +- .../ColorAndIconPicker/IconPickerModal.tsx | 12 +++- .../ColorAndIconPicker/availableColors.ts | 41 +++++++++++++ 7 files changed, 67 insertions(+), 67 deletions(-) create mode 100644 src/components/ColorAndIconPicker/availableColors.ts diff --git a/src/components/ColorAndIconPicker/ColorAndIconPicker.tsx b/src/components/ColorAndIconPicker/ColorAndIconPicker.tsx index cf0321c5..8260e559 100644 --- a/src/components/ColorAndIconPicker/ColorAndIconPicker.tsx +++ b/src/components/ColorAndIconPicker/ColorAndIconPicker.tsx @@ -17,7 +17,6 @@ export function ColorAndIconPicker({ return (
-
) diff --git a/src/components/ColorAndIconPicker/ColorPicker.module.css b/src/components/ColorAndIconPicker/ColorPicker.module.css index 77075001..3ffea902 100644 --- a/src/components/ColorAndIconPicker/ColorPicker.module.css +++ b/src/components/ColorAndIconPicker/ColorPicker.module.css @@ -3,7 +3,7 @@ gap: var(--spacers-dp8); padding: var(--spacers-dp8); border-radius: 3px; - border: 1px solid var(--colors-grey600); + border: 1px solid var(--colors-grey500); background: var(--colors-white); width: 68px; cursor: pointer; @@ -15,7 +15,8 @@ justify-content: center; height: 26px; width: 26px; - border: 1px solid var(--colors-black); + border: 1px dashed var(--colors-grey400); + border-radius: 2px; } .hasColor .chosenColor { diff --git a/src/components/ColorAndIconPicker/ColorPicker.tsx b/src/components/ColorAndIconPicker/ColorPicker.tsx index 36857731..34ee79e9 100644 --- a/src/components/ColorAndIconPicker/ColorPicker.tsx +++ b/src/components/ColorAndIconPicker/ColorPicker.tsx @@ -1,57 +1,10 @@ -import { - IconChevronDown16, - IconChevronUp16, - IconEmptyFrame24, - Layer, - Popper, -} from '@dhis2/ui' +import { IconChevronDown16, IconChevronUp16, Layer, Popper } from '@dhis2/ui' import cx from 'classnames' import React, { useRef, useState } from 'react' import { SwatchesPicker } from 'react-color' +import { AVAILABLE_COLORS } from './availableColors' import classes from './ColorPicker.module.css' -const COLORS = [ - [ - '#ffcdd2', - '#e57373', - '#d32f2f', - '#f06292', - '#c2185b', - '#880e4f', - '#f50057', - ], - [ - '#e1bee7', - '#ba68c8', - '#8e24aa', - '#aa00ff', - '#7e57c2', - '#4527a0', - '#7c4dff', - '#6200ea', - ], - ['#c5cae9', '#7986cb', '#3949ab', '#304ffe'], - ['#e3f2fd', '#64b5f6', '#1976d2', '#0288d1'], - ['#40c4ff', '#00b0ff', '#80deea'], - ['#00acc1', '#00838f', '#006064'], - ['#e0f2f1', '#80cbc4', '#00695c', '#64ffda'], - ['#c8e6c9', '#66bb6a', '#2e7d32', '#1b5e20'], - ['#00e676', '#aed581', '#689f38', '#33691e'], - ['#76ff03', '#64dd17', '#cddc39', '#9e9d24', '#827717'], - [ - '#fff9c4', - '#fbc02d', - '#f57f17', - '#ffff00', - '#ffcc80', - '#ffccbc', - '#ffab91', - ], - ['#bcaaa4', '#8d6e63', '#4e342e'], - ['#fafafa', '#bdbdbd', '#757575', '#424242'], - ['#cfd8dc', '#b0bec5', '#607d8b', '#37474f'], -] - export function ColorPicker({ onColorPick, color = '', @@ -74,9 +27,7 @@ export function ColorPicker({
- {!color && } -
+ />
{showPicker ? : } @@ -88,7 +39,7 @@ export function ColorPicker({
- {!icon && } {selectedIcon && ( setShowPicker(false)} onChange={({ icon }) => { onIconPick({ icon }) diff --git a/src/components/ColorAndIconPicker/IconPickerModal.tsx b/src/components/ColorAndIconPicker/IconPickerModal.tsx index 00227bff..7cc1fd39 100644 --- a/src/components/ColorAndIconPicker/IconPickerModal.tsx +++ b/src/components/ColorAndIconPicker/IconPickerModal.tsx @@ -19,15 +19,17 @@ import { useIconsQuery, Icon } from './useIconsQuery' type TabName = 'all' | 'positive' | 'negative' | 'outline' export function IconPickerModal({ + selected, onChange, onCancel, }: { + selected: string onChange: ({ icon }: { icon: string }) => void onCancel: () => void }) { const [searchValue, setSearchValue] = useState('') const [activeTab, setActiveTab] = useState('all') - const [icon, setIcon] = useState('') + const [icon, setIcon] = useState(selected) const icons = useIconsQuery() const displayIcons = searchValue ? filterIcons(icons.data[activeTab], searchValue) @@ -107,11 +109,15 @@ export function IconPickerModal({ disabled={!icon} onClick={() => onChange({ icon })} > - Select + {i18n.t('Select')} + + + diff --git a/src/components/ColorAndIconPicker/availableColors.ts b/src/components/ColorAndIconPicker/availableColors.ts new file mode 100644 index 00000000..81b1977c --- /dev/null +++ b/src/components/ColorAndIconPicker/availableColors.ts @@ -0,0 +1,41 @@ +export const AVAILABLE_COLORS = [ + [ + '#ffcdd2', + '#e57373', + '#d32f2f', + '#f06292', + '#c2185b', + '#880e4f', + '#f50057', + ], + [ + '#e1bee7', + '#ba68c8', + '#8e24aa', + '#aa00ff', + '#7e57c2', + '#4527a0', + '#7c4dff', + '#6200ea', + ], + ['#c5cae9', '#7986cb', '#3949ab', '#304ffe'], + ['#e3f2fd', '#64b5f6', '#1976d2', '#0288d1'], + ['#40c4ff', '#00b0ff', '#80deea'], + ['#00acc1', '#00838f', '#006064'], + ['#e0f2f1', '#80cbc4', '#00695c', '#64ffda'], + ['#c8e6c9', '#66bb6a', '#2e7d32', '#1b5e20'], + ['#00e676', '#aed581', '#689f38', '#33691e'], + ['#76ff03', '#64dd17', '#cddc39', '#9e9d24', '#827717'], + [ + '#fff9c4', + '#fbc02d', + '#f57f17', + '#ffff00', + '#ffcc80', + '#ffccbc', + '#ffab91', + ], + ['#bcaaa4', '#8d6e63', '#4e342e'], + ['#fafafa', '#bdbdbd', '#757575', '#424242'], + ['#cfd8dc', '#b0bec5', '#607d8b', '#37474f'], +] From 402de21fa4ac08a7ba9cb3f6c7834fa8852a2bb1 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 4 Oct 2023 13:07:01 +0200 Subject: [PATCH 04/10] feat(de form): add aggregation level intro text --- i18n/en.pot | 35 ++++++++++--------- .../form/DataElementFormFields.tsx | 14 ++++---- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index fc9d9f30..64ac5e45 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-10-11T10:29:42.492Z\n" -"PO-Revision-Date: 2023-10-11T10:29:42.492Z\n" +"POT-Creation-Date: 2023-10-25T11:41:05.785Z\n" +"PO-Revision-Date: 2023-10-25T11:41:05.785Z\n" msgid "schemas" msgstr "schemas" @@ -48,6 +48,15 @@ msgstr "Search for menu items" msgid "Search icons" msgstr "Search icons" +msgid "Select" +msgstr "Select" + +msgid "Remove icon" +msgstr "Remove icon" + +msgid "Cancel" +msgstr "Cancel" + msgid "Retry" msgstr "Retry" @@ -69,12 +78,15 @@ msgstr "Aggregation level(s)" msgid "Category combo" msgstr "Category combo" -msgid "None" -msgstr "None" +msgid "Default (none)" +msgstr "Default (none)" msgid "Filter legend sets" msgstr "Filter legend sets" +msgid "None" +msgstr "None" + msgid "Option set" msgstr "Option set" @@ -108,9 +120,6 @@ msgstr "Type to filter options" msgid "No matches" msgstr "No matches" -msgid "Data set" -msgstr "Data set" - msgid "Clear all filters" msgstr "Clear all filters" @@ -135,9 +144,6 @@ msgstr "Failed to save" msgid "Manage {{section}} table columns" msgstr "Manage {{section}} table columns" -msgid "Cancel" -msgstr "Cancel" - msgid "Update table columns" msgstr "Update table columns" @@ -213,6 +219,9 @@ msgstr "Data element group set" msgid "Data element group sets" msgstr "Data element group sets" +msgid "Data set" +msgstr "Data set" + msgid "Data sets" msgstr "Data sets" @@ -621,9 +630,6 @@ msgstr "{{fieldLabel}} (required)" msgid "A data element name should be concise and easy to recognize." msgstr "A data element name should be concise and easy to recognize." -msgid "Short name" -msgstr "Short name" - msgid "Often used in reports where space is limited" msgstr "Often used in reports where space is limited" @@ -704,9 +710,6 @@ msgstr "Refresh list" msgid "Add new" msgstr "Add new" -msgid "Value type" -msgstr "Value type" - msgid "The type of data that will be recorded." msgstr "The type of data that will be recorded." diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 8fa7613a..4cf302b1 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -217,12 +217,14 @@ export function DataElementFormFields() { {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. - `} + By default, the aggregation will start at the lowest + assigned organisation unit. If you for example select + "Chiefdom", it means that "Chiefdom", + "District" and "National" aggregates use + "Chiefdom" (the highest aggregation level + available) as the data source, and PHU data will not be + included. PHU will still be available for the PHU level, but + not included in the aggregations to the levels above. From 32f7e20a1ae4f7a20d5971c7546ba3de42b38aa7 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 4 Oct 2023 17:15:56 +0200 Subject: [PATCH 05/10] feat(de edit and new): handle loading and error states correctly --- i18n/en.pot | 10 +- src/pages/dataElements/Edit.tsx | 100 +++++++++++------- src/pages/dataElements/New.tsx | 49 ++++----- .../form/DataElementFormFields.tsx | 25 ++--- 4 files changed, 96 insertions(+), 88 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 64ac5e45..b1b577d6 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-10-25T11:41:05.785Z\n" -"PO-Revision-Date: 2023-10-25T11:41:05.785Z\n" +"POT-Creation-Date: 2023-10-25T11:43:32.223Z\n" +"PO-Revision-Date: 2023-10-25T11:43:32.223Z\n" msgid "schemas" msgstr "schemas" @@ -606,6 +606,9 @@ msgstr "Owner" msgid "Zero is significant" msgstr "Zero is significant" +msgid "Custom attributes" +msgstr "Custom attributes" + msgid "Something went wrong when submitting the form" msgstr "Something went wrong when submitting the form" @@ -682,9 +685,6 @@ msgstr "" msgid "Aggregation levels" msgstr "Aggregation levels" -msgid "Custom attributes" -msgstr "Custom attributes" - msgid "Custom fields for your DHIS2 instance" msgstr "Custom fields for your DHIS2 instance" diff --git a/src/pages/dataElements/Edit.tsx b/src/pages/dataElements/Edit.tsx index 767a2cdd..85adb34c 100644 --- a/src/pages/dataElements/Edit.tsx +++ b/src/pages/dataElements/Edit.tsx @@ -5,7 +5,11 @@ import { FORM_ERROR, FormApi } from 'final-form' import React, { useEffect, useRef } from 'react' import { withTypes } from 'react-final-form' import { useNavigate, useParams } from 'react-router-dom' -import { StandardFormActions, StandardFormSection } from '../../components' +import { + Loader, + StandardFormActions, + StandardFormSection, +} from '../../components' import { SCHEMA_SECTIONS, getSectionPath } from '../../lib' import { JsonPatchOperation } from '../../types' import { Attribute, DataElement } from '../../types/generated' @@ -40,12 +44,18 @@ function useDataElementQuery(id: string) { } function computeInitialValues({ + dataElementId, dataElement, customAttributes, }: { + dataElementId: string dataElement: DataElement customAttributes: Attribute[] }) { + if (!dataElement) { + return {} + } + // We want to have an array in the state with all attributes, not just the // ones that have a value which is what the endpoint responds with const attributeValues = customAttributes.map((attribute) => { @@ -62,6 +72,7 @@ function computeInitialValues({ }) return { + id: dataElementId, name: dataElement.name, shortName: dataElement.shortName, code: dataElement.code, @@ -86,34 +97,22 @@ function computeInitialValues({ } } -export const Component = () => { +function usePatchDirtyFields() { const dataEngine = useDataEngine() - const navigate = useNavigate() - const params = useParams() - - const dataElementId = params.id as string - const dataElementQuery = useDataElementQuery(dataElementId) - const customAttributesQuery = useCustomAttributesQuery() - const loading = dataElementQuery.loading || customAttributesQuery.loading - const error = dataElementQuery.error || customAttributesQuery.error - - if (error && !loading) { - // @TODO(Edit): Implement error screen - return `Error: ${error.toString()}` - } - - if (loading) { - // @TODO(Edit): Implement loading screen - return 'Loading...' - } - - async function onSubmit(values: FormValues, form: FinalFormFormApi) { - const dirtyFields = form.getState().dirtyFields + return async ({ + values, + dirtyFields, + dataElement, + }: { + values: FormValues + dirtyFields: { [name: string]: boolean } + dataElement: DataElement + }) => { const jsonPatchPayload = createJsonPatchOperations({ values, dirtyFields, - dataElement: dataElementQuery.data?.dataElement as DataElement, + dataElement, }) // We want the promise so we know when submitting is done. The promise @@ -121,7 +120,7 @@ export const Component = () => { // resolve const ADD_NEW_DATA_ELEMENT_MUTATION = { resource: 'dataElements', - id: dataElementId, + id: values.id, type: 'json-patch', data: ({ operations }: { operations: JsonPatchOperation[] }) => operations, @@ -134,26 +133,55 @@ export const Component = () => { } catch (e) { return { [FORM_ERROR]: (e as Error | string).toString() } } + } +} + +export const Component = () => { + const navigate = useNavigate() + const params = useParams() + const dataElementId = params.id as string + const dataElementQuery = useDataElementQuery(dataElementId) + const customAttributesQuery = useCustomAttributesQuery() + const patchDirtyFields = usePatchDirtyFields() + + async function onSubmit(values: FormValues, form: FinalFormFormApi) { + const errors = await patchDirtyFields({ + values, + dirtyFields: form.getState().dirtyFields, + dataElement: dataElementQuery.data?.dataElement as DataElement, + }) + + if (errors) { + return errors + } navigate(listPath) } const initialValues = computeInitialValues({ + dataElementId, dataElement: dataElementQuery.data?.dataElement as DataElement, - customAttributes: customAttributesQuery.data, + customAttributes: customAttributesQuery.data || [], }) return ( -
- {({ handleSubmit, submitting, submitError }) => ( - - - - )} - + + +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + )} + +
+
) } diff --git a/src/pages/dataElements/New.tsx b/src/pages/dataElements/New.tsx index 0543277e..8f03acba 100644 --- a/src/pages/dataElements/New.tsx +++ b/src/pages/dataElements/New.tsx @@ -5,7 +5,11 @@ import { FORM_ERROR } from 'final-form' import React, { useEffect, useRef } from 'react' import { Form } from 'react-final-form' import { useNavigate } from 'react-router-dom' -import { StandardFormActions, StandardFormSection } from '../../components' +import { + Loader, + StandardFormActions, + StandardFormSection, +} from '../../components' import { SCHEMA_SECTIONS, getSectionPath } from '../../lib' import { Attribute } from '../../types/generated' import { DataElementFormFields, useCustomAttributesQuery } from './form' @@ -80,27 +84,11 @@ function formatFormValues({ values }: { values: FormValues }) { } } -// @TODO(DataElements/new): values dynamic or static? export const Component = () => { const dataEngine = useDataEngine() - const navigate = useNavigate() const customAttributesQuery = useCustomAttributesQuery() - - const loading = customAttributesQuery.loading - const error = customAttributesQuery.error - - if (error && !loading) { - // @TODO(Edit): Implement error screen - return <>Error: {error.toString()} - } - - if (loading) { - // @TODO(Edit): Implement loading screen - return <>Loading... - } - - const initialValues = computeInitialValues(customAttributesQuery.data) + const initialValues = computeInitialValues(customAttributesQuery.data || []) async function onSubmit(values: FormValues) { const payload = formatFormValues({ values }) @@ -120,16 +108,21 @@ export const Component = () => { } return ( -
- {({ handleSubmit, submitting, submitError }) => ( - - - - )} - + +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + )} + +
) } diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 4cf302b1..43fb642f 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -3,6 +3,7 @@ import { CheckboxFieldFF, InputFieldFF, TextAreaFieldFF } from '@dhis2/ui' import React from 'react' import { Field as FieldRFF } from 'react-final-form' import { + Loader, StandardFormSection, StandardFormSectionTitle, StandardFormSectionDescription, @@ -25,25 +26,11 @@ import { useCustomAttributesQuery } from './useCustomAttributesQuery' export function DataElementFormFields() { const customAttributes = useCustomAttributesQuery() - const loading = customAttributes.loading - const error = customAttributes.error - - if (loading) { - return <>@TODO(DataElementForm): Loading - } - - if (error) { - return ( - <> - @TODO(DataElementForm): Error -
- {error.toString()} - - ) - } - return ( - <> + {i18n.t('Basic information')} @@ -246,6 +233,6 @@ export function DataElementFormFields() { /> )} - + ) } From 7e5c907b71b88b593fed77e8f730b4fe33661a9e Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 17 Oct 2023 14:45:47 +0200 Subject: [PATCH 06/10] fix(searchable single selects): use old-app default labels --- .../CategoryComboSelect/useOptionsQuery.ts | 4 +--- .../ModelSingleSelect/ModelSingleSelect.tsx | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/metadataFormControls/CategoryComboSelect/useOptionsQuery.ts b/src/components/metadataFormControls/CategoryComboSelect/useOptionsQuery.ts index 277f904a..d3b0b3f1 100644 --- a/src/components/metadataFormControls/CategoryComboSelect/useOptionsQuery.ts +++ b/src/components/metadataFormControls/CategoryComboSelect/useOptionsQuery.ts @@ -64,9 +64,7 @@ export function useOptionsQuery() { value: id, // This should be distinguishable from other selects // where "none" means no selection - label: isDefault - ? i18n.t('Default (none)') - : displayName, + label: isDefault ? i18n.t('None') : displayName, } }) || []), ]) diff --git a/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelect.tsx b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelect.tsx index 04623c55..cf6b672a 100644 --- a/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelect.tsx +++ b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelect.tsx @@ -38,7 +38,11 @@ function computeDisplayOptions({ : options if (!required) { - return [{ value: '', label: i18n.t('None') }, ...withSelectedOption] + // This default value has been copied from the old app + return [ + { value: '', label: i18n.t('') }, + ...withSelectedOption, + ] } return withSelectedOption From 92fb57ac908ac91066c4fb683c1acb8f4ebb69d1 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 23 Oct 2023 22:07:02 +0300 Subject: [PATCH 07/10] fix(form): fix render 0 when empty custom attributes --- src/pages/dataElements/form/DataElementFormFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 43fb642f..907102c2 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -219,7 +219,7 @@ export function DataElementFormFields() {
- {customAttributes.data?.length && ( + {customAttributes.data?.length > 0 && ( {i18n.t('Custom attributes')} From de8cf8cbb71ad4c743d6ff50bd043ae60910fe95 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 25 Oct 2023 13:29:20 +0200 Subject: [PATCH 08/10] feat(custom attributes): add "No value" option to optionset attributes --- src/pages/dataElements/form/CustomAttributes.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/dataElements/form/CustomAttributes.tsx b/src/pages/dataElements/form/CustomAttributes.tsx index ce0b0326..fb67856a 100644 --- a/src/pages/dataElements/form/CustomAttributes.tsx +++ b/src/pages/dataElements/form/CustomAttributes.tsx @@ -1,3 +1,4 @@ +import i18n from '@dhis2/d2-i18n' import { InputFieldFF, SingleSelectFieldFF, TextAreaFieldFF } from '@dhis2/ui' import * as React from 'react' import { Field as FieldRFF } from 'react-final-form' @@ -15,13 +16,18 @@ function CustomAttribute({ attribute, index }: CustomAttributeProps) { const name = `attributeValues[${index}].value` if (attribute.optionSet?.options) { - const options = attribute.optionSet?.options.map( + const attributeOptions = attribute.optionSet?.options.map( ({ code, displayName }) => ({ value: code, label: displayName, }) ) + const options = [ + { value: '', label: i18n.t('') }, + ...(attributeOptions || []), + ] + return ( Date: Wed, 25 Oct 2023 13:32:33 +0200 Subject: [PATCH 09/10] fix(de form fields): make text translatable --- src/pages/dataElements/form/DataElementFormFields.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 907102c2..edf5b8c5 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -204,14 +204,9 @@ export function DataElementFormFields() { {i18n.t('Aggregation levels')} - By default, the aggregation will start at the lowest - assigned organisation unit. If you for example select - "Chiefdom", it means that "Chiefdom", - "District" and "National" aggregates use - "Chiefdom" (the highest aggregation level - available) as the data source, and PHU data will not be - included. PHU will still be available for the PHU level, but - not included in the aggregations to the levels above. + {i18n.t( + 'By default, the aggregation will start at the lowest assigned organisation unit. If you for example select "Chiefdom", it means that "Chiefdom", "District" and "National" aggregates use "Chiefdom" (the highest aggregation level available) as the data source, and PHU data will not be included. PHU will still be available for the PHU level, but not included in the aggregations to the levels above.' + )} From c10ec33781ee2efc0864dc08b8d20238312ba093 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Mon, 30 Oct 2023 12:27:40 +0100 Subject: [PATCH 10/10] fix(custom attributes optionsets): add no-value-option only when not required --- src/pages/dataElements/form/CustomAttributes.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/dataElements/form/CustomAttributes.tsx b/src/pages/dataElements/form/CustomAttributes.tsx index fb67856a..f379ffd5 100644 --- a/src/pages/dataElements/form/CustomAttributes.tsx +++ b/src/pages/dataElements/form/CustomAttributes.tsx @@ -14,25 +14,25 @@ type CustomAttributeProps = { function CustomAttribute({ attribute, index }: CustomAttributeProps) { const name = `attributeValues[${index}].value` + const required = attribute.mandatory if (attribute.optionSet?.options) { - const attributeOptions = attribute.optionSet?.options.map( + const options = attribute.optionSet?.options.map( ({ code, displayName }) => ({ value: code, label: displayName, }) ) - const options = [ - { value: '', label: i18n.t('') }, - ...(attributeOptions || []), - ] + if (required) { + options.unshift({ value: '', label: i18n.t('') }) + } return (