diff --git a/i18n/en.pot b/i18n/en.pot
index fc9d9f30..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-11T10:29:42.492Z\n"
-"PO-Revision-Date: 2023-10-11T10:29:42.492Z\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"
@@ -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"
@@ -597,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"
@@ -621,9 +633,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"
@@ -676,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"
@@ -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/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'],
+]
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..f379ffd5 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'
@@ -13,6 +14,7 @@ type CustomAttributeProps = {
function CustomAttribute({ attribute, index }: CustomAttributeProps) {
const name = `attributeValues[${index}].value`
+ const required = attribute.mandatory
if (attribute.optionSet?.options) {
const options = attribute.optionSet?.options.map(
@@ -22,11 +24,15 @@ function CustomAttribute({ attribute, index }: CustomAttributeProps) {
})
)
+ if (required) {
+ options.unshift({ value: '', label: i18n.t('') })
+ }
+
return (
@TODO(DataElementForm): Loading>
- }
-
- if (error) {
- return (
- <>
- @TODO(DataElementForm): Error
-
- {error.toString()}
- >
- )
- }
-
return (
- <>
+
{i18n.t('Basic information')}
@@ -217,12 +204,9 @@ 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.
- `}
+ {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.'
+ )}
@@ -230,7 +214,7 @@ export function DataElementFormFields() {
- {customAttributes.data?.length && (
+ {customAttributes.data?.length > 0 && (
{i18n.t('Custom attributes')}
@@ -244,6 +228,6 @@ export function DataElementFormFields() {
/>
)}
- >
+
)
}
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}
>