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 ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + ({ + label: c.label, + value: c.path, + }))} + /> + ({ + label: f.label, + value: f.filterKey, + }))} + /> + {submitError && ( +

+ + {submitError} + +

+ )} + {children({ + submitting, + })} + + )} + + ) +} +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 +}