diff --git a/i18n/en.pot b/i18n/en.pot
index c63f1ee4..e756f185 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-11-23T14:09:23.068Z\n"
-"PO-Revision-Date: 2023-11-23T14:09:23.068Z\n"
+"POT-Creation-Date: 2024-03-01T17:52:14.520Z\n"
+"PO-Revision-Date: 2024-03-01T17:52:14.520Z\n"
msgid "schemas"
msgstr "schemas"
@@ -66,6 +66,12 @@ msgstr "All"
msgid "Filter options"
msgstr "Filter options"
+msgid "Something went wrong when submitting the form"
+msgstr "Something went wrong when submitting the form"
+
+msgid "Save and close"
+msgstr "Save and close"
+
msgid "Failed to load {{label}}"
msgstr "Failed to load {{label}}"
@@ -99,8 +105,8 @@ msgstr "New"
msgid "Download"
msgstr "Download"
-msgid "Manage Columns"
-msgstr "Manage Columns"
+msgid "Manage View"
+msgstr "Manage View"
msgid "There aren't any items that match your filter."
msgstr "There aren't any items that match your filter."
@@ -153,38 +159,56 @@ msgstr "Details"
msgid "Failed to load details"
msgstr "Failed to load details"
+msgid "Clear all filters"
+msgstr "Clear all filters"
+
msgid "Type to filter options"
msgstr "Type to filter options"
msgid "No matches"
msgstr "No matches"
-msgid "Clear all filters"
-msgstr "Clear all filters"
+msgid "Data set"
+msgstr "Data set"
msgid "Search by name, code or ID"
msgstr "Search by name, code or ID"
+msgid "Public access"
+msgstr "Public access"
+
msgid "At least one column must be selected"
msgstr "At least one column must be selected"
-msgid "Available table columns"
-msgstr "Available table columns"
+msgid "At least one filter must be selected"
+msgstr "At least one filter must be selected"
-msgid "Selected table columns"
-msgstr "Selected table columns"
+msgid "An unknown error occurred"
+msgstr "An unknown error occurred"
-msgid "Reset to default columns"
-msgstr "Reset to default columns"
+msgid "Available columns"
+msgstr "Available columns"
+
+msgid "Selected columns"
+msgstr "Selected columns"
+
+msgid "Available filters"
+msgstr "Available filters"
+
+msgid "Selected filters"
+msgstr "Selected filters"
msgid "Failed to save"
msgstr "Failed to save"
-msgid "Manage {{section}} table columns"
-msgstr "Manage {{section}} table columns"
+msgid "Reset to default"
+msgstr "Reset to default"
-msgid "Update table columns"
-msgstr "Update table columns"
+msgid "Manage {{section}} view"
+msgstr "Manage {{section}} view"
+
+msgid "Update view"
+msgstr "Update view"
msgid "Public can edit"
msgstr "Public can edit"
@@ -195,15 +219,6 @@ msgstr "Public can view"
msgid "Public cannot access"
msgstr "Public cannot access"
-msgid "Public access"
-msgstr "Public access"
-
-msgid "Domain"
-msgstr "Domain"
-
-msgid "Value type"
-msgstr "Value type"
-
msgid "Category"
msgstr "Category"
@@ -258,9 +273,6 @@ 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"
@@ -630,12 +642,6 @@ msgstr "Required"
msgid "Custom attributes"
msgstr "Custom attributes"
-msgid "Something went wrong when submitting the form"
-msgstr "Something went wrong when submitting the form"
-
-msgid "Save and close"
-msgstr "Save and close"
-
msgid "Exit without saving"
msgstr "Exit without saving"
@@ -679,6 +685,9 @@ msgstr "Description"
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 "Domain"
+msgstr "Domain"
+
msgid "A data element can either be aggregated or tracked data."
msgstr "A data element can either be aggregated or tracked data."
diff --git a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
index 496a3759..91c2cf91 100644
--- a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
+++ b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx
@@ -6,11 +6,10 @@ import {
SingleSelectOption,
} from '@dhis2/ui'
import React, { forwardRef, useEffect, useState } from 'react'
-// import { useDebouncedCallback } from 'use-debounce'
import { useDebouncedState } from '../../lib'
import classes from './SearchableSingleSelect.module.css'
-interface Option {
+export interface Option {
value: string
label: string
}
@@ -53,8 +52,10 @@ interface SearchableSingleSelectPropTypes {
onFilterChange: OnFilterChange
onEndReached: () => void
onRetryClick: () => void
+ dense?: boolean
options: Option[]
placeholder: string
+ prefix?: string
showEndLoader: boolean
loading: boolean
disabled?: boolean
@@ -70,8 +71,10 @@ export const SearchableSingleSelect = ({
invalid,
disabled,
error,
+ dense,
loading,
placeholder,
+ prefix,
onBlur,
onChange,
onEndReached,
@@ -129,8 +132,10 @@ export const SearchableSingleSelect = ({
error={invalid}
onChange={onChange}
placeholder={placeholder}
+ prefix={prefix}
onBlur={onBlur}
onFocus={onFocus}
+ dense={dense}
>
diff --git a/src/components/SearchableSingleSelect/index.ts b/src/components/SearchableSingleSelect/index.ts
index 7f9aa7b6..92c577e4 100644
--- a/src/components/SearchableSingleSelect/index.ts
+++ b/src/components/SearchableSingleSelect/index.ts
@@ -1 +1 @@
-export { SearchableSingleSelect } from './SearchableSingleSelect'
+export * from './SearchableSingleSelect'
diff --git a/src/components/sectionList/SectionListHeaderNormal.tsx b/src/components/sectionList/SectionListHeaderNormal.tsx
index 3e76e34b..1724d730 100644
--- a/src/components/sectionList/SectionListHeaderNormal.tsx
+++ b/src/components/sectionList/SectionListHeaderNormal.tsx
@@ -20,7 +20,7 @@ export const SectionListHeader = () => {
{manageColumnsOpen && (
diff --git a/src/components/sectionList/SectionListWrapper.tsx b/src/components/sectionList/SectionListWrapper.tsx
index 2dacbcea..1f1c9052 100644
--- a/src/components/sectionList/SectionListWrapper.tsx
+++ b/src/components/sectionList/SectionListWrapper.tsx
@@ -16,19 +16,16 @@ import { SectionListRow } from './SectionListRow'
import { SectionListTitle } from './SectionListTitle'
type SectionListWrapperProps = {
- filterElement?: React.ReactElement
data: ModelCollection | undefined
pager: Pager | undefined
error: FetchError | undefined
}
export const SectionListWrapper = ({
- filterElement,
data,
error,
pager,
}: SectionListWrapperProps) => {
- data
const { columns: headerColumns } = useModelListView()
const schema = useSchemaFromHandle()
const [selectedModels, setSelectedModels] = useState
>(new Set())
@@ -78,7 +75,7 @@ export const SectionListWrapper = ({
return (
-
{filterElement}
+
>
+
+const filterKeyToComponentMap: FilterKeyToComponentMap = {
+ categoryCombo: CategoryComboFilter,
+ dataSet: DataSetFilter,
+ domainType: DomainTypeSelectionFilter,
+ valueType: ValueTypeSelectionFilter,
+ aggregationType: AggregationTypeFilter,
+ publicAccess: PublicAccessFilter,
+}
+
+export const DynamicFilters = () => {
+ const filterKeys = useFilterKeys()
+ return (
+ <>
+ {filterKeys.map((filterKey) => {
+ const FilterComponent = filterKeyToComponentMap[filterKey]
+ return FilterComponent ? (
+
+ ) : null
+ })}
+ >
+ )
+}
diff --git a/src/components/sectionList/filters/FilterWrapper.module.css b/src/components/sectionList/filters/FilterWrapper.module.css
new file mode 100644
index 00000000..46b41f9e
--- /dev/null
+++ b/src/components/sectionList/filters/FilterWrapper.module.css
@@ -0,0 +1,6 @@
+.filterWrapper {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+}
diff --git a/src/components/sectionList/filters/FilterWrapper.tsx b/src/components/sectionList/filters/FilterWrapper.tsx
index 79a3fdf5..1c79c88c 100644
--- a/src/components/sectionList/filters/FilterWrapper.tsx
+++ b/src/components/sectionList/filters/FilterWrapper.tsx
@@ -2,12 +2,11 @@ import i18n from '@dhis2/d2-i18n'
import { Button } from '@dhis2/ui'
import React from 'react'
import { useSectionListFilters } from './../../../lib'
-import css from './Filters.module.css'
-import { IdentifiableFilter } from './IdentifiableFilter'
+import { DynamicFilters } from './DynamicFilters'
+import { IdentifiableFilter } from './filterSelectors/IdentifiableFilter'
+import css from './FilterWrapper.module.css'
-type FilterWrapperProps = React.PropsWithChildren
-
-export const FilterWrapper = ({ children }: FilterWrapperProps) => {
+export const FilterWrapper = () => {
const [, setFilters] = useSectionListFilters()
const handleClear = () => {
@@ -17,7 +16,7 @@ export const FilterWrapper = ({ children }: FilterWrapperProps) => {
return (
- {children}
+
diff --git a/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx b/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx
new file mode 100644
index 00000000..c8de3924
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/CategoryComboFilter.tsx
@@ -0,0 +1,24 @@
+import i18n from '@dhis2/d2-i18n'
+import React from 'react'
+import { useSectionListFilter } from '../../../../lib'
+import { createFilterDataQuery } from './createFilterDataQuery'
+import { ModelFilterSelect } from './ModelFilter'
+
+const query = createFilterDataQuery('categoryCombos')
+
+export const CategoryComboFilter = () => {
+ const [filter, setFilter] = useSectionListFilter('categoryCombo')
+
+ const selected = filter?.[0]
+
+ return (
+
+ setFilter(selected ? [selected] : undefined)
+ }
+ />
+ )
+}
diff --git a/src/components/sectionList/filters/ConstantFilters.tsx b/src/components/sectionList/filters/filterSelectors/ConstantFilters.tsx
similarity index 62%
rename from src/components/sectionList/filters/ConstantFilters.tsx
rename to src/components/sectionList/filters/filterSelectors/ConstantFilters.tsx
index 635b0340..202ee16a 100644
--- a/src/components/sectionList/filters/ConstantFilters.tsx
+++ b/src/components/sectionList/filters/filterSelectors/ConstantFilters.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { DOMAIN_TYPE, VALUE_TYPE } from '../../../lib'
+import { AGGREGATION_TYPE, DOMAIN_TYPE, VALUE_TYPE } from '../../../../lib'
import { ConstantSelectionFilter } from './ConstantSelectionFilter'
export const DomainTypeSelectionFilter = () => {
@@ -22,3 +22,14 @@ export const ValueTypeSelectionFilter = () => {
/>
)
}
+
+export const AggregationTypeFilter = () => {
+ return (
+
+ )
+}
diff --git a/src/components/sectionList/filters/ConstantSelectionFilter.tsx b/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx
similarity index 69%
rename from src/components/sectionList/filters/ConstantSelectionFilter.tsx
rename to src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx
index c518f14f..b6932eef 100644
--- a/src/components/sectionList/filters/ConstantSelectionFilter.tsx
+++ b/src/components/sectionList/filters/filterSelectors/ConstantSelectionFilter.tsx
@@ -1,8 +1,8 @@
import i18n from '@dhis2/d2-i18n'
import { SingleSelect, SingleSelectOption } from '@dhis2/ui'
import React from 'react'
-import { FilterKey, useSectionListFilter } from '../../../lib'
-import { SelectOnChangeObject } from '../../../types'
+import { FilterKey, useSectionListFilter } from '../../../../lib'
+import { SelectOnChangeObject } from '../../../../types'
import css from './Filters.module.css'
type ConstantSelectionFilterProps = {
@@ -10,6 +10,7 @@ type ConstantSelectionFilterProps = {
constants: Record
filterKey: FilterKey
filterable?: boolean
+ formatFilter?: (filter: string | undefined) => string | undefined
}
export const ConstantSelectionFilter = ({
@@ -17,18 +18,28 @@ export const ConstantSelectionFilter = ({
filterKey,
label,
filterable,
+ formatFilter,
}: ConstantSelectionFilterProps) => {
const [filter, setFilter] = useSectionListFilter(filterKey)
+ let selected = Array.isArray(filter) ? filter[0] : filter
+ if (formatFilter) {
+ selected = formatFilter(selected)
+ }
+
+ const isInOptions =
+ selected && constants[selected as keyof typeof constants]
+
return (
{
setFilter(selected ? [selected] : undefined)
}}
- selected={Array.isArray(filter) ? filter[0] : filter}
+ selected={isInOptions ? selected : undefined}
placeholder={label}
dense
+ prefix={label}
filterable={filterable}
filterPlaceholder={i18n.t('Type to filter options')}
noMatchText={i18n.t('No matches')}
diff --git a/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx b/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx
new file mode 100644
index 00000000..f0c387fc
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/DataSetFilter.tsx
@@ -0,0 +1,24 @@
+import i18n from '@dhis2/d2-i18n'
+import React from 'react'
+import { useSectionListFilter } from '../../../../lib'
+import { createFilterDataQuery } from './createFilterDataQuery'
+import { ModelFilterSelect } from './ModelFilter'
+
+const query = createFilterDataQuery('dataSets')
+
+export const DataSetFilter = () => {
+ const [filter, setFilter] = useSectionListFilter('dataSet')
+
+ const selected = filter?.[0]
+
+ return (
+
+ setFilter(selected ? [selected] : undefined)
+ }
+ />
+ )
+}
diff --git a/src/components/sectionList/filters/Filters.module.css b/src/components/sectionList/filters/filterSelectors/Filters.module.css
similarity index 100%
rename from src/components/sectionList/filters/Filters.module.css
rename to src/components/sectionList/filters/filterSelectors/Filters.module.css
diff --git a/src/components/sectionList/filters/IdentifiableFilter.tsx b/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx
similarity index 94%
rename from src/components/sectionList/filters/IdentifiableFilter.tsx
rename to src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx
index deab647a..75cf841a 100644
--- a/src/components/sectionList/filters/IdentifiableFilter.tsx
+++ b/src/components/sectionList/filters/filterSelectors/IdentifiableFilter.tsx
@@ -3,13 +3,13 @@ import { Input, InputEventPayload } from '@dhis2/ui'
import React, { useEffect, useState } from 'react'
import {
useDebounce,
- IDENTIFIABLE_KEY,
+ IDENTIFIABLE_FILTER_KEY,
useSectionListFilter,
-} from '../../../lib'
+} from '../../../../lib'
import css from './Filters.module.css'
export const IdentifiableFilter = () => {
- const [filter, setFilter] = useSectionListFilter(IDENTIFIABLE_KEY)
+ const [filter, setFilter] = useSectionListFilter(IDENTIFIABLE_FILTER_KEY)
const [value, setValue] = useState(filter || '')
const debouncedValue = useDebounce(value, 200)
diff --git a/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx b/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx
new file mode 100644
index 00000000..7377b2b0
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/ModelFilter.tsx
@@ -0,0 +1,154 @@
+import { useDataQuery } from '@dhis2/app-runtime'
+import React, { useCallback, useRef, useState } from 'react'
+import { useInfiniteDataQuery } from '../../../../lib/query'
+import type { ResultQuery, WrapQueryResponse } from '../../../../types'
+import { Option, SearchableSingleSelect } from '../../../SearchableSingleSelect'
+
+type OptionResult = {
+ id: string
+ displayName: string
+}
+
+function computeDisplayOptions({
+ initialSelectedOption,
+ initialSelected,
+ options,
+}: {
+ options: OptionResult[]
+ initialSelected?: string
+ initialSelectedOption?: OptionResult
+}): Option[] {
+ // This happens only when we haven't fetched the label for an initially
+ // selected value. Don't show anything to prevent error that an option is
+ // missing
+ if (!initialSelectedOption && initialSelected) {
+ return []
+ }
+
+ const optionsContainSelected = options?.find(
+ ({ id }) => id === initialSelected
+ )
+
+ const withSelectedOption =
+ initialSelectedOption && !optionsContainSelected
+ ? [...options, initialSelectedOption]
+ : options
+
+ return withSelectedOption.map((option) => ({
+ value: option.id,
+ label: option.displayName,
+ }))
+}
+
+const createInitialOptionQuery = (resource: string): ResultQuery => ({
+ result: {
+ resource: resource,
+ id: (variables: Record) => variables.id,
+ params: {
+ fields: ['id', 'displayName'],
+ },
+ },
+})
+
+export interface ModelSingleSelectProps {
+ onChange: ({ selected }: { selected: string | undefined }) => void
+ selected?: string
+ placeholder: string
+ query: ResultQuery
+}
+
+export const ModelFilterSelect = ({
+ onChange,
+ selected,
+ query,
+ placeholder,
+}: ModelSingleSelectProps) => {
+ // Using a ref because we don't want to react to changes.
+ // We're using this value only when imperatively calling `refetch`,
+ // nothing that depends on the render-cycle depends on this value
+ const filterRef = useRef()
+ const initialSelected = useRef(selected)
+
+ // this is only done once, and will not update if query changes
+ // using useState instead of useRef because it's unecessary to call this every render
+ // and useRef does not support a callback
+ const [initialQuery] = useState(() =>
+ createInitialOptionQuery(query.result.resource)
+ )
+
+ const initialOptionResult = useDataQuery>(
+ initialQuery,
+ {
+ // run only when we have an initial selected value
+ lazy: initialSelected.current === undefined,
+ variables: { id: selected },
+ }
+ )
+
+ const initialSelectedOption = initialOptionResult.data?.result
+
+ const optionsQueryResult = useInfiniteDataQuery(query)
+ const { refetch, data, incrementPage } = optionsQueryResult
+
+ const pager = data?.result.pager
+ const page = pager?.page || 0
+ const pageCount = pager?.pageCount || 0
+
+ const refetchWithFilter = useCallback(
+ ({ value }: { value: string }) => {
+ filterRef.current = value ? `displayName:ilike:${value}` : undefined
+ refetch({
+ page: 1,
+ filter: filterRef.current,
+ })
+ },
+ [refetch]
+ )
+
+ const loading =
+ optionsQueryResult.fetching ||
+ optionsQueryResult.loading ||
+ initialOptionResult.loading
+
+ const error =
+ optionsQueryResult.error || initialOptionResult.error
+ ? // @TODO: Ask Joe what do do here!
+ 'An error has occurred. Please try again'
+ : ''
+
+ const dataResultKey = query.result.resource
+ const options = data?.result[dataResultKey] || []
+
+ const displayOptions = computeDisplayOptions({
+ initialSelectedOption,
+ initialSelected: initialSelected.current,
+ options,
+ })
+
+ return (
+
+ {
+ onChange({ selected })
+ }}
+ onEndReached={incrementPage}
+ options={displayOptions}
+ selected={selected}
+ showEndLoader={!loading && page < pageCount}
+ onFilterChange={refetchWithFilter}
+ loading={loading}
+ error={error}
+ onRetryClick={() => {
+ refetch({
+ page: page,
+ filter: filterRef.current,
+ })
+ }}
+ />
+
+ )
+}
diff --git a/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx b/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx
new file mode 100644
index 00000000..f9111c93
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/PublicAccessFilter.tsx
@@ -0,0 +1,39 @@
+import i18n from '@dhis2/d2-i18n'
+import React from 'react'
+import { formatPublicAccess, parsePublicAccessString } from '../../../../lib'
+import { ConstantSelectionFilter } from './ConstantSelectionFilter'
+
+// currently we only care about metadata access
+// we may want to revist this and potentially rename to "publicMetadataAccess"
+// and have another component for data access
+const constants = {
+ 'rw------': 'Public can edit',
+ 'r-------': 'Public can view',
+ '--------': 'Public cannot access',
+}
+
+export const PublicAccessFilter = () => {
+ const formatFilter = (filter: string | undefined) => {
+ if (!filter) {
+ return undefined
+ }
+ const parsedPublicAccessString = parsePublicAccessString(filter)
+ if (!parsedPublicAccessString) {
+ return undefined
+ }
+ const withoutDataAccess = formatPublicAccess({
+ metadata: parsedPublicAccessString.metadata,
+ data: { read: false, write: false },
+ })
+ return withoutDataAccess
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/sectionList/filters/filterSelectors/createFilterDataQuery.ts b/src/components/sectionList/filters/filterSelectors/createFilterDataQuery.ts
new file mode 100644
index 00000000..108c66ce
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/createFilterDataQuery.ts
@@ -0,0 +1,13 @@
+import { ResultQuery } from '../../../../types'
+
+export const createFilterDataQuery = (resource: string): ResultQuery => ({
+ result: {
+ resource: resource,
+ params: (params) => ({
+ ...params,
+ fields: ['id', 'displayName'],
+ order: 'displayName:asc',
+ pageSize: 5,
+ }),
+ },
+})
diff --git a/src/components/sectionList/filters/filterSelectors/index.ts b/src/components/sectionList/filters/filterSelectors/index.ts
new file mode 100644
index 00000000..55442aca
--- /dev/null
+++ b/src/components/sectionList/filters/filterSelectors/index.ts
@@ -0,0 +1,6 @@
+export * from './ConstantFilters'
+export * from './DataSetFilter'
+export * from './CategoryComboFilter'
+export * from './IdentifiableFilter'
+export * from './ConstantSelectionFilter'
+export * from './PublicAccessFilter'
diff --git a/src/components/sectionList/filters/index.ts b/src/components/sectionList/filters/index.ts
index 40486a57..8f2e9468 100644
--- a/src/components/sectionList/filters/index.ts
+++ b/src/components/sectionList/filters/index.ts
@@ -1,3 +1 @@
-export * from './ConstantSelectionFilter'
-export * from './IdentifiableFilter'
-export * from './ConstantFilters'
+export * from './filterSelectors'
diff --git a/src/components/sectionList/filters/useFilterKeys.tsx b/src/components/sectionList/filters/useFilterKeys.tsx
new file mode 100644
index 00000000..ab7425ed
--- /dev/null
+++ b/src/components/sectionList/filters/useFilterKeys.tsx
@@ -0,0 +1,31 @@
+import { useMemo } from 'react'
+import {
+ useSectionListFilters,
+ ConfigurableFilterKey,
+ IDENTIFIABLE_FILTER_KEY,
+} from '../../../lib'
+import { useModelListView } from '../listView'
+
+/**
+ * Get the filterKeys for for which filters to show.
+ * This depends on the current "modelList" view, and selected filters with values in the url */
+export const useFilterKeys = () => {
+ const [filters] = useSectionListFilters()
+ const { filters: viewFilters } = useModelListView()
+ // combine filters and views, since filters in URL might not be selected for view
+ // but we should show them when they have a value
+ const filterKeys = useMemo(() => {
+ const viewFilterKeys = viewFilters.map(({ filterKey }) => filterKey)
+ const selectedFiltersNotInView = Object.entries(filters)
+ .filter(
+ ([filterKey, value]) =>
+ value !== undefined &&
+ filterKey !== IDENTIFIABLE_FILTER_KEY &&
+ !viewFilterKeys.includes(filterKey as ConfigurableFilterKey)
+ )
+ .map(([filterKey]) => filterKey) as ConfigurableFilterKey[]
+
+ return viewFilterKeys.concat(selectedFiltersNotInView)
+ }, [filters, viewFilters])
+ return filterKeys
+}
diff --git a/src/components/sectionList/listView/ManageListView.module.css b/src/components/sectionList/listView/ManageListView.module.css
index 71f62f86..380055b8 100644
--- a/src/components/sectionList/listView/ManageListView.module.css
+++ b/src/components/sectionList/listView/ManageListView.module.css
@@ -1,9 +1,23 @@
-.resetDefaultButton {
- margin-top: var(--spacers-dp12) !important;
-}
-
.transferHeader {
margin: var(--spacers-dp8) 0px;
color: var(--colors-grey700);
font-weight: 400;
}
+
+.transferContainer {
+ padding-bottom: var(--spacers-dp16);
+}
+
+/* placing reset-button inside right-footer while keeping the ui implementation
+of reorder-buttons */
+.transferContainer :global(div[data-test='dhis2-uicore-transfer-rightfooter']) {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: var(--spacers-dp8);
+}
+
+.resetDefaultButton {
+ margin-top: var(--spacers-dp8);
+}
diff --git a/src/components/sectionList/listView/ManageListView.tsx b/src/components/sectionList/listView/ManageListView.tsx
index 70ced1f6..69f5d9d6 100644
--- a/src/components/sectionList/listView/ManageListView.tsx
+++ b/src/components/sectionList/listView/ManageListView.tsx
@@ -1,132 +1,211 @@
import { FetchError } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
-import { Button, Field, NoticeBox, Transfer } from '@dhis2/ui'
-import React, { useEffect, useMemo, useRef, useState } from 'react'
+import { Button, Field, NoticeBox, Transfer, TransferOption } from '@dhis2/ui'
+import { FORM_ERROR } from 'final-form'
+import React, { useMemo } from 'react'
+import { Form, useField } from 'react-final-form'
import {
getColumnsForSection,
+ getFiltersForSection,
useModelSectionHandleOrThrow,
} from '../../../lib'
import css from './ManageListView.module.css'
import { useModelListView, useMutateModelListViews } from './useModelListView'
interface RenderProps {
- handleSave: () => void
- isSaving: boolean
+ submitting: boolean
}
+
type ManageColumnsDialogProps = {
onSaved: () => void
children: (props: RenderProps) => React.ReactNode
}
const toPath = (propertyDescriptor: { path: string }) => propertyDescriptor.path
+const toFilterKey = (filterDescriptor: { filterKey: string }) =>
+ filterDescriptor.filterKey
+
+type FormValues = {
+ columns: string[]
+ filters: string[]
+}
+const validate = (values: FormValues) => {
+ const errors: Record = {}
+
+ 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 = ({
onSaved,
children,
}: ManageColumnsDialogProps) => {
+ const {
+ columns: savedColumns,
+ filters: savedFilters,
+ query,
+ } = useModelListView()
const section = useModelSectionHandleOrThrow()
- // ignore updates to saved-columns while selecting
- const isTouched = useRef(false)
+ const { saveView } = useMutateModelListViews()
- const { columns: savedColumns, query } = useModelListView()
- const [pendingSelectedColumns, setPendingSelectedColumns] = useState<
- string[]
- >(() => savedColumns.map(toPath))
- const [error, setError] = useState()
- const [saveError, setSaveError] = useState()
+ const columnsConfig = getColumnsForSection(section.name)
+ const filtersConfig = getFiltersForSection(section.name)
- const { saveColumns, mutation } = useMutateModelListViews()
+ const defaultColumns = columnsConfig.default.map(toPath)
+ const defaultFilters = filtersConfig.default.map(toFilterKey)
- const columnsConfig = getColumnsForSection(section.name)
+ const handleSave = async (values: FormValues) => {
+ const isDefault = (arr: string[], def: string[]) =>
+ arr.join() === def.join()
- useEffect(() => {
- // if savedColumns were to update while selecting (it shouldn't )
- // make sure to not overwrite the selected columns
- if (isTouched.current) {
- return
+ // save empty view if default, this makes the app able to update the default view
+ const view = {
+ name: 'default',
+ columns: isDefault(values.columns, defaultColumns)
+ ? []
+ : values.columns,
+ filters: isDefault(values.filters, defaultFilters)
+ ? []
+ : values.filters,
}
- setPendingSelectedColumns(savedColumns.map(toPath))
- }, [savedColumns])
- const handleSave = () => {
- if (pendingSelectedColumns.length < 1) {
- setError(i18n.t('At least one column must be selected'))
- return
- }
- saveColumns(pendingSelectedColumns, {
- onSuccess: () => onSaved(),
- onError: (error) => {
- if (error instanceof FetchError) {
- setSaveError(error)
- }
- },
+ return new Promise((resolve) => {
+ saveView(view, {
+ onSuccess: () => resolve(onSaved()),
+ onError: (error) => {
+ if (error instanceof FetchError) {
+ resolve({ [FORM_ERROR]: error.message })
+ }
+ resolve({
+ [FORM_ERROR]: i18n.t('An unknown error occurred'),
+ })
+ },
+ })
})
}
- const handleChange = ({ selected }: { selected: string[] }) => {
- isTouched.current = true
+ const initialValues = useMemo(() => {
+ return {
+ columns:
+ savedColumns.length > 0
+ ? savedColumns.map(toPath)
+ : defaultColumns,
+ filters:
+ savedFilters.length > 0
+ ? savedFilters.map(toFilterKey)
+ : defaultFilters,
+ }
+ }, [savedFilters, savedColumns, defaultColumns, defaultFilters])
- setPendingSelectedColumns(selected)
- setError(undefined)
- setSaveError(undefined)
- }
+ return (
+
+ )}
+
+ )
+}
+type TransferField = {
+ name: string
+ loading?: boolean
+ defaultOptions: string[]
+ availableOptions: TransferOption[]
+ availableLabel: string
+ selectedLabel: string
+}
+const TransferField = ({
+ name,
+ loading,
+ defaultOptions,
+ availableOptions,
+ availableLabel,
+ selectedLabel,
+}: TransferField) => {
+ const { input, meta } = useField(name, {
+ multiple: true,
+ })
const handleSetDefault = () => {
- handleChange({ selected: columnsConfig.default.map(toPath) })
+ input.onChange(defaultOptions)
}
- const transferOptions = useMemo(
- () =>
- columnsConfig.available
- .map((column) => ({
- label: column.label,
- value: column.path,
- }))
- .sort((a, b) => a.label.localeCompare(b.label)),
- [columnsConfig.available]
- )
-
return (
- <>
-
+
+
- {i18n.t('Available table columns')}
-
+ {availableLabel}
}
rightHeader={
-
- {i18n.t('Selected table columns')}
-
+ {selectedLabel}
+ }
+ selected={input.value}
+ onChange={({ selected }) => input.onChange(selected)}
+ options={availableOptions}
+ rightFooter={
+
}
- onChange={handleChange}
- loading={query.isLoading}
- loadingPicked={query.isLoading}
- options={transferOptions}
- selected={pendingSelectedColumns}
/>
-
- {saveError && (
-
-
- {saveError.message}
-
-
- )}
- {children({ handleSave, isSaving: mutation.isLoading })}
- >
+
)
}
diff --git a/src/components/sectionList/listView/ManageListViewDialog.tsx b/src/components/sectionList/listView/ManageListViewDialog.tsx
index 550e8020..8a1ab4bc 100644
--- a/src/components/sectionList/listView/ManageListViewDialog.tsx
+++ b/src/components/sectionList/listView/ManageListViewDialog.tsx
@@ -22,24 +22,24 @@ export const ManageListViewDialog = ({
return (
- {i18n.t('Manage {{section}} table columns', {
+ {i18n.t('Manage {{section}} view', {
section: section.title,
})}
- {({ handleSave, isSaving }) => (
+ {({ submitting }) => (
diff --git a/src/components/sectionList/listView/useModelListView.tsx b/src/components/sectionList/listView/useModelListView.tsx
index ddca5cd9..6f89f414 100644
--- a/src/components/sectionList/listView/useModelListView.tsx
+++ b/src/components/sectionList/listView/useModelListView.tsx
@@ -66,21 +66,22 @@ const parseViewToModelListView = (
const parsedView = listView.data
- const availableColumnsMap = new Map(
- viewConfig.columns.available.map((c) => [c.path, c] as const)
- )
// map to config to make sure we don't use invalid columns
// Preserve order by mapping from parsedView to config-object
- const columns = parsedView.columns
- .filter((col) => availableColumnsMap.has(col))
- .map((col) => {
- const columnConfig = availableColumnsMap.get(col)
- return columnConfig as NonNullable
- })
-
- const filters = viewConfig.filters.available.filter((filterDescriptor) =>
- parsedView.filters.includes(filterDescriptor.filterKey)
- )
+ const columns = parsedView.columns.flatMap((path) => {
+ const columnConfig = viewConfig.columns.available.find(
+ (col) => col.path === path
+ )
+ return columnConfig ? [columnConfig] : []
+ })
+
+ const filters = parsedView.filters.flatMap((filterKey) => {
+ const filterConfig = viewConfig.filters.available.find(
+ (filter) => filter.filterKey === filterKey
+ )
+
+ return filterConfig ? [filterConfig] : []
+ })
return {
...parsedView,
@@ -138,10 +139,17 @@ export const useModelListView = () => {
console.error(query.error)
}
- const selectedView = query.data || getDefaultViewForSection(section.name)
+ const defaultView = getDefaultViewForSection(section.name)
+ const selectedView = query.data || defaultView
- const columns = selectedView.columns
- const filters = selectedView.filters
+ const columns =
+ selectedView.columns.length < 1
+ ? defaultView.columns
+ : selectedView.columns
+ const filters =
+ selectedView.filters.length < 1
+ ? defaultView.filters
+ : selectedView.filters
return { view: selectedView, columns, filters, query }
}
@@ -221,5 +229,5 @@ export const useMutateModelListViews = () => {
[saveView]
)
- return { mutation, saveColumns }
+ return { mutation, saveColumns, saveView }
}
diff --git a/src/lib/constants/index.ts b/src/lib/constants/index.ts
index c8301647..c700910f 100644
--- a/src/lib/constants/index.ts
+++ b/src/lib/constants/index.ts
@@ -1,6 +1,5 @@
export * from './sections'
export * from './translatedModelConstants'
export * from './translatedModelProperties'
-export * from './sectionListView'
-export const IDENTIFIABLE_KEY = 'identifiable'
+export const IDENTIFIABLE_FILTER_KEY = 'identifiable'
diff --git a/src/lib/constants/translatedModelProperties.ts b/src/lib/constants/translatedModelProperties.ts
index 5ef6c0bf..e9ee6d00 100644
--- a/src/lib/constants/translatedModelProperties.ts
+++ b/src/lib/constants/translatedModelProperties.ts
@@ -10,6 +10,7 @@ const TRANSLATED_PROPERTY: Record = {
lastUpdatedBy: i18n.t('Last updated by'),
created: i18n.t('Created'),
domainType: i18n.t('Domain type'),
+ dataSet: i18n.t('Data set'),
lastUpdated: i18n.t('Last updated'),
name: i18n.t('Name'),
sharing: i18n.t('Sharing'),
diff --git a/src/lib/models/index.ts b/src/lib/models/index.ts
index 8d62b280..cdba6bb1 100644
--- a/src/lib/models/index.ts
+++ b/src/lib/models/index.ts
@@ -1,5 +1,8 @@
export { isValidUid } from './uid'
-export { parsePublicAccessString } from './parsePublicAccess'
+export {
+ parsePublicAccessString,
+ formatPublicAccess,
+} from './parsePublicAccess'
export { getIn, stringToPathArray, getFieldFilterFromPath } from './path'
export { useIsFieldValueUnique } from './useIsFieldValueUnique'
export { useCheckMaxLengthFromSchema } from './useCheckMaxLengthFromSchema'
diff --git a/src/lib/models/parsePublicAccess.ts b/src/lib/models/parsePublicAccess.ts
index 44e7ea7b..968cae63 100644
--- a/src/lib/models/parsePublicAccess.ts
+++ b/src/lib/models/parsePublicAccess.ts
@@ -26,10 +26,24 @@ export const parsePublicAccessString = (
if (!matches) {
return null
}
- const [_, metadata, data] = matches
+ const [, metadata, data] = matches
return {
metadata: parseAccessPart(metadata),
data: parseAccessPart(data),
}
}
+
+const accessPartToString = (accessPart: PublicAccessPart): string => {
+ if (accessPart.write) {
+ return 'rw'
+ }
+ return accessPart.read ? 'r-' : '--'
+}
+
+export const formatPublicAccess = (publicAccess: PublicAccess): string => {
+ const metadata = accessPartToString(publicAccess.metadata)
+ const data = accessPartToString(publicAccess.data)
+
+ return metadata + data + '----'
+}
diff --git a/src/lib/query/index.ts b/src/lib/query/index.ts
new file mode 100644
index 00000000..572da64c
--- /dev/null
+++ b/src/lib/query/index.ts
@@ -0,0 +1 @@
+export * from './useInfiniteDataQuery'
diff --git a/src/lib/query/useInfiniteDataQuery.ts b/src/lib/query/useInfiniteDataQuery.ts
new file mode 100644
index 00000000..41ff629f
--- /dev/null
+++ b/src/lib/query/useInfiniteDataQuery.ts
@@ -0,0 +1,73 @@
+import { useDataQuery } from '@dhis2/app-runtime'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { ResultQuery, WrapQueryResponse } from '../../types'
+import { Pager } from '../../types/generated'
+
+type PagerObject = {
+ pager: Pager
+}
+
+type PagedResponse = PagerObject & { [key: string]: TData[] }
+
+type InfiniteQueryResult = WrapQueryResponse>
+
+type QueryOptions = Parameters[1]
+type InfiniteQueryOptions = QueryOptions & {
+ dataResultKey?: string
+}
+export const useInfiniteDataQuery = (
+ query: ResultQuery,
+ options?: InfiniteQueryOptions
+) => {
+ const [allResult, setAllResult] = useState([])
+
+ let queryOptions: QueryOptions = undefined
+ let dataKey = query.result.resource
+ if (options) {
+ const { dataResultKey, ...opts } = options
+ dataKey = dataResultKey || query.result.resource
+ queryOptions = opts
+ }
+
+ const queryResult = useDataQuery>(
+ query,
+ queryOptions
+ )
+ const { refetch, data } = queryResult
+
+ const pager = data?.result.pager
+ const page = pager?.page || 0
+ const pageCount = pager?.pageCount || 0
+
+ useEffect(() => {
+ const result = data?.result[dataKey]
+ if (result) {
+ setAllResult((prev) => {
+ const pager = data.result.pager
+ if (pager.page === 1) {
+ return data.result[dataKey]
+ }
+ return [...prev, ...data.result[dataKey]]
+ })
+ }
+ }, [data, dataKey, setAllResult])
+
+ const incrementPage = useCallback(() => {
+ refetch({ page: page + 1 })
+ }, [refetch, page])
+
+ const newData = useMemo(() => {
+ return {
+ result: {
+ ...data?.result,
+ [dataKey]: allResult,
+ },
+ }
+ }, [allResult, data, dataKey])
+ return {
+ ...queryResult,
+ data: newData,
+ incrementPage,
+ hasNextPage: page < pageCount,
+ }
+}
diff --git a/src/lib/sectionList/filters/filterConfig.tsx b/src/lib/sectionList/filters/filterConfig.tsx
index 0fae40c3..54cc65e6 100644
--- a/src/lib/sectionList/filters/filterConfig.tsx
+++ b/src/lib/sectionList/filters/filterConfig.tsx
@@ -1,8 +1,8 @@
import { StringParam } from 'use-query-params'
import { z } from 'zod'
import { DataElement } from '../../../types/generated'
-import { IDENTIFIABLE_KEY } from '../../constants'
-import { isValidUid } from '../../models'
+import { IDENTIFIABLE_FILTER_KEY } from '../../constants'
+import { isValidUid, parsePublicAccessString } from '../../models'
import { CustomDelimitedArrayParam } from './customParams'
const zodArrayIds = z.array(z.string().refine((val) => isValidUid(val)))
@@ -10,24 +10,28 @@ const zodArrayIds = z.array(z.string().refine((val) => isValidUid(val)))
/* Zod schema for validation of the decoded params */
export const filterParamsSchema = z
.object({
- [IDENTIFIABLE_KEY]: z.string(),
+ [IDENTIFIABLE_FILTER_KEY]: z.string(),
aggregationType: z.array(z.nativeEnum(DataElement.aggregationType)),
categoryCombo: zodArrayIds,
dataSet: zodArrayIds,
domainType: z.array(z.nativeEnum(DataElement.domainType)),
- valueType: z.array(z.string()),
+ publicAccess: z.array(
+ z.string().refine((val) => parsePublicAccessString(val) !== null)
+ ),
+ valueType: z.array(z.nativeEnum(DataElement.valueType)),
})
.partial()
/* useQueryParams config-map object
Mapping each filter to a config object that handles encoding/decoding */
export const filterQueryParamType = {
- [IDENTIFIABLE_KEY]: StringParam,
+ [IDENTIFIABLE_FILTER_KEY]: StringParam,
aggregationType: CustomDelimitedArrayParam,
domainType: CustomDelimitedArrayParam,
valueType: CustomDelimitedArrayParam,
dataSet: CustomDelimitedArrayParam,
categoryCombo: CustomDelimitedArrayParam,
+ publicAccess: CustomDelimitedArrayParam,
} as const satisfies QueryParamsConfigMap
export const validFilterKeys = Object.keys(filterQueryParamType)
@@ -49,5 +53,8 @@ type QueryParamsConfigMap = {
export type FilterKey = keyof ParsedFilterParams
// Identifiable is not configurable, and is always shown in the list
-export type ConfigurableFilterKey = Exclude
+export type ConfigurableFilterKey = Exclude<
+ FilterKey,
+ typeof IDENTIFIABLE_FILTER_KEY
+>
export type FilterKeys = FilterKey[]
diff --git a/src/lib/sectionList/filters/parseFiltersToQueryParams.ts b/src/lib/sectionList/filters/parseFiltersToQueryParams.ts
index c74d6189..a804e461 100644
--- a/src/lib/sectionList/filters/parseFiltersToQueryParams.ts
+++ b/src/lib/sectionList/filters/parseFiltersToQueryParams.ts
@@ -13,22 +13,26 @@ type FilterToQueryParamsMap = {
) => string
}
+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}`
+}
+
/* Override how to resolve the actual queryParam (when used in a request) for a filter */
const filterToQueryParamMap: FilterToQueryParamsMap = {
identifiable: (value) => `identifiable:token:${value}`,
+ categoryCombo: (value) => inFilter('categoryCombo.id', value),
dataSet: (value, section) =>
section.name === SchemaName.dataElement
- ? `dataSetElements.dataSet.id:in:[${value.join(',')}]`
+ ? inFilter('dataSetElements.dataSet.id', value)
: defaultFilter('dataSet', value),
- aggregationType: (value) => `aggregationType: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}`
+ publicAccess: (value) => inFilter('sharing.public', value),
}
const getQueryParamForFilter = (
@@ -60,6 +64,5 @@ export const parseFiltersToQueryParams = (
.filter(
(queryFilter): queryFilter is string => queryFilter !== undefined
)
-
return queryFilters
}
diff --git a/src/lib/sectionList/filters/useSectionListFilters.ts b/src/lib/sectionList/filters/useSectionListFilters.ts
index a0655c71..3e150da1 100644
--- a/src/lib/sectionList/filters/useSectionListFilters.ts
+++ b/src/lib/sectionList/filters/useSectionListFilters.ts
@@ -31,9 +31,10 @@ export const useSectionListFilters = () => {
const resetParams = Object.fromEntries(
validFilterKeys.map((key) => [key, undefined])
)
- return setFilterParams(resetParams)
+ setFilterParams(resetParams)
+ } else {
+ setFilterParams(filter, updateType)
}
- setFilterParams(filter, updateType)
// set page to 1 when filter changes
// do this here instead of useEffect to prevent unnecessary refetches
setPagingParams((pagingParams) => ({ ...pagingParams, page: 1 }))
diff --git a/src/lib/sectionList/index.ts b/src/lib/sectionList/index.ts
index ab050cdf..180856e0 100644
--- a/src/lib/sectionList/index.ts
+++ b/src/lib/sectionList/index.ts
@@ -5,3 +5,4 @@ export {
useUpdatePaginationParams,
} from './usePaginationParams'
export * from './useParamsForDataQuery'
+export * from './listViews'
diff --git a/src/lib/constants/sectionListView/index.ts b/src/lib/sectionList/listViews/index.ts
similarity index 100%
rename from src/lib/constants/sectionListView/index.ts
rename to src/lib/sectionList/listViews/index.ts
diff --git a/src/lib/constants/sectionListView/sectionListViewsConfig.ts b/src/lib/sectionList/listViews/sectionListViewsConfig.ts
similarity index 80%
rename from src/lib/constants/sectionListView/sectionListViewsConfig.ts
rename to src/lib/sectionList/listViews/sectionListViewsConfig.ts
index d9f56652..0a72a568 100644
--- a/src/lib/constants/sectionListView/sectionListViewsConfig.ts
+++ b/src/lib/sectionList/listViews/sectionListViewsConfig.ts
@@ -1,5 +1,5 @@
import i18n from '@dhis2/d2-i18n'
-import type { ConfigurableFilterKey } from '../../sectionList/filters/'
+import type { ConfigurableFilterKey } from '../filters'
export interface ModelPropertyDescriptor {
label: string
@@ -11,6 +11,10 @@ export interface FilterDescriptor {
filterKey: ConfigurableFilterKey
}
+type Descriptor =
+ | (ModelPropertyDescriptor & Partial)
+ | (FilterDescriptor & Partial)
+
/* Configs can either define the label and filterKey, or a string
If config is a string, getTranslatedProperty will be used to get the label. */
@@ -36,8 +40,12 @@ export type SectionListViewConfig = {
}
const DESCRIPTORS = {
- publicAccess: { path: 'sharing.public', label: i18n.t('Public access') },
-} satisfies Record
+ publicAccess: {
+ path: 'sharing.public',
+ label: i18n.t('Public access'),
+ filterKey: 'publicAccess',
+ },
+} satisfies Record
// This is the default views, and can be overriden per section in modelListViewsConfig below
export const defaultModelViewConfig = {
@@ -74,18 +82,19 @@ export const defaultModelViewConfig = {
export const modelListViewsConfig = {
dataElement: {
columns: {
- available: ['zeroIsSignificant', 'categoryCombo'],
+ available: ['zeroIsSignificant'],
default: [
'name',
- { label: i18n.t('Domain'), path: 'domainType' },
+ { label: i18n.t('Domain type'), path: 'domainType' },
{ label: i18n.t('Value type'), path: 'valueType' },
+ 'categoryCombo',
'lastUpdated',
DESCRIPTORS.publicAccess,
],
},
filters: {
- default: ['domainType', 'valueType'],
- available: ['categoryCombo'],
+ default: ['domainType', 'valueType', 'dataSet', 'categoryCombo'],
+ available: [DESCRIPTORS.publicAccess],
},
},
} satisfies SectionListViewConfig
diff --git a/src/lib/constants/sectionListView/viewConfigResolver.ts b/src/lib/sectionList/listViews/viewConfigResolver.ts
similarity index 92%
rename from src/lib/constants/sectionListView/viewConfigResolver.ts
rename to src/lib/sectionList/listViews/viewConfigResolver.ts
index c7434499..740d93ae 100644
--- a/src/lib/constants/sectionListView/viewConfigResolver.ts
+++ b/src/lib/sectionList/listViews/viewConfigResolver.ts
@@ -1,5 +1,5 @@
+import { getTranslatedProperty } from '../../constants/translatedModelProperties'
import { uniqueBy } from '../../utils'
-import { getTranslatedProperty } from '../translatedModelProperties'
import {
defaultModelViewConfig,
modelListViewsConfig,
@@ -23,7 +23,7 @@ interface ResolvedSectionListView {
[key: string]: ResolvedViewConfig
}
-const toModelPropertyDescriptor = (
+export const toModelPropertyDescriptor = (
propertyConfig: ModelPropertyConfig
): ModelPropertyDescriptor => {
if (typeof propertyConfig === 'string') {
@@ -35,7 +35,9 @@ const toModelPropertyDescriptor = (
return propertyConfig
}
-const toFilterDescriptor = (propertyConfig: FilterConfig): FilterDescriptor => {
+export const toFilterDescriptor = (
+ propertyConfig: FilterConfig
+): FilterDescriptor => {
if (typeof propertyConfig === 'string') {
return {
label: getTranslatedProperty(propertyConfig),
@@ -111,7 +113,7 @@ const resolveListViewsConfig = () => {
return merged
}
-const mergedModelViewsConfig = resolveListViewsConfig()
+const resolvedModelViewsConfig = resolveListViewsConfig()
const resolvedDefaultConfig = {
columns: resolveColumnConfig(defaultModelViewConfig.columns),
filters: resolveFilterConfig(defaultModelViewConfig.filters),
@@ -120,7 +122,7 @@ const resolvedDefaultConfig = {
export const getViewConfigForSection = (
sectionName: string
): ResolvedViewConfig => {
- const resolvedConfig = mergedModelViewsConfig[sectionName]
+ const resolvedConfig = resolvedModelViewsConfig[sectionName]
if (resolvedConfig) {
return resolvedConfig
}
diff --git a/src/pages/dataElements/List.spec.tsx b/src/pages/dataElements/List.spec.tsx
index 56da08a2..6d54386c 100644
--- a/src/pages/dataElements/List.spec.tsx
+++ b/src/pages/dataElements/List.spec.tsx
@@ -89,10 +89,11 @@ describe('Data Elements List', () => {
const { getByText } = await renderSection(customData)
const columns = [
'Name',
- 'Domain',
+ 'Domain type',
'Value type',
- 'Last updated',
+ 'Category combination',
'Public access',
+ 'Last updated',
'Actions',
]
diff --git a/src/pages/dataElements/List.tsx b/src/pages/dataElements/List.tsx
index fcaf2b3f..2226a255 100644
--- a/src/pages/dataElements/List.tsx
+++ b/src/pages/dataElements/List.tsx
@@ -1,28 +1,13 @@
import { useDataQuery } from '@dhis2/app-runtime'
import React, { useEffect } from 'react'
-import {
- SectionListWrapper,
- DomainTypeSelectionFilter,
- ValueTypeSelectionFilter,
-} from '../../components'
+import { SectionListWrapper } from '../../components'
import { useModelListView } from '../../components/sectionList/listView'
import { useSchemaFromHandle, useParamsForDataQuery } from '../../lib/'
import { getFieldFilter } from '../../lib/models/path'
import { Query, WrapQueryResponse } from '../../types'
import { DataElement, ModelCollectionResponse } from '../../types/models'
-const filterFields = [
- 'access',
- 'id',
- 'name',
- 'code',
- 'domainType',
- 'valueType',
- 'lastUpdated',
- 'sharing',
-] as const
-
-type FilteredDataElement = Pick
+type FilteredDataElement = Pick & Partial
type DataElements = ModelCollectionResponse
@@ -62,12 +47,6 @@ export const Component = () => {
return (
-
-
- >
- }
error={error}
data={data?.result.dataElements}
pager={data?.result.pager}
diff --git a/src/types/query.ts b/src/types/query.ts
index 033fa358..533dcf2e 100644
--- a/src/types/query.ts
+++ b/src/types/query.ts
@@ -4,8 +4,14 @@ export type QueryResponse = ReturnType
export type Query = Parameters[0]
+export type ResourceQuery = Query[keyof Query]
+
export type QueryRefetchFunction = QueryResponse['refetch']
export type WrapQueryResponse = {
[K in S]: T
}
+
+export type ResultQuery = {
+ result: ResourceQuery
+}