From fc2f881546977fd6f931e40ccbfffad84c7bf209 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 19 Mar 2024 14:03:27 +0100 Subject: [PATCH 01/10] fix(list): fix filter being cleared on refresh (#382) --- .../filterSelectors/IdentifiableFilter.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx b/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx index 75cf841a..160f01fc 100644 --- a/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx +++ b/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx @@ -1,23 +1,30 @@ import i18n from '@dhis2/d2-i18n' import { Input, InputEventPayload } from '@dhis2/ui' import React, { useEffect, useState } from 'react' -import { - useDebounce, - IDENTIFIABLE_FILTER_KEY, - useSectionListFilter, -} from '../../../../lib' +import { useDebouncedCallback } from 'use-debounce' +import { IDENTIFIABLE_FILTER_KEY, useSectionListFilter } from '../../../../lib' import css from './Filters.module.css' export const IdentifiableFilter = () => { const [filter, setFilter] = useSectionListFilter(IDENTIFIABLE_FILTER_KEY) const [value, setValue] = useState(filter || '') - const debouncedValue = useDebounce(value, 200) - useEffect(() => { - setFilter(debouncedValue || undefined) // convert empty string to undefined - }, [debouncedValue, setFilter]) + const debouncedSetFilter = useDebouncedCallback( + (debouncedFilter: string) => + // convert empty string to undefined + // to prevent empty-value like "identifiable=" in URL + setFilter(debouncedFilter || undefined), + 200 + ) + + const handleSetValue = (event: InputEventPayload) => { + const eventValue = event.value ?? '' + setValue(eventValue) + debouncedSetFilter(eventValue) + } useEffect(() => { + // clear input-value when "Clear all filters" if (!filter) { setValue('') } @@ -28,9 +35,7 @@ export const IdentifiableFilter = () => { - setValue(value.value ?? '') - } + onChange={handleSetValue} value={value} dataTest="input-search-name" dense From 0fb0d12926ab1fdbd86d28802bba2a359b49447b Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Wed, 13 Mar 2024 23:03:37 +0100 Subject: [PATCH 02/10] fix(form): fix cancel link --- src/components/form/DefaultFormContents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/form/DefaultFormContents.tsx b/src/components/form/DefaultFormContents.tsx index 2f0bd6af..76322b8b 100644 --- a/src/components/form/DefaultFormContents.tsx +++ b/src/components/form/DefaultFormContents.tsx @@ -21,7 +21,7 @@ export function DefaultFormContents({ const formErrorRef = useRef(null) const navigate = useNavigate() - const listPath = getSectionPath(section) + const listPath = `/${getSectionPath(section)}` useEffect(() => { if (submitError) { formErrorRef.current?.scrollIntoView({ behavior: 'smooth' }) From da4bbc9d5b6e14d36de821be361018ec71fc4b5c Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 25 Mar 2024 13:04:14 +0100 Subject: [PATCH 03/10] fix(form): hide custom attributes section if no attributes assigned (#384) * fix(form): hide custom attributes section if no attributes assigned * fix: bad merge --- .../form/attributes/CustomAttributes.tsx | 22 +++++++++++++++---- src/components/form/attributes/index.ts | 2 +- .../form/DataElementGroupSetFormFields.tsx | 14 ++---------- .../form/DataElementGroupFormFields.tsx | 14 ++---------- .../form/DataElementFormFields.tsx | 14 ++---------- 5 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/components/form/attributes/CustomAttributes.tsx b/src/components/form/attributes/CustomAttributes.tsx index c02ba003..d897f869 100644 --- a/src/components/form/attributes/CustomAttributes.tsx +++ b/src/components/form/attributes/CustomAttributes.tsx @@ -2,7 +2,11 @@ import i18n from '@dhis2/d2-i18n' import { InputFieldFF, SingleSelectFieldFF, TextAreaFieldFF } from '@dhis2/ui' import * as React from 'react' import { Field as FieldRFF, useFormState } from 'react-final-form' -import { StandardFormSection } from '../..' +import { + StandardFormSection, + StandardFormSectionDescription, + StandardFormSectionTitle, +} from '../..' import { Attribute, AttributeValue } from '../../../types/generated' const inputWidth = '440px' @@ -78,7 +82,7 @@ function CustomAttribute({ attribute, index }: CustomAttributeProps) { throw new Error(`Implement value type "${attribute.valueType}"!`) } -export function CustomAttributes() { +export function CustomAttributesSection() { const formState = useFormState({ subscription: { initialValues: true }, }) @@ -86,9 +90,19 @@ export function CustomAttributes() { const customAttributes = formState.initialValues.attributeValues?.map( (av) => av.attribute ) + if (!customAttributes || customAttributes?.length < 1) { + return null + } return ( - <> + + + {i18n.t('Custom attributes')} + + + + {i18n.t('Custom fields for your DHIS2 instance')} + {customAttributes?.map((customAttribute, index) => { return ( ) })} - + ) } diff --git a/src/components/form/attributes/index.ts b/src/components/form/attributes/index.ts index ae4e404a..8604c2c0 100644 --- a/src/components/form/attributes/index.ts +++ b/src/components/form/attributes/index.ts @@ -1,2 +1,2 @@ -export { CustomAttributes } from './CustomAttributes' +export { CustomAttributesSection } from './CustomAttributes' export { useCustomAttributesQuery } from './useCustomAttributesQuery' diff --git a/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx b/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx index 85884b1f..471269f6 100644 --- a/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx +++ b/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx @@ -1,7 +1,7 @@ import i18n from '@dhis2/d2-i18n' import React from 'react' import { - CustomAttributes, + CustomAttributesSection, StandardFormSection, StandardFormSectionTitle, StandardFormSectionDescription, @@ -68,17 +68,7 @@ export function DataElementGroupSetFormFields() { - - - {i18n.t('Custom attributes')} - - - - {i18n.t('Custom fields for your DHIS2 instance')} - - - - + ) } diff --git a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx index 8e61e43b..faaf47cc 100644 --- a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx +++ b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx @@ -5,7 +5,7 @@ import { StandardFormSectionTitle, StandardFormSectionDescription, StandardFormField, - CustomAttributes, + CustomAttributesSection, } from '../../../components' import { DefaultIdentifiableFields, @@ -56,17 +56,7 @@ export function DataElementGroupFormFields() { - - - {i18n.t('Custom attributes')} - - - - {i18n.t('Custom fields for your DHIS2 instance')} - - - - + ) } diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index 7884ac35..f4801f05 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -5,7 +5,7 @@ import { StandardFormSectionTitle, StandardFormSectionDescription, StandardFormField, - CustomAttributes, + CustomAttributesSection, } from '../../../components' import { CodeField, @@ -151,17 +151,7 @@ export function DataElementFormFields() { - - - {i18n.t('Custom attributes')} - - - - {i18n.t('Custom fields for your DHIS2 instance')} - - - - + ) } From 523e8e9be7c60f2d2e22120ddc9ec8d394ff29e4 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 25 Mar 2024 13:04:28 +0100 Subject: [PATCH 04/10] fix(form): stylistic issues: max-width, todos, labels (#385) --- .../standardForm/StandardFormSectionDescription.module.css | 1 + .../form/DataElementGroupSetFormFields.tsx | 3 ++- .../dataElementGroups/form/DataElementGroupFormFields.tsx | 3 ++- src/pages/dataElements/form/DataElementFormFields.tsx | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/standardForm/StandardFormSectionDescription.module.css b/src/components/standardForm/StandardFormSectionDescription.module.css index 3bbb171f..118f3b75 100644 --- a/src/components/standardForm/StandardFormSectionDescription.module.css +++ b/src/components/standardForm/StandardFormSectionDescription.module.css @@ -3,4 +3,5 @@ font-size: 14px; line-height: 19px; color: var(--colors-grey800); + max-width: 600px; } diff --git a/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx b/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx index 471269f6..d99dff16 100644 --- a/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx +++ b/src/pages/dataElementGroupSets/form/DataElementGroupSetFormFields.tsx @@ -60,7 +60,8 @@ export function DataElementGroupSetFormFields() { - {i18n.t('@TODO')} + {/* TODO: ADD DESCRIPTION */} + {''} diff --git a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx index faaf47cc..c639c26f 100644 --- a/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx +++ b/src/pages/dataElementGroups/form/DataElementGroupFormFields.tsx @@ -48,7 +48,8 @@ export function DataElementGroupFormFields() { - {i18n.t('@TODO')} + {/* TODO: ADD DESCRIPTION */} + {''} diff --git a/src/pages/dataElements/form/DataElementFormFields.tsx b/src/pages/dataElements/form/DataElementFormFields.tsx index f4801f05..3c8825a3 100644 --- a/src/pages/dataElements/form/DataElementFormFields.tsx +++ b/src/pages/dataElements/form/DataElementFormFields.tsx @@ -121,7 +121,7 @@ export function DataElementFormFields() { - {i18n.t('LegendSet')} + {i18n.t('Legend set')} From 49236a052bb9e0a5f69432dd6bc0c3493a98f235 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 25 Mar 2024 13:06:07 +0100 Subject: [PATCH 05/10] refactor: remove unused files (#380) --- src/lib/dataStore/DataStore.ts | 1 - .../filters/parseFiltersToQueryParams.ts | 13 ++-- .../dataElementGroups/fields/CodeField.tsx | 25 -------- .../fields/DescriptionField.tsx | 28 -------- .../dataElementGroups/fields/NameField.tsx | 64 ------------------- .../fields/ShortNameField.tsx | 62 ------------------ 6 files changed, 8 insertions(+), 185 deletions(-) delete mode 100644 src/lib/dataStore/DataStore.ts delete mode 100644 src/pages/dataElementGroups/fields/CodeField.tsx delete mode 100644 src/pages/dataElementGroups/fields/DescriptionField.tsx delete mode 100644 src/pages/dataElementGroups/fields/NameField.tsx delete mode 100644 src/pages/dataElementGroups/fields/ShortNameField.tsx diff --git a/src/lib/dataStore/DataStore.ts b/src/lib/dataStore/DataStore.ts deleted file mode 100644 index 314dbe15..00000000 --- a/src/lib/dataStore/DataStore.ts +++ /dev/null @@ -1 +0,0 @@ -export class DataStore {} diff --git a/src/lib/sectionList/filters/parseFiltersToQueryParams.ts b/src/lib/sectionList/filters/parseFiltersToQueryParams.ts index a804e461..0fcb7e84 100644 --- a/src/lib/sectionList/filters/parseFiltersToQueryParams.ts +++ b/src/lib/sectionList/filters/parseFiltersToQueryParams.ts @@ -16,11 +16,14 @@ type FilterToQueryParamsMap = { const inFilter = (filterPath: string, value: string[]) => `${filterPath}:in:[${value.join(',')}]` -const defaultFilter = (key: FilterKey, value: AllValues): string => { - const isArray = Array.isArray(value) - const valuesString = isArray ? `[${value.join(',')}]` : value?.toString() - const operator = isArray ? 'in' : 'eq' - return `${key}:${operator}:${valuesString}` +const defaultFilter = ( + key: FilterKey, + value: NonNullable +): string => { + if (Array.isArray(value)) { + return inFilter(key, value) + } + return `${key}:eq:${value}` } /* Override how to resolve the actual queryParam (when used in a request) for a filter */ diff --git a/src/pages/dataElementGroups/fields/CodeField.tsx b/src/pages/dataElementGroups/fields/CodeField.tsx deleted file mode 100644 index fedc4eb8..00000000 --- a/src/pages/dataElementGroups/fields/CodeField.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { InputFieldFF } from '@dhis2/ui' -import React from 'react' -import { Field as FieldRFF } from 'react-final-form' -import { useCheckMaxLengthFromSchema } from '../../../lib' -import type { SchemaName } from '../../../types' - -export function CodeField() { - const validate = useCheckMaxLengthFromSchema( - 'dataElement' as SchemaName, - 'code' - ) - - return ( - - ) -} diff --git a/src/pages/dataElementGroups/fields/DescriptionField.tsx b/src/pages/dataElementGroups/fields/DescriptionField.tsx deleted file mode 100644 index c11298b8..00000000 --- a/src/pages/dataElementGroups/fields/DescriptionField.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { TextAreaFieldFF } from '@dhis2/ui' -import React from 'react' -import { Field as FieldRFF } from 'react-final-form' -import { useCheckMaxLengthFromSchema } from '../../../lib' -import type { SchemaName } from '../../../types' - -export function DescriptionField() { - const validate = useCheckMaxLengthFromSchema( - 'dataElement' as SchemaName, - 'formName' - ) - - return ( - - ) -} diff --git a/src/pages/dataElementGroups/fields/NameField.tsx b/src/pages/dataElementGroups/fields/NameField.tsx deleted file mode 100644 index b6c38724..00000000 --- a/src/pages/dataElementGroups/fields/NameField.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { InputFieldFF } from '@dhis2/ui' -import React, { useMemo } from 'react' -import { Field as FieldRFF, useField } from 'react-final-form' -import { useParams } from 'react-router-dom' -import { - composeAsyncValidators, - required, - useCheckMaxLengthFromSchema, - useIsFieldValueUnique, -} from '../../../lib' -import { SchemaName } from '../../../types' -import type { FormValues } from '../form' - -function useValidator() { - const params = useParams() - const dataElementId = params.id as string - const checkIsValueTaken = useIsFieldValueUnique({ - model: 'dataElements', - field: 'name', - id: dataElementId, - }) - - const checkMaxLength = useCheckMaxLengthFromSchema( - SchemaName.dataElement, - 'name' - ) - - return useMemo( - () => - composeAsyncValidators([ - checkIsValueTaken, - checkMaxLength, - required, - ]), - [checkIsValueTaken, checkMaxLength] - ) -} - -export function NameField() { - const validator = useValidator() - const { meta } = useField('name', { - subscription: { validating: true }, - }) - - return ( - - loading={meta.validating} - component={InputFieldFF} - dataTest="dataelementsformfields-name" - required - inputWidth="400px" - label={i18n.t('{{fieldLabel}} (required)', { - fieldLabel: i18n.t('Name'), - })} - name="name" - helpText={i18n.t( - 'A data element name should be concise and easy to recognize.' - )} - validate={(name?: string) => validator(name)} - validateFields={[]} - /> - ) -} diff --git a/src/pages/dataElementGroups/fields/ShortNameField.tsx b/src/pages/dataElementGroups/fields/ShortNameField.tsx deleted file mode 100644 index 7e1845d5..00000000 --- a/src/pages/dataElementGroups/fields/ShortNameField.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import i18n from '@dhis2/d2-i18n' -import { InputFieldFF } from '@dhis2/ui' -import React, { useMemo } from 'react' -import { Field as FieldRFF, useField } from 'react-final-form' -import { useParams } from 'react-router-dom' -import { - composeAsyncValidators, - required, - useCheckMaxLengthFromSchema, - useIsFieldValueUnique, -} from '../../../lib' -import type { SchemaName } from '../../../types' -import type { FormValues } from '../form' - -function useValidator() { - const params = useParams() - const dataElementId = params.id as string - const checkIsValueTaken = useIsFieldValueUnique({ - model: 'dataElements', - field: 'name', - id: dataElementId, - }) - - const checkMaxLength = useCheckMaxLengthFromSchema( - 'dataElement' as SchemaName, - 'formName' - ) - - return useMemo( - () => - composeAsyncValidators([ - checkIsValueTaken, - checkMaxLength, - required, - ]), - [checkIsValueTaken, checkMaxLength] - ) -} - -export function ShortNameField() { - const validator = useValidator() - const { meta } = useField('shortName', { - subscription: { validating: true }, - }) - - return ( - - loading={meta.validating} - component={InputFieldFF} - dataTest="dataelementsformfields-shortname" - required - inputWidth="400px" - label={i18n.t('{{fieldLabel}} (required)', { - fieldLabel: i18n.t('Short name'), - })} - name="shortName" - helpText={i18n.t('Often used in reports where space is limited')} - validate={(name?: string) => validator(name)} - validateFields={[]} - /> - ) -} From d1eeef9015e22fa0b8eabcc39d9288f8c1d03ab3 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 26 Mar 2024 10:38:05 +0800 Subject: [PATCH 06/10] feat(list view): add delete action to row items (#376) * feat(list view): add delete action to row items * feat(delete action): add alerts for success / failure * refactor(delete model alerts): move alerts to delete mutation hook * feat(use delete model): add model's displayName and failure report to alerts * feat(section list actions): disable "Delete" action without delete auth * refactor(default list actions): rename "canX" props to "x-able" props * feat(delete action): add confirmation dialog * feat(delete model action): move deletion logic into delete action component & improve message * refactor: move error & success handling from hook to * refactor(use delete modal mutation): allow useMutation options and use "onSuccess" option * refactor(delete action): change confirm label to "Confirm deletion" * fix(delete action): call correct mutate function * fix(delete action): disable modal cancel button during delete request * refactor(delete action): make component accept entire "model" * fix: delete model mutation types * refactor(delete action): remove superfluous "modelType" prop * refactor(delete action): reword cancel button label from "No" to "Cancel" * refactor(delete action): use section-handle instead of schema for proper translations * fix: more defensive errorreports --------- Co-authored-by: Birk Johansson --- i18n/en.pot | 40 ++++- .../sectionList/SectionListWrapper.tsx | 5 +- .../listActions/DefaultListActions.tsx | 22 ++- .../listActions/DeleteAction.module.css | 8 + .../sectionList/listActions/DeleteAction.tsx | 166 ++++++++++++++++++ .../listActions/SectionListActions.tsx | 45 +++-- src/lib/constants/tooltips.ts | 1 + src/lib/models/access.ts | 4 + src/lib/models/index.ts | 1 + src/lib/models/useDeleteModelMutation.ts | 35 ++++ src/lib/sectionList/fieldFilters.ts | 2 +- src/pages/dataElements/List.tsx | 1 + src/types/fixedModels.ts | 24 +++ src/types/generated/models.ts | 10 +- src/types/index.ts | 1 + 15 files changed, 335 insertions(+), 30 deletions(-) create mode 100644 src/components/sectionList/listActions/DeleteAction.module.css create mode 100644 src/components/sectionList/listActions/DeleteAction.tsx create mode 100644 src/lib/models/useDeleteModelMutation.ts create mode 100644 src/types/fixedModels.ts diff --git a/i18n/en.pot b/i18n/en.pot index 5b64f05d..7b74c9db 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: 2024-03-13T00:05:18.041Z\n" -"PO-Revision-Date: 2024-03-13T00:05:18.041Z\n" +"POT-Creation-Date: 2024-03-20T01:08:59.818Z\n" +"PO-Revision-Date: 2024-03-20T01:08:59.818Z\n" msgid "schemas" msgstr "schemas" @@ -249,15 +249,45 @@ msgstr "Search by name, code or ID" msgid "Public access" msgstr "Public access" +msgid "Delete" +msgstr "Delete" + +msgid "Successfully deleted {{modelType}} \"{{displayName}}\"" +msgstr "Successfully deleted {{modelType}} \"{{displayName}}\"" + +msgid "Are you sure that you want to delete this {{modelType}}?" +msgstr "Are you sure that you want to delete this {{modelType}}?" + +msgid "Something went wrong deleting the {{modelType}}" +msgstr "Something went wrong deleting the {{modelType}}" + +msgid "Failed to delete {{modelType}} \"{{displayName}}\"! {{messages}}" +msgstr "Failed to delete {{modelType}} \"{{displayName}}\"! {{messages}}" + +msgid "Try again" +msgstr "Try again" + +msgid "Confirm deletion" +msgstr "Confirm deletion" + msgid "Show details" msgstr "Show details" +msgid "Sharing settings" +msgstr "Sharing settings" + msgid "At least one column must be selected" msgstr "At least one column must be selected" msgid "At least one filter must be selected" msgstr "At least one filter must be selected" +msgid "Columns" +msgstr "Columns" + +msgid "Filters" +msgstr "Filters" + msgid "Available columns" msgstr "Available columns" @@ -552,6 +582,12 @@ msgstr "Locale" msgid "Locales" msgstr "Locales" +msgid "You do not have access to edit this item." +msgstr "You do not have access to edit this item." + +msgid "You do not have access to delete this item." +msgstr "You do not have access to delete this item." + msgid "Sum" msgstr "Sum" diff --git a/src/components/sectionList/SectionListWrapper.tsx b/src/components/sectionList/SectionListWrapper.tsx index 56101986..80cd5069 100644 --- a/src/components/sectionList/SectionListWrapper.tsx +++ b/src/components/sectionList/SectionListWrapper.tsx @@ -24,12 +24,14 @@ type SectionListWrapperProps = { data: ModelCollection | undefined pager: Pager | undefined error: FetchError | undefined + refetch: () => void } export const SectionListWrapper = ({ data, error, pager, + refetch, }: SectionListWrapperProps) => { const { columns: headerColumns } = useModelListView() const schema = useSchemaFromHandle() @@ -97,9 +99,10 @@ export const SectionListWrapper = ({ model={model} onShowDetailsClick={handleDetailsClick} onOpenSharingClick={setSharingDialogId} + onDeleteSuccess={refetch} /> ), - [handleDetailsClick, setSharingDialogId] + [handleDetailsClick, setSharingDialogId, refetch] ) const isAllSelected = data ? checkAllSelected(data) : false diff --git a/src/components/sectionList/listActions/DefaultListActions.tsx b/src/components/sectionList/listActions/DefaultListActions.tsx index a78054f9..8fd5b966 100644 --- a/src/components/sectionList/listActions/DefaultListActions.tsx +++ b/src/components/sectionList/listActions/DefaultListActions.tsx @@ -1,30 +1,34 @@ import React from 'react' -import { canEditModel } from '../../../lib/models/access' -import { BaseIdentifiableObject } from '../../../types/generated' +import { BaseListModel } from '../../../lib' +import { canEditModel, canDeleteModel } from '../../../lib/models/access' import { ListActions, ActionEdit, ActionMore } from './SectionListActions' -type ModelWithAccess = Pick - type DefaultListActionProps = { - model: ModelWithAccess - onShowDetailsClick: (model: ModelWithAccess) => void + model: BaseListModel + onShowDetailsClick: (model: BaseListModel) => void onOpenSharingClick: (id: string) => void + onDeleteSuccess: () => void } export const DefaultListActions = ({ model, onShowDetailsClick, onOpenSharingClick, + onDeleteSuccess, }: DefaultListActionProps) => { - const editAccess = canEditModel(model) + const deletable = canDeleteModel(model) + const editable = canEditModel(model) + return ( onShowDetailsClick(model)} onOpenSharingClick={() => onOpenSharingClick(model.id)} + onDeleteSuccess={onDeleteSuccess} /> ) diff --git a/src/components/sectionList/listActions/DeleteAction.module.css b/src/components/sectionList/listActions/DeleteAction.module.css new file mode 100644 index 00000000..c61f208a --- /dev/null +++ b/src/components/sectionList/listActions/DeleteAction.module.css @@ -0,0 +1,8 @@ +.deleteButtonLoadingIcon { + display: inline-block; + margin-right: 8px; +} + +.deleteButtonLoadingIcon :global([role="progressbar"]) { + border-color: rgba(110, 122, 138, 0.15) rgba(110, 122, 138, 0.15) white; +} diff --git a/src/components/sectionList/listActions/DeleteAction.tsx b/src/components/sectionList/listActions/DeleteAction.tsx new file mode 100644 index 00000000..01b952b2 --- /dev/null +++ b/src/components/sectionList/listActions/DeleteAction.tsx @@ -0,0 +1,166 @@ +import { useAlert } from '@dhis2/app-runtime' +import i18n from '@dhis2/d2-i18n' +import { + Button, + ButtonStrip, + CircularLoader, + IconDelete16, + MenuItem, + Modal, + ModalActions, + ModalContent, + ModalTitle, + NoticeBox, +} from '@dhis2/ui' +import React, { useState } from 'react' +import { + BaseListModel, + useDeleteModelMutation, + useSchemaSectionHandleOrThrow, +} from '../../../lib' +import classes from './DeleteAction.module.css' + +export function DeleteAction({ + disabled, + model, + onCancel, + onDeleteSuccess, +}: { + disabled: boolean + model: BaseListModel + onCancel: () => void + onDeleteSuccess: () => void +}) { + const [showConfirmationDialog, setShowConfirmationDialog] = useState(false) + const deleteAndClose = () => { + setShowConfirmationDialog(false) + onDeleteSuccess() + } + const closeAndCancel = () => { + setShowConfirmationDialog(false) + onCancel() + } + + return ( + <> + } + onClick={() => setShowConfirmationDialog(true)} + /> + + {showConfirmationDialog && ( + + )} + + ) +} + +function ConfirmationDialog({ + model, + onCancel, + onDeleteSuccess, +}: { + model: BaseListModel + onCancel: () => void + onDeleteSuccess: () => void +}) { + const section = useSchemaSectionHandleOrThrow() + + const deleteModelMutation = useDeleteModelMutation(section.namePlural, { + onSuccess: () => { + showDeletionSuccess() + onDeleteSuccess() + }, + }) + + const { show: showDeletionSuccess } = useAlert( + () => + i18n.t('Successfully deleted {{modelType}} "{{displayName}}"', { + displayName: model.displayName, + modelType: section.title, + }), + { success: true } + ) + + const errorReports = + deleteModelMutation.error?.details?.response?.errorReports + return ( + + + {i18n.t( + 'Are you sure that you want to delete this {{modelType}}?', + { modelType: section.title } + )} + + + {!!deleteModelMutation.error && ( + + +
+ {i18n.t( + 'Failed to delete {{modelType}} "{{displayName}}"! {{messages}}', + { + displayName: model.displayName, + modelType: section.title, + } + )} +
+ + {!!errorReports?.length && ( +
    + {errorReports.map(({ message }) => ( +
  • {message}
  • + ))} +
+ )} +
+
+ )} + + + + + + + + +
+ ) +} diff --git a/src/components/sectionList/listActions/SectionListActions.tsx b/src/components/sectionList/listActions/SectionListActions.tsx index 5eeec25a..c133c408 100644 --- a/src/components/sectionList/listActions/SectionListActions.tsx +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -12,9 +12,10 @@ import { } from '@dhis2/ui' import React, { useRef, useState } from 'react' import { useHref, useLinkClickHandler } from 'react-router-dom' -import { TOOLTIPS } from '../../../lib' +import { TOOLTIPS, BaseListModel } from '../../../lib' import { LinkButton } from '../../LinkButton' import { TooltipWrapper } from '../../tooltip' +import { DeleteAction } from './DeleteAction' import css from './SectionListActions.module.css' export const ListActions = ({ children }: React.PropsWithChildren) => { @@ -30,22 +31,26 @@ export const ActionEdit = ({ modelId }: { modelId: string }) => { } type ActionMoreProps = { - modelId: string - editAccess: boolean + deletable: boolean + editable: boolean + model: BaseListModel onShowDetailsClick: () => void onOpenSharingClick: () => void + onDeleteSuccess: () => void } export const ActionMore = ({ - modelId, - editAccess, + deletable, + editable, + model, onOpenSharingClick, onShowDetailsClick, + onDeleteSuccess, }: ActionMoreProps) => { const [open, setOpen] = useState(false) const ref = useRef(null) - const href = useHref(modelId, { relative: 'path' }) + const href = useHref(model.id, { relative: 'path' }) - const handleEditClick = useLinkClickHandler(modelId) + const handleEditClick = useLinkClickHandler(model.id) return (
@@ -54,7 +59,7 @@ export const ActionMore = ({ secondary onClick={() => setOpen(!open)} icon={} - > + /> {open && ( + /> + } onClick={() => { onOpenSharingClick() setOpen(false) }} - > + /> + + + + { + onDeleteSuccess() + setOpen(false) + }} + onCancel={() => setOpen(false)} + /> diff --git a/src/lib/constants/tooltips.ts b/src/lib/constants/tooltips.ts index f044a1ff..3d617ba6 100644 --- a/src/lib/constants/tooltips.ts +++ b/src/lib/constants/tooltips.ts @@ -2,4 +2,5 @@ import i18n from '@dhis2/d2-i18n' export const TOOLTIPS = { noEditAccess: i18n.t('You do not have access to edit this item.'), + noDeleteAccess: i18n.t('You do not have access to delete this item.'), } diff --git a/src/lib/models/access.ts b/src/lib/models/access.ts index b3d11d5b..d083387c 100644 --- a/src/lib/models/access.ts +++ b/src/lib/models/access.ts @@ -1,10 +1,14 @@ import type { Access, BaseIdentifiableObject } from '../../types/generated' export const hasWriteAccess = (access: Partial) => !!access.write +export const hasDeleteAccess = (access: Partial) => !!access.delete export const canEditModel = (model: Pick) => hasWriteAccess(model.access) +export const canDeleteModel = (model: Pick) => + hasDeleteAccess(model.access) + export type ParsedAccessPart = { read: boolean write: boolean diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts index c8bc5f7c..dafa0a81 100644 --- a/src/lib/models/index.ts +++ b/src/lib/models/index.ts @@ -1,5 +1,6 @@ export { isValidUid } from './uid' export * from './access' export { getIn, stringToPathArray, getFieldFilterFromPath } from './path' +export { useDeleteModelMutation } from './useDeleteModelMutation' export { useIsFieldValueUnique } from './useIsFieldValueUnique' export { useCheckMaxLengthFromSchema } from './useCheckMaxLengthFromSchema' diff --git a/src/lib/models/useDeleteModelMutation.ts b/src/lib/models/useDeleteModelMutation.ts new file mode 100644 index 00000000..836cf662 --- /dev/null +++ b/src/lib/models/useDeleteModelMutation.ts @@ -0,0 +1,35 @@ +import { FetchError, useDataEngine } from '@dhis2/app-runtime' +import { useMutation, UseMutationOptions } from 'react-query' +import { ImportSummary } from '../../types' + +type MutationFnArgs = { + id: string + displayName: string +} + +type DeleteMutationError = Omit & { + details: ImportSummary +} + +type Options = Omit< + UseMutationOptions, + 'mutationFn' +> + +export function useDeleteModelMutation( + schemaResource: string, + options?: Options +) { + const engine = useDataEngine() + + return useMutation({ + ...options, + mutationFn: (variables) => { + return engine.mutate({ + resource: schemaResource, + id: variables.id, + type: 'delete', + }) as Promise + }, + }) +} diff --git a/src/lib/sectionList/fieldFilters.ts b/src/lib/sectionList/fieldFilters.ts index 5e996b00..5c293a3f 100644 --- a/src/lib/sectionList/fieldFilters.ts +++ b/src/lib/sectionList/fieldFilters.ts @@ -1,6 +1,6 @@ import { BaseIdentifiableObject } from '../../types/generated' -export const DEFAULT_FIELD_FILTERS = ['id', 'access'] as const +export const DEFAULT_FIELD_FILTERS = ['id', 'access', 'displayName'] as const export type DefaultFields = (typeof DEFAULT_FIELD_FILTERS)[number] export type BaseListModel = Pick diff --git a/src/pages/dataElements/List.tsx b/src/pages/dataElements/List.tsx index 7dea09ac..51c39765 100644 --- a/src/pages/dataElements/List.tsx +++ b/src/pages/dataElements/List.tsx @@ -57,6 +57,7 @@ export const Component = () => { error={error} data={data?.result.dataElements} pager={data?.result.pager} + refetch={refetch} />
) diff --git a/src/types/fixedModels.ts b/src/types/fixedModels.ts new file mode 100644 index 00000000..2a36334d --- /dev/null +++ b/src/types/fixedModels.ts @@ -0,0 +1,24 @@ +import type { ErrorReportLegacy } from './generated' +// Some of the generated models are wrong, or outdated +// The import summaries and error reports changed in 2.41 +export type ImportSummary = { + httpStatus: string + httpStatusCode: number + message?: string + status: string + response: ImportResponse +} + +export type ImportResponse = { + errorReports: ErrorReport[] + klass: string + responseType: string + uid: string +} + +export type ErrorReport = Pick< + ErrorReportLegacy, + 'errorCode' | 'errorProperties' | 'errorKlass' | 'mainKlass' | 'message' +> & { + args: string[] +} diff --git a/src/types/generated/models.ts b/src/types/generated/models.ts index ed712852..14b8e7c8 100644 --- a/src/types/generated/models.ts +++ b/src/types/generated/models.ts @@ -551,7 +551,7 @@ export type BulkSmsGatewayConfig = { export type CascadeSharingReport = { countUpdatedDashboardItems: number - errorReports: Array + errorReports: Array updateObjects: Record> } @@ -2481,7 +2481,7 @@ export type Error = { uid: string } -export type ErrorReport = { +export type ErrorReportLegacy = { errorCode: ErrorReport.errorCode errorKlass: string errorProperties: Array> @@ -4555,7 +4555,7 @@ export type ImportSummaries = { deleted: number ignored: number importOptions: ImportOptions - importSummaries: Array + importSummaries: Array imported: number responseType: string status: ImportSummaries.status @@ -4571,7 +4571,7 @@ export namespace ImportSummaries { } } -export type ImportSummary = { +export type ImportSummaryLegacy = { conflicts: Array dataSetComplete: string description: string @@ -5945,7 +5945,7 @@ export type ObjectCount = { export type ObjectReport = { displayName: string - errorReports: Array + errorReports: Array index: number klass: string uid: string diff --git a/src/types/index.ts b/src/types/index.ts index ec7356f7..fc76aafd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,3 +8,4 @@ export * from './section' export type * from './query' export * from './ui' export * from './systemSettings' +export * from './fixedModels' From f7a2c500957fc6aec2a25e220ac1cb4bf2dd6e40 Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Wed, 13 Mar 2024 15:11:43 +0800 Subject: [PATCH 07/10] fix(color and icon picker): make button component a html button instead of div --- .../ColorAndIconPicker/ColorPicker.module.css | 1 + src/components/ColorAndIconPicker/ColorPicker.tsx | 11 ++++++----- .../ColorAndIconPicker/IconPicker.module.css | 1 + src/components/ColorAndIconPicker/IconPicker.tsx | 13 +++++++------ 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/ColorAndIconPicker/ColorPicker.module.css b/src/components/ColorAndIconPicker/ColorPicker.module.css index 3ffea902..75526909 100644 --- a/src/components/ColorAndIconPicker/ColorPicker.module.css +++ b/src/components/ColorAndIconPicker/ColorPicker.module.css @@ -7,6 +7,7 @@ background: var(--colors-white); width: 68px; cursor: pointer; + align-items: center; } .chosenColor { diff --git a/src/components/ColorAndIconPicker/ColorPicker.tsx b/src/components/ColorAndIconPicker/ColorPicker.tsx index 1fe97075..b50d188e 100644 --- a/src/components/ColorAndIconPicker/ColorPicker.tsx +++ b/src/components/ColorAndIconPicker/ColorPicker.tsx @@ -17,22 +17,23 @@ export function ColorPicker({ return ( <> -
setShowPicker(true)} className={cx(classes.container, { [classes.hasColor]: !!color, })} + data-test="colorpicker-trigger" > -
-
+ {showPicker ? : } -
-
+ + {showPicker && ( setShowPicker(false)} translucent> diff --git a/src/components/ColorAndIconPicker/IconPicker.module.css b/src/components/ColorAndIconPicker/IconPicker.module.css index 5fc0e2d3..1c1ab747 100644 --- a/src/components/ColorAndIconPicker/IconPicker.module.css +++ b/src/components/ColorAndIconPicker/IconPicker.module.css @@ -7,6 +7,7 @@ background: var(--colors-white); width: 68px; cursor: pointer; + align-items: center; } .chosenIcon { diff --git a/src/components/ColorAndIconPicker/IconPicker.tsx b/src/components/ColorAndIconPicker/IconPicker.tsx index b4e966cc..a927bf67 100644 --- a/src/components/ColorAndIconPicker/IconPicker.tsx +++ b/src/components/ColorAndIconPicker/IconPicker.tsx @@ -18,13 +18,14 @@ export function IconPicker({ return ( <> -
setShowPicker(true)} className={cx(classes.container, { [classes.hasIcon]: !!icon, })} + data-test="iconpicker-trigger" > -
+ {selectedIcon && ( )} -
+ -
+ {showPicker ? : } -
-
+ + {showPicker && ( Date: Wed, 13 Mar 2024 15:13:06 +0800 Subject: [PATCH 08/10] refactor(sidenav): add data-test attribute --- i18n/en.pot | 29 ++++++--------- src/app/sidebar/sidenav/Sidenav.tsx | 4 ++- .../sectionListViewsConfig.spec.ts | 35 +++++++++++++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 src/lib/sectionList/sectionListViewsConfig.spec.ts diff --git a/i18n/en.pot b/i18n/en.pot index 7b74c9db..49ba40cc 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: 2024-03-20T01:08:59.818Z\n" -"PO-Revision-Date: 2024-03-20T01:08:59.818Z\n" +"POT-Creation-Date: 2024-04-30T06:34:11.673Z\n" +"PO-Revision-Date: 2024-04-30T06:34:11.673Z\n" msgid "schemas" msgstr "schemas" @@ -75,6 +75,12 @@ msgstr "Save and close" msgid "" msgstr "" +msgid "Custom attributes" +msgstr "Custom attributes" + +msgid "Custom fields for your DHIS2 instance" +msgstr "Custom fields for your DHIS2 instance" + msgid "Code" msgstr "Code" @@ -759,9 +765,6 @@ msgstr "This field requires a unique value, please choose another one" msgid "Required" msgstr "Required" -msgid "Custom attributes" -msgstr "Custom attributes" - msgid "Exit without saving" msgstr "Exit without saving" @@ -792,21 +795,9 @@ msgstr "Set up the information for this data element group" msgid "Explain the purpose of this data element group." msgstr "Explain the purpose of this data element group." -msgid "@TODO" -msgstr "@TODO" - -msgid "Custom fields for your DHIS2 instance" -msgstr "Custom fields for your DHIS2 instance" - msgid "Selected data elements" msgstr "Selected data elements" -msgid "Explain the purpose of this data element and how it's measured." -msgstr "Explain the purpose of this data element and how it's measured." - -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 "Create data element" msgstr "Create data element" @@ -896,8 +887,8 @@ msgstr "Disaggregation and Option sets" msgid "Set up disaggregation and predefined options." msgstr "Set up disaggregation and predefined options." -msgid "LegendSet" -msgstr "LegendSet" +msgid "Legend set" +msgstr "Legend set" msgid "" "Visualize values for this data element in Analytics app. Multiple legendSet " diff --git a/src/app/sidebar/sidenav/Sidenav.tsx b/src/app/sidebar/sidenav/Sidenav.tsx index accc79a0..399db122 100644 --- a/src/app/sidebar/sidenav/Sidenav.tsx +++ b/src/app/sidebar/sidenav/Sidenav.tsx @@ -4,7 +4,9 @@ import React, { PropsWithChildren } from 'react' import styles from './Sidenav.module.css' export const Sidenav = ({ children }: PropsWithChildren) => ( - + ) export const SidenavItems = ({ children }: PropsWithChildren) => ( diff --git a/src/lib/sectionList/sectionListViewsConfig.spec.ts b/src/lib/sectionList/sectionListViewsConfig.spec.ts new file mode 100644 index 00000000..c873a22f --- /dev/null +++ b/src/lib/sectionList/sectionListViewsConfig.spec.ts @@ -0,0 +1,35 @@ +import { modelListViewsConfig } from './sectionListViewsConfig' + +test('All but specified configs must contain defaults', () => { + // If a section should not contain the default model view config, + // put the section name here + const sectionsWithoutDefaults = Object + .entries(modelListViewsConfig) + .filter(([name]) => !([] as string[]).includes(name)) + + sectionsWithoutDefaults.forEach(([, config]) => { + expect(config.columns.available).toEqual( + expect.arrayContaining(modelListViewsConfig.default.columns.available) + ) + expect(config.columns.default).toEqual( + expect.arrayContaining(modelListViewsConfig.default.columns.default) + ) + expect(config.filters.available).toEqual( + expect.arrayContaining(modelListViewsConfig.default.filters.available) + ) + expect(config.filters.default).toEqual( + expect.arrayContaining(modelListViewsConfig.default.filters.default) + ) + }) +}) + +test('defaults must be available', () => { + Object.values(modelListViewsConfig).forEach(config => { + expect(config.columns.available).toEqual( + expect.arrayContaining(config.columns.default) + ) + expect(config.filters.available).toEqual( + expect.arrayContaining(config.filters.default) + ) + }) +}) From 7abdde25af41f0ad4d44408608032b12eb93598d Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Wed, 13 Mar 2024 15:14:15 +0800 Subject: [PATCH 09/10] test(data element new form): add cypress test --- .github/workflows/dhis2-verify-app.yml | 6 +- .gitignore | 3 +- cypress/e2e/dataElements/New.spec.ts | 209 ++++++++++++++++++ i18n/en.pot | 4 +- .../sectionListViewsConfig.spec.ts | 35 --- 5 files changed, 216 insertions(+), 41 deletions(-) create mode 100644 cypress/e2e/dataElements/New.spec.ts delete mode 100644 src/lib/sectionList/sectionListViewsConfig.spec.ts diff --git a/.github/workflows/dhis2-verify-app.yml b/.github/workflows/dhis2-verify-app.yml index 174c8a3b..30a3fe61 100644 --- a/.github/workflows/dhis2-verify-app.yml +++ b/.github/workflows/dhis2-verify-app.yml @@ -69,7 +69,7 @@ jobs: run: yarn d2-app-scripts test --coverage - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 e2e: runs-on: ubuntu-latest @@ -98,8 +98,8 @@ jobs: BROWSER: none GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - CYPRESS_dhis2BaseUrl: https://debug.dhis2.org/dev + CYPRESS_dhis2BaseUrl: https://debug.dhis2.org/2.41dev CYPRESS_dhis2Username: ${{ secrets.CYPRESS_DHIS2_USERNAME }} CYPRESS_dhis2Password: ${{ secrets.CYPRESS_DHIS2_PASSWORD }} - CYPRESS_dhis2ApiVersion: 40 + CYPRESS_dhis2ApiVersion: 41 CYPRESS_networkMode: live diff --git a/.gitignore b/.gitignore index 196a4d6a..732e9121 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ cypress.env.json .env.test.local .env.production.local cypress/screenshots/* -coverage/ \ No newline at end of file +coverage/ +cypress.env.json diff --git a/cypress/e2e/dataElements/New.spec.ts b/cypress/e2e/dataElements/New.spec.ts new file mode 100644 index 00000000..f934cd92 --- /dev/null +++ b/cypress/e2e/dataElements/New.spec.ts @@ -0,0 +1,209 @@ +describe('Data elements', () => { + it('should create a data element with only the required values', () => { + const now = Date.now() + const newDataElementName = `ZZZ ${now}` // Will be at the end, does not pollute the first page of the list + + cy.visit('/') + + // Open data elements group in side nav + cy.get('[data-test="sidenav"] button:contains("Data elements")', { + timeout: 10000, + }).click() + + // Navigate to data element list view + cy.get('[data-test="sidenav"] a:contains("Data element")') + .first() // the selector will also grab "Data element group" and "Data element group set" + .click() + + // Go to New form + cy.get('button:contains("New")').click() + + cy.get('[data-test="formfields-name-content"] input').type( + newDataElementName + ) + cy.get('[data-test="formfields-shortname-content"] input').type( + `shortname ${now}` + ) + cy.get( + '[data-test="formfields-categorycombo"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("None")' + ).click() + + // Submit form + cy.get('button:contains("Create data element")').click() + + cy.contains('Data element management').should('exist') + }) + + it('should create a data element with all values', () => { + const now = Date.now() + const newDataElementName = `ZZZ ${now}` // Will be at the end, does not pollute the first page of the list + + cy.visit('/') + + // Open data elements group in side nav + cy.get('[data-test="sidenav"] button:contains("Data elements")', { + timeout: 10000, + }).click() + + // Navigate to data element list view + cy.get('[data-test="sidenav"] a:contains("Data element")') + .first() // the selector will also grab "Data element group" and "Data element group set" + .click() + + // Go to New form + cy.get('button:contains("New")').click() + + cy.get('[data-test="formfields-name"] input').type(newDataElementName) + cy.get('[data-test="formfields-shortname"] input').type( + `Short name ${now}` + ) + cy.get('[data-test="formfields-formname"] input').type( + `Form name ${now}` + ) + cy.get('[data-test="formfields-code"] input').type(`Code ${now}`) + cy.get('[data-test="formfields-description"] textarea').type( + `Multiline{enter}description ${now}` + ) + cy.get('[data-test="formfields-url"] input').type( + `https://dhis2.org ${now}` + ) + + // pick color + cy.get('[data-test="colorpicker-trigger"]').click() + cy.get('[title="#b71c1c"]').click() + + // icon color + cy.get('[data-test="iconpicker-trigger"]').click() + cy.get('[data-test="dhis2-uicore-modal"] img[src$="/icon"]') + .first() + .click() + cy.get( + '[data-test="dhis2-uicore-modal"] button:contains("Select")' + ).click() + + cy.get('[data-test="formfields-fieldmask"] input').type( + `000 1111 000 ${now}` + ) + cy.get('[data-test="formfields-zeroissignificant"] input').check() + + // Select value type + cy.get( + '[data-test="formfields-valuetype"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("Integer")' + ).click() + + // Select aggregation type + cy.get( + '[data-test="formfields-aggregationtype"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("Sum")' + ).click() + + // Select category combo + cy.get( + '[data-test="formfields-categorycombo"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("None")' + ).click() + + // Select category combo + cy.get( + '[data-test="formfields-optionset"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("ARV drugs")' + ).click() + + // Select category combo + cy.get( + '[data-test="formfields-commentoptionset"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("ARV treatment plan")' + ).click() + + // Select legend sets + cy.get( + '[data-test="formfields-legendsets"] [data-test="dhis2-uicore-transferoption"]:contains("ANC Coverage")' + ).dblclick() + cy.get( + '[data-test="formfields-legendsets"] [data-test="dhis2-uicore-transferoption"]:contains("Age 10y interval")' + ).dblclick() + + // Select aggregation levels + cy.get( + '[data-test="formfields-aggregationlevels"] [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-checkbox"]:contains("Chiefdom")' + ).click() + cy.get( + '[data-test="dhis2-uicore-checkbox"]:contains("District")' + ).click() + cy.get('.backdrop').click() + + // Select custom attribute "Classification" + cy.get( + '[data-test="dhis2-uiwidgets-singleselectfield"]:contains("Classification") [data-test="dhis2-uicore-select-input"]' + ).click() + cy.get( + '[data-test="dhis2-uicore-singleselectoption"]:contains("Input")' + ).click() + + cy.get('[name="attributeValues[1].value"]').type( + `Collection{enter}method! ${now}` + ) + cy.get('[name="attributeValues[2].value"]').type(`PEPFAR ID! ${now}`) + cy.get('[name="attributeValues[3].value"]').type(`Rationale! ${now}`) + cy.get('[name="attributeValues[4].value"]').type( + `Unit of measure! ${now}` + ) + + // Submit form + cy.get('button:contains("Create data element")').click() + + cy.contains('Data element management').should('exist') + }) + + it('should not create a DE when reuired fields are missing', () => { + cy.visit('/') + + // Open data elements group in side nav + cy.get('[data-test="sidenav"] button:contains("Data elements")', { + timeout: 10000, + }).click() + + // Navigate to data element list view + cy.get('[data-test="sidenav"] a:contains("Data element")') + .first() // the selector will also grab "Data element group" and "Data element group set" + .click() + + // Go to New form + cy.get('button:contains("New")').click() + + // Submit form + cy.get('button:contains("Create data element")').click() + + // Should have required errors for name, shortname and cat combo + cy.get('[data-test$="-validation"]:contains("Required")').should( + 'have.length', + 3 + ) + cy.get( + '[data-test="formfields-name-validation"]:contains("Required")' + ).should('exist') + cy.get( + '[data-test="formfields-shortname-validation"]:contains("Required")' + ).should('exist') + cy.get( + '[data-test="formfields-categorycombo-validation"]:contains("Required")' + ).should('exist') + }) +}) diff --git a/i18n/en.pot b/i18n/en.pot index 49ba40cc..c151fec8 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: 2024-04-30T06:34:11.673Z\n" -"PO-Revision-Date: 2024-04-30T06:34:11.673Z\n" +"POT-Creation-Date: 2024-04-30T06:34:26.402Z\n" +"PO-Revision-Date: 2024-04-30T06:34:26.402Z\n" msgid "schemas" msgstr "schemas" diff --git a/src/lib/sectionList/sectionListViewsConfig.spec.ts b/src/lib/sectionList/sectionListViewsConfig.spec.ts deleted file mode 100644 index c873a22f..00000000 --- a/src/lib/sectionList/sectionListViewsConfig.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { modelListViewsConfig } from './sectionListViewsConfig' - -test('All but specified configs must contain defaults', () => { - // If a section should not contain the default model view config, - // put the section name here - const sectionsWithoutDefaults = Object - .entries(modelListViewsConfig) - .filter(([name]) => !([] as string[]).includes(name)) - - sectionsWithoutDefaults.forEach(([, config]) => { - expect(config.columns.available).toEqual( - expect.arrayContaining(modelListViewsConfig.default.columns.available) - ) - expect(config.columns.default).toEqual( - expect.arrayContaining(modelListViewsConfig.default.columns.default) - ) - expect(config.filters.available).toEqual( - expect.arrayContaining(modelListViewsConfig.default.filters.available) - ) - expect(config.filters.default).toEqual( - expect.arrayContaining(modelListViewsConfig.default.filters.default) - ) - }) -}) - -test('defaults must be available', () => { - Object.values(modelListViewsConfig).forEach(config => { - expect(config.columns.available).toEqual( - expect.arrayContaining(config.columns.default) - ) - expect(config.filters.available).toEqual( - expect.arrayContaining(config.filters.default) - ) - }) -}) From 77f569550a85ca221a59a839a90d3bd9c2f77df2 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 14 May 2024 10:48:23 +0200 Subject: [PATCH 10/10] feat(list): add list for group and groupset (#379) * feat(list): add generic list component, use for de group/set * refactor: rename to GenericSectionList * refactor(list): use component files instead of array of enabled routes * fix(list): revert dataelement list refactor due to failing tests * refactor: minor cleanup --- .../sectionList/listView/ManageListView.tsx | 3 -- .../sectionList/listView/useModelListView.tsx | 2 +- .../listViews/sectionListViewsConfig.ts | 4 +- src/pages/DefaultSectionList.tsx | 54 +++++++++++++++++++ src/pages/dataElementGroupSets/List.tsx | 3 ++ src/pages/dataElementGroups/List.tsx | 3 ++ 6 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 src/pages/DefaultSectionList.tsx create mode 100644 src/pages/dataElementGroupSets/List.tsx create mode 100644 src/pages/dataElementGroups/List.tsx diff --git a/src/components/sectionList/listView/ManageListView.tsx b/src/components/sectionList/listView/ManageListView.tsx index 7267f2fc..e78269c7 100644 --- a/src/components/sectionList/listView/ManageListView.tsx +++ b/src/components/sectionList/listView/ManageListView.tsx @@ -44,9 +44,6 @@ const validate = (values: FormValues) => { if (values.columns.length < 1) { errors.columns = i18n.t('At least one column must be selected') } - if (values.filters.length < 1) { - errors.filters = i18n.t('At least one filter must be selected') - } return errors } export const ManageListView = ({ diff --git a/src/components/sectionList/listView/useModelListView.tsx b/src/components/sectionList/listView/useModelListView.tsx index 6f89f414..189901b4 100644 --- a/src/components/sectionList/listView/useModelListView.tsx +++ b/src/components/sectionList/listView/useModelListView.tsx @@ -114,7 +114,7 @@ const createValidViewSelect = (sectionName: string) => { return getDefaultViewForSection(sectionName) } - const viewForSection = modelListViews.data[sectionName][0] + const viewForSection = modelListViews.data[sectionName]?.[0] if (!viewForSection) { return getDefaultViewForSection(sectionName) } diff --git a/src/lib/sectionList/listViews/sectionListViewsConfig.ts b/src/lib/sectionList/listViews/sectionListViewsConfig.ts index 0a72a568..c83141c5 100644 --- a/src/lib/sectionList/listViews/sectionListViewsConfig.ts +++ b/src/lib/sectionList/listViews/sectionListViewsConfig.ts @@ -64,7 +64,7 @@ export const defaultModelViewConfig = { default: ['name', DESCRIPTORS.publicAccess, 'lastUpdated'], }, filters: { - available: [], + available: [DESCRIPTORS.publicAccess], default: [ // NOTE: Identifiable is special, and is always included in the default filters // It should not be handled the same way as "configurable" filters @@ -93,8 +93,8 @@ export const modelListViewsConfig = { ], }, filters: { + available: [], default: ['domainType', 'valueType', 'dataSet', 'categoryCombo'], - available: [DESCRIPTORS.publicAccess], }, }, } satisfies SectionListViewConfig diff --git a/src/pages/DefaultSectionList.tsx b/src/pages/DefaultSectionList.tsx new file mode 100644 index 00000000..fcfdfa62 --- /dev/null +++ b/src/pages/DefaultSectionList.tsx @@ -0,0 +1,54 @@ +import { FetchError, useDataEngine } from '@dhis2/app-runtime' +import React from 'react' +import { useQuery } from 'react-query' +import { SectionListWrapper } from '../components' +import { useModelListView } from '../components/sectionList/listView' +import { + useSchemaFromHandle, + useParamsForDataQuery, + BaseListModel, + DEFAULT_FIELD_FILTERS, +} from '../lib' +import { getFieldFilter } from '../lib/models/path' +import { WrapQueryResponse } from '../types' +import { PagedResponse } from '../types/models' + +type ModelListResponse = WrapQueryResponse> + +export const DefaultSectionList = () => { + const { columns } = useModelListView() + const schema = useSchemaFromHandle() + const engine = useDataEngine() + const modelListName = schema.plural + + const initialParams = useParamsForDataQuery() + + const query = { + result: { + resource: modelListName, + params: { + ...initialParams, + fields: columns + .map((column) => getFieldFilter(schema, column.path)) + .concat(DEFAULT_FIELD_FILTERS), + }, + }, + } + const { error, data } = useQuery({ + queryKey: [query], + queryFn: ({ queryKey: [query], signal }) => { + return engine.query(query, { signal }) as Promise + }, + }) + const modelList = data?.result[modelListName] + + return ( +
+ +
+ ) +} diff --git a/src/pages/dataElementGroupSets/List.tsx b/src/pages/dataElementGroupSets/List.tsx new file mode 100644 index 00000000..e03292f8 --- /dev/null +++ b/src/pages/dataElementGroupSets/List.tsx @@ -0,0 +1,3 @@ +import { DefaultSectionList } from '../DefaultSectionList' + +export const Component = DefaultSectionList diff --git a/src/pages/dataElementGroups/List.tsx b/src/pages/dataElementGroups/List.tsx new file mode 100644 index 00000000..e03292f8 --- /dev/null +++ b/src/pages/dataElementGroups/List.tsx @@ -0,0 +1,3 @@ +import { DefaultSectionList } from '../DefaultSectionList' + +export const Component = DefaultSectionList