diff --git a/i18n/en.pot b/i18n/en.pot index 00dddc21..c1b2cdea 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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" 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 bbac9776..1fe97075 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'], +] 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) { + // This default value has been copied from the old app + return [ + { value: '', label: i18n.t('') }, + ...withSelectedOption, + ] } - return options + return withSelectedOption } type UseInitialOptionQuery = ({ @@ -44,8 +56,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 +68,11 @@ interface ModelSingleSelectProps { useOptionsQuery: () => QueryResponse } -export interface ModelSingleSelectHandle { - refetch: () => void -} - export const ModelSingleSelect = forwardRef(function ModelSingleSelect( { onChange, placeholder = '', + required, selected, showAllOption, onBlur, @@ -134,6 +144,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 ( { @@ -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 0a352f2f..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' @@ -26,8 +30,10 @@ function computeInitialValues(customAttributes: Attribute[]) { code: '', description: '', url: '', - color: '', - icon: '', + style: { + color: '', + icon: '', + }, fieldMask: '', domainType: 'AGGREGATE', formName: '', @@ -78,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 }) @@ -111,7 +101,6 @@ export const Component = () => { variables: payload, }) } catch (e) { - console.log('> e', e) return { [FORM_ERROR]: (e as Error | string).toString() } } @@ -119,16 +108,21 @@ export const Component = () => { } return ( -
- {({ handleSubmit, submitting, submitError }) => ( - - - - )} - + +
+ {({ handleSubmit, submitting, submitError }) => ( + + + + )} + +
) } 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) || '', })) } 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/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 8fa7613a..907102c2 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')} @@ -217,12 +204,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. @@ -230,7 +219,7 @@ export function DataElementFormFields() { - {customAttributes.data?.length && ( + {customAttributes.data?.length > 0 && ( {i18n.t('Custom attributes')} @@ -244,6 +233,6 @@ export function DataElementFormFields() { /> )} - + ) } diff --git a/src/pages/dataElements/form/customFields.tsx b/src/pages/dataElements/form/customFields.tsx index 11c45532..5f536961 100644 --- a/src/pages/dataElements/form/customFields.tsx +++ b/src/pages/dataElements/form/customFields.tsx @@ -252,6 +252,7 @@ export function CategoryComboField() { validationText={meta.error} >