diff --git a/i18n/en.pot b/i18n/en.pot index 9866e147..acd09333 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-10-23T11:07:53.225Z\n" -"PO-Revision-Date: 2024-10-23T11:07:53.225Z\n" +"POT-Creation-Date: 2024-10-31T16:21:47.811Z\n" +"PO-Revision-Date: 2024-10-31T16:21:47.811Z\n" msgid "schemas" msgstr "schemas" @@ -123,21 +123,6 @@ msgstr "Failed to load {{label}}" msgid "Failed to load" msgstr "Failed to load" -msgid "Download" -msgstr "Download" - -msgid "Merge" -msgstr "Merge" - -msgid "Delete source data element values" -msgstr "Delete source data element values" - -msgid "Last updated" -msgstr "Last updated" - -msgid "Discard" -msgstr "Discard" - msgid "Aggregation level(s)" msgstr "Aggregation level(s)" @@ -237,6 +222,9 @@ msgstr "Created" msgid "Last updated by" msgstr "Last updated by" +msgid "Last updated" +msgstr "Last updated" + msgid "Id" msgstr "Id" @@ -258,6 +246,9 @@ msgstr "Details" msgid "Failed to load details" msgstr "Failed to load details" +msgid "Download" +msgstr "Download" + msgid "Download {{section}}" msgstr "Download {{section}}" @@ -297,6 +288,9 @@ msgstr "Clear all filters" msgid "Category" msgstr "Category" +msgid "Category option" +msgstr "Category option" + msgid "Category option group" msgstr "Category option group" @@ -435,9 +429,6 @@ msgstr "Search for a user or group" msgid "Categories" msgstr "Categories" -msgid "Category option" -msgstr "Category option" - msgid "Category options" msgstr "Category options" @@ -855,9 +846,15 @@ msgstr "Zero is significant" msgid "Data dimension type" msgstr "Data dimension type" +msgid "Ignore data approval" +msgstr "Ignore data approval" + msgid "This field requires a unique value, please choose another one" msgstr "This field requires a unique value, please choose another one" +msgid "{{label}} (required)" +msgstr "{{label}} (required)" + msgid "No changes to be saved" msgstr "No changes to be saved" @@ -879,23 +876,27 @@ msgstr "Basic information" msgid "Set up the basic information for this category." msgstr "Set up the basic information for this category." -msgid "Explain the purpose of this category." -msgstr "Explain the purpose of this category." +msgid "Explain the purpose of this category option group." +msgstr "Explain the purpose of this category option group." msgid "Data configuration" msgstr "Data configuration" -msgid "Choose how this category will be used to capture and analyze" -msgstr "Choose how this category will be used to capture and analyze" +msgid "Choose how this category option group will be used to capture and analyze" +msgstr "Choose how this category option group will be used to capture and analyze" msgid "Use as data dimension" msgstr "Use as data dimension" -msgid "Category will be available to the analytics as another dimension" -msgstr "Category will be available to the analytics as another dimension" +msgid "" +"Category option group will be available to the analytics as another " +"dimension" +msgstr "" +"Category option group will be available to the analytics as another " +"dimension" -msgid "Choose the category options to include in this category." -msgstr "Choose the category options to include in this category." +msgid "Choose the category options to include in this category option group." +msgstr "Choose the category options to include in this category option group." msgid "Available category options" msgstr "Available category options" @@ -933,6 +934,15 @@ msgstr "Filter selected categories" msgid "At least one category is required" msgstr "At least one category is required" +msgid "Set up the basic information for this category option group." +msgstr "Set up the basic information for this category option group." + +msgid "Choose how this category option will be used to capture and analyze" +msgstr "Choose how this category option will be used to capture and analyze" + +msgid "Choose the category options to include in this category." +msgstr "Choose the category options to include in this category." + msgid "Set up the basic information for this category option." msgstr "Set up the basic information for this category option." @@ -1136,11 +1146,11 @@ msgstr "Latitude" msgid "Longitude" msgstr "Longitude" -msgid "Reference assignments" -msgstr "Reference assignments" +msgid "Reference assignment" +msgstr "Reference assignment" -msgid "Assign the organisation unit to related models." -msgstr "Assign the organisation unit to related models." +msgid "Assign the organisation unit to related objects." +msgstr "Assign the organisation unit to related objects." msgid "Available data sets" msgstr "Available data sets" diff --git a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx index ce165dbf..02024f32 100644 --- a/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx +++ b/src/components/SearchableSingleSelect/SearchableSingleSelect.tsx @@ -47,14 +47,14 @@ function Error({ type OnChange = ({ selected }: { selected: string }) => void type OnFilterChange = ({ value }: { value: string }) => void -interface SearchableSingleSelectPropTypes { +export interface SearchableSingleSelectPropTypes { onChange: OnChange onFilterChange: OnFilterChange onEndReached?: () => void onRetryClick: () => void dense?: boolean options: Option[] - placeholder: string + placeholder?: string prefix?: string showEndLoader: boolean loading: boolean diff --git a/src/components/metadataFormControls/ModelSingleSelect/BaseModelSingleSelect.tsx b/src/components/metadataFormControls/ModelSingleSelect/BaseModelSingleSelect.tsx new file mode 100644 index 00000000..c35425be --- /dev/null +++ b/src/components/metadataFormControls/ModelSingleSelect/BaseModelSingleSelect.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useMemo } from 'react' +import { DisplayableModel } from '../../../types/models' +import { + SearchableSingleSelect, + SearchableSingleSelectPropTypes, +} from '../../SearchableSingleSelect' + +const toDisplayOption = (model: DisplayableModel) => ({ + value: model.id, + label: model.displayName, +}) + +type OwnProps = { + selected: TModel | undefined + available: TModel[] + onChange: ({ selected }: { selected: TModel | undefined }) => void +} + +export type BaseModelSingleSelectProps = Omit< + SearchableSingleSelectPropTypes, + keyof OwnProps | 'options' | 'selected' +> & + OwnProps + +/* Simple wrapper component handle generic models with SingleSelect-component. */ +export const BaseModelSingleSelect = ({ + available, + selected, + onChange, + ...searchableSingleSelectProps +}: BaseModelSingleSelectProps) => { + const { allModelsMap, allSingleSelectOptions } = useMemo(() => { + const allModels = selected ? [selected].concat(available) : available + const allModelsMap = new Map(allModels.map((o) => [o.id, o])) + const allSingleSelectOptions = allModels.map(toDisplayOption) + + return { + allModelsMap, + allSingleSelectOptions, + } + }, [available, selected]) + + const handleOnChange: SearchableSingleSelectPropTypes['onChange'] = + useCallback( + ({ selected }) => { + // map the selected ids to the full model + const fullSelectedModel = allModelsMap.get(selected) + onChange({ + selected: fullSelectedModel, + }) + }, + [onChange, allModelsMap] + ) + + return ( + + ) +} diff --git a/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectField.tsx b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectField.tsx new file mode 100644 index 00000000..2aaec03c --- /dev/null +++ b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectField.tsx @@ -0,0 +1,53 @@ +import { Field } from '@dhis2/ui' +import React from 'react' +import { useField } from 'react-final-form' +import { PlainResourceQuery } from '../../../types' +import { DisplayableModel } from '../../../types/models' +import { + ModelSingleSelectProps, + ModelSingleSelect, +} from './ModelSingleSelectRefactor' + +// this currently does not need a generic, because the value of the field is not passed +// or available from props. However if it's made available, +// the generic of should be added. +type ModelSingleSelectFieldProps = { + name: string + query: PlainResourceQuery + label?: string + placeholder?: string + helpText?: string +} & ModelSingleSelectProps + +export function ModelSingleSelectField({ + name, + query, + label, + helpText, + ...modelSingleSelectProps +}: ModelSingleSelectFieldProps) { + const { input, meta } = useField(name, { + validateFields: [], + }) + + return ( + + { + input.onChange(selected) + input.onBlur() + }} + query={query} + /> + + ) +} diff --git a/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectRefactor.tsx b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectRefactor.tsx new file mode 100644 index 00000000..52c13eb1 --- /dev/null +++ b/src/components/metadataFormControls/ModelSingleSelect/ModelSingleSelectRefactor.tsx @@ -0,0 +1,90 @@ +import React, { useMemo, useState } from 'react' +import { useInfiniteQuery } from 'react-query' +import { useDebouncedCallback } from 'use-debounce' +import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn' +import { PlainResourceQuery } from '../../../types' +import { PagedResponse } from '../../../types/generated' +import { DisplayableModel } from '../../../types/models' +import { + BaseModelSingleSelect, + BaseModelSingleSelectProps, +} from './BaseModelSingleSelect' + +type Response = PagedResponse + +const defaultQuery = { + params: { + order: 'displayName:asc', + fields: ['id', 'displayName'], + }, +} + +export type ModelSingleSelectProps = Omit< + BaseModelSingleSelectProps, + | 'available' + | 'onFilterChange' + | 'onRetryClick' + | 'showEndLoader' + | 'loading' + | 'error' +> & { + query: Omit + onFilterChange?: (value: string) => void +} + +export const ModelSingleSelect = ({ + selected, + query, + ...baseModelSingleSelectProps +}: ModelSingleSelectProps) => { + const queryFn = useBoundResourceQueryFn() + const [searchTerm, setSearchTerm] = useState('') + + const searchFilter = `identifiable:token:${searchTerm}` + const filter: string[] = searchTerm ? [searchFilter] : [] + const params = query.params + + const queryObject = { + ...query, + params: { + ...defaultQuery.params, + ...params, + filter: filter.concat(params?.filter || []), + }, + } + const modelName = query.resource + + const queryResult = useInfiniteQuery({ + queryKey: [queryObject] as const, + queryFn: queryFn>, + keepPreviousData: true, + getNextPageParam: (lastPage) => + lastPage.pager.nextPage ? lastPage.pager.page + 1 : undefined, + getPreviousPageParam: (firstPage) => + firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined, + }) + const allDataMap = useMemo( + () => queryResult.data?.pages.flatMap((page) => page[modelName]) ?? [], + [queryResult.data, modelName] + ) + + const handleFilterChange = useDebouncedCallback(({ value }) => { + if (value != undefined) { + setSearchTerm(value) + } + baseModelSingleSelectProps.onFilterChange?.(value) + }, 250) + + return ( + + ) +} diff --git a/src/components/metadataFormControls/ModelSingleSelect/useRefreshSingleSelect.tsx b/src/components/metadataFormControls/ModelSingleSelect/useRefreshSingleSelect.tsx new file mode 100644 index 00000000..ce1c998d --- /dev/null +++ b/src/components/metadataFormControls/ModelSingleSelect/useRefreshSingleSelect.tsx @@ -0,0 +1,16 @@ +import { useCallback } from 'react' +import { useQueryClient, InvalidateQueryFilters } from 'react-query' +import { PlainResourceQuery } from '../../../types' + +export const useRefreshModelSingleSelect = ( + query: Omit +) => { + const queryClient = useQueryClient() + + return useCallback( + (invalidateFilters?: InvalidateQueryFilters) => { + queryClient.invalidateQueries([query], invalidateFilters) + }, + [queryClient, query] + ) +}