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 (
-
- )}
-
+
+
+
+ )}
+
+
+
)
}
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 (
-
- )}
-
+
+
+ )}
+
+
)
}
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}
>