diff --git a/global.d.ts b/global.d.ts index 2672df9f..7e822cfd 100644 --- a/global.d.ts +++ b/global.d.ts @@ -2,6 +2,7 @@ declare module '@dhis2/d2-i18n' { export function t(key: string, options?: any): string + export function exists(key: string): boolean } declare module '@dhis2/ui' diff --git a/i18n/en.pot b/i18n/en.pot index dea9e1c3..2f287c06 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-06-21T21:00:56.973Z\n" -"PO-Revision-Date: 2023-06-21T21:00:56.973Z\n" +"POT-Creation-Date: 2023-08-19T21:39:42.855Z\n" +"PO-Revision-Date: 2023-08-19T21:39:42.855Z\n" msgid "schemas" msgstr "schemas" @@ -51,6 +51,63 @@ msgstr "Failed to load {{label}}" msgid "Failed to load" msgstr "Failed to load" +msgid "Actions" +msgstr "Actions" + +msgid "There aren't any items that match your filter." +msgstr "There aren't any items that match your filter." + +msgid "An error occurred" +msgstr "An error occurred" + +msgid "An error occurred while loading the items." +msgstr "An error occurred while loading the items." + +msgid "{{modelName}} management" +msgstr "{{modelName}} management" + +msgid "New" +msgstr "New" + +msgid "Download" +msgstr "Download" + +msgid "Manage Columns" +msgstr "Manage Columns" + +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 "Search by name, code or ID" +msgstr "Search by name, code or ID" + +msgid "Manage {{section}} table columns" +msgstr "Manage {{section}} table columns" + +msgid "Available table columns" +msgstr "Available table columns" + +msgid "Selected table columns" +msgstr "Selected table columns" + +msgid "Reset to default columns" +msgstr "Reset to default columns" + +msgid "Cancel" +msgstr "Cancel" + +msgid "Update table columns" +msgstr "Update table columns" + +msgid "None" +msgstr "None" + msgid "Category" msgstr "Category" @@ -288,9 +345,6 @@ msgstr "SQL views" msgid "Programs and Tracker" msgstr "Programs and Tracker" -msgid "Programs and Trackers" -msgstr "Programs and Trackers" - msgid "Validation" msgstr "Validation" @@ -300,15 +354,204 @@ msgstr "Validations" msgid "Other" msgstr "Other" -msgid "Others" -msgstr "Others" - msgid "Locale" msgstr "Locale" msgid "Locales" msgstr "Locales" +msgid "Sum" +msgstr "Sum" + +msgid "Average" +msgstr "Average" + +msgid "Average (sum in org unit)" +msgstr "Average (sum in org unit)" + +msgid "Last value (sum in org unit hierarchy)" +msgstr "Last value (sum in org unit hierarchy)" + +msgid "Last value (average in org unit)" +msgstr "Last value (average in org unit)" + +msgid "Last value (last in org unit hierarchy)" +msgstr "Last value (last in org unit hierarchy)" + +msgid "Last value in period (sum in org unit hierarchy)" +msgstr "Last value in period (sum in org unit hierarchy)" + +msgid "Lastinperiodaverageorgunit" +msgstr "Lastinperiodaverageorgunit" + +msgid "First value (sum in org unit hierarchy)" +msgstr "First value (sum in org unit hierarchy)" + +msgid "First value (average in org unit hierarchy)" +msgstr "First value (average in org unit hierarchy)" + +msgid "First value (first in org unit hierarchy)" +msgstr "First value (first in org unit hierarchy)" + +msgid "Count" +msgstr "Count" + +msgid "Standard deviation" +msgstr "Standard deviation" + +msgid "Variance" +msgstr "Variance" + +msgid "Min" +msgstr "Min" + +msgid "Max" +msgstr "Max" + +msgid "Min (sum in org unit)" +msgstr "Min (sum in org unit)" + +msgid "Max (sum in org unit)" +msgstr "Max (sum in org unit)" + +msgid "Custom" +msgstr "Custom" + +msgid "Default" +msgstr "Default" + +msgid "Aggregate" +msgstr "Aggregate" + +msgid "Tracker" +msgstr "Tracker" + +msgid "Text" +msgstr "Text" + +msgid "Long text" +msgstr "Long text" + +msgid "Text with multiple values" +msgstr "Text with multiple values" + +msgid "Letter" +msgstr "Letter" + +msgid "Phone number" +msgstr "Phone number" + +msgid "Email" +msgstr "Email" + +msgid "Yes/No" +msgstr "Yes/No" + +msgid "Yes only" +msgstr "Yes only" + +msgid "Date" +msgstr "Date" + +msgid "Date and time" +msgstr "Date and time" + +msgid "Time" +msgstr "Time" + +msgid "Number" +msgstr "Number" + +msgid "Unit interval" +msgstr "Unit interval" + +msgid "Percentage" +msgstr "Percentage" + +msgid "Integer" +msgstr "Integer" + +msgid "Positive integer" +msgstr "Positive integer" + +msgid "Negative integer" +msgstr "Negative integer" + +msgid "Positive or Zero integer" +msgstr "Positive or Zero integer" + +msgid "Tracker associate" +msgstr "Tracker associate" + +msgid "Username" +msgstr "Username" + +msgid "Coordinate" +msgstr "Coordinate" + +msgid "Reference" +msgstr "Reference" + +msgid "Age" +msgstr "Age" + +msgid "URL" +msgstr "URL" + +msgid "File" +msgstr "File" + +msgid "Image" +msgstr "Image" + +msgid "GeoJSON" +msgstr "GeoJSON" + +msgid "Code" +msgstr "Code" + +msgid "Created by" +msgstr "Created by" + +msgid "Favorite" +msgstr "Favorite" + +msgid "Href" +msgstr "Href" + +msgid "Id" +msgstr "Id" + +msgid "Last updated by" +msgstr "Last updated by" + +msgid "Created" +msgstr "Created" + +msgid "Domain type" +msgstr "Domain type" + +msgid "Last updated" +msgstr "Last updated" + +msgid "Name" +msgstr "Name" + +msgid "Sharing" +msgstr "Sharing" + +msgid "Short name" +msgstr "Short name" + +msgid "Value type" +msgstr "Value type" + +msgid "Owner" +msgstr "Owner" + +msgid "Zero is significant" +msgstr "Zero is significant" + msgid "Metadata management" msgstr "Metadata management" diff --git a/src/components/sectionList/SelectionListHeaderNormal.tsx b/src/components/sectionList/SelectionListHeaderNormal.tsx index e6e71e93..9f000f1e 100644 --- a/src/components/sectionList/SelectionListHeaderNormal.tsx +++ b/src/components/sectionList/SelectionListHeaderNormal.tsx @@ -4,7 +4,7 @@ import { IconAdd24 } from '@dhis2/ui-icons' import React from 'react' import { Link } from 'react-router-dom' import { routePaths } from '../../app/routes/routePaths' -import { ManageColumnsDialog } from './manageColumns/ManageColumns' +import { ManageColumnsDialog } from './listView/ManageColumns' import css from './SectionList.module.css' export const SelectionListHeader = () => { diff --git a/src/components/sectionList/manageColumns/ManageColumns.module.css b/src/components/sectionList/listView/ManageColumns.module.css similarity index 100% rename from src/components/sectionList/manageColumns/ManageColumns.module.css rename to src/components/sectionList/listView/ManageColumns.module.css diff --git a/src/components/sectionList/manageColumns/ManageColumns.tsx b/src/components/sectionList/listView/ManageColumns.tsx similarity index 96% rename from src/components/sectionList/manageColumns/ManageColumns.tsx rename to src/components/sectionList/listView/ManageColumns.tsx index 3de38ea1..9ca353bf 100644 --- a/src/components/sectionList/manageColumns/ManageColumns.tsx +++ b/src/components/sectionList/listView/ManageColumns.tsx @@ -9,8 +9,8 @@ import { Transfer, } from '@dhis2/ui' import React, { useEffect, useMemo, useRef, useState } from 'react' -import { getColumnsForSection, getTranslatedProperty } from '../../../constants' -import { mergeArraysUnique, useModelSectionHandleOrThrow } from '../../../lib' +import { getTranslatedProperty, getColumnsForSection } from '../../../constants' +import { useModelSectionHandleOrThrow } from '../../../lib' import css from './ManageColumns.module.css' import { useMutateSelectedColumns, @@ -32,6 +32,7 @@ export const ManageColumnsDialog = ({ onClose }: ManageColumnsDialogProps) => { const { saveColumns, mutation } = useMutateSelectedColumns() const columnsConfig = getColumnsForSection(section.name) + columnsConfig.available useEffect(() => { // if savedColumns were to update while selecting (it shouldn't ) diff --git a/src/components/sectionList/listView/ManageListView.tsx b/src/components/sectionList/listView/ManageListView.tsx new file mode 100644 index 00000000..ebc82933 --- /dev/null +++ b/src/components/sectionList/listView/ManageListView.tsx @@ -0,0 +1,128 @@ +import i18n from '@dhis2/d2-i18n' +import { + Modal, + ModalActions, + ModalContent, + ModalTitle, + Button, + ButtonStrip, + Transfer, +} from '@dhis2/ui' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { getColumnsForSection, getTranslatedProperty } from '../../../constants' +import { useModelSectionHandleOrThrow } from '../../../lib' +import css from './ManageColumns.module.css' +import { + useMutateSelectedColumns, + useSelectedColumns, +} from './useSelectedColumns' + +type ManageColumnsDialogProps = { + onClose: () => void +} +export const ManageColumnsDialog = ({ onClose }: ManageColumnsDialogProps) => { + const section = useModelSectionHandleOrThrow() + const [pendingSelectedColumns, setPendingSelectedColumns] = useState< + string[] + >([]) + // ignore updates to saved-columns while selecting + const isTouched = useRef(false) + + const { columns: savedColumns, query } = useSelectedColumns() + const { saveColumns, mutation } = useMutateSelectedColumns() + + const columnsConfig = getColumnsForSection(section.name) + console.log({ columnsConfig }) + useEffect(() => { + // if savedColumns were to update while selecting (it shouldn't ) + // make sure to not overwrite the selected columns + if (isTouched.current) { + return + } + setPendingSelectedColumns(savedColumns) + }, [savedColumns]) + + const handleSave = () => { + saveColumns(pendingSelectedColumns, { + onSuccess: () => onClose(), + }) + } + + const handleSetDefault = () => { + setPendingSelectedColumns(columnsConfig.default) + } + + const handleChange = ({ selected }: { selected: string[] }) => { + isTouched.current = true + setPendingSelectedColumns(selected) + } + + const transferOptions = useMemo( + () => + columnsConfig.available + .map((column) => ({ + label: getTranslatedProperty(column), + value: column, + })) + .sort((a, b) => a.label.localeCompare(b.label)), + [columnsConfig.available] + ) + + return ( + + + {i18n.t('Manage {{section}} table columns', { + section: section.title, + })} + + + + {i18n.t('Available table columns')} + + } + rightHeader={ + + {i18n.t('Selected table columns')} + + } + onChange={handleChange} + loading={query.isLoading} + loadingPicked={query.isLoading} + options={transferOptions} + selected={pendingSelectedColumns} + /> + + + + + + + + + + ) +} + +const TransferHeader = ({ children }: React.PropsWithChildren) => ( +
{children}
+) diff --git a/src/components/sectionList/manageColumns/index.ts b/src/components/sectionList/listView/index.ts similarity index 100% rename from src/components/sectionList/manageColumns/index.ts rename to src/components/sectionList/listView/index.ts diff --git a/src/components/sectionList/listView/types.ts b/src/components/sectionList/listView/types.ts new file mode 100644 index 00000000..3d4147df --- /dev/null +++ b/src/components/sectionList/listView/types.ts @@ -0,0 +1,22 @@ +import { SectionName } from '../../../constants' + +export interface ModelListView { + name: string + sectionModel: SectionName + columns: Array + filters: Array +} + +export type ModelListViews = { + [key in SectionName]?: ModelListView +} + +interface ModelListViewColumn { + label: string + path: string +} + +interface ModelListViewFilter { + name: string + path: string +} diff --git a/src/components/sectionList/manageColumns/useSelectedColumns.tsx b/src/components/sectionList/listView/useSelectedColumns.tsx similarity index 98% rename from src/components/sectionList/manageColumns/useSelectedColumns.tsx rename to src/components/sectionList/listView/useSelectedColumns.tsx index 390f0d45..ed38286e 100644 --- a/src/components/sectionList/manageColumns/useSelectedColumns.tsx +++ b/src/components/sectionList/listView/useSelectedColumns.tsx @@ -18,7 +18,7 @@ type ColumnsResult = Record< > const maintenanceNamespace = 'maintenance' -const configurableColumnsKey = 'configurableColumnsTest' +const configurableColumnsKey = 'modelListViews' // check that columns are valid - because data in dataStore should not // be trusted - since there's no validation server-side. diff --git a/src/components/sectionList/useHeaderColumns.ts b/src/components/sectionList/useHeaderColumns.ts index 14cd0697..1557b6eb 100644 --- a/src/components/sectionList/useHeaderColumns.ts +++ b/src/components/sectionList/useHeaderColumns.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { getTranslatedProperty } from '../../constants' -import { useSelectedColumns } from './manageColumns/useSelectedColumns' +import { useSelectedColumns } from './listView/useSelectedColumns' import type { SelectedColumn } from './types' export const useHeaderColumns = () => { diff --git a/src/constants/index.ts b/src/constants/index.ts index 47214623..a202fbe6 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,3 @@ export * from './sections' export * from './translatedModelProperties' -export * from './sectionListColumns' +export * from './sectionListViews' diff --git a/src/constants/sectionListColumns.ts b/src/constants/sectionListColumns.ts deleted file mode 100644 index 3d2fa594..00000000 --- a/src/constants/sectionListColumns.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { SECTIONS_MAP, SectionName } from './sections' - -interface ColumnConfig { - available?: string[] - overrideDefaultAvailable?: boolean - default: string[] -} - -interface MergedColumnConfig { - available: string[] - default: string[] -} - -type ColumnsForSection = { - [key in SectionName]?: ColumnConfig -} - -type MergedColumnsForSection = Record - -// list of modelProperties to show in List -// this is the default that all columns that are not specified -// in columnsForSection below -const columnsDefault = { - available: [ - 'name', - 'shortName', - 'code', - 'created', - 'createdBy', - 'href', - 'id', - 'lastUpdatedBy', - ], - default: ['name', 'sharing', 'lastUpdated'], -} satisfies ColumnConfig - -// this is the default columns shown in the list for each section -// Note: by default, the available columns are merged with columnsDefault.available above. -// If it's needed to override this for a section, set overrideDefaultAvailable to true -// and list all available columns in the available array below. -// Default-list will NOT be merged with columnsDefault.default - list all explicitly. -// elements in the default array implies they are also available, no need to list them in both. -const columnsForSection: ColumnsForSection = { - dataElement: { - default: ['name', 'domainType', 'valueType', 'lastUpdated', 'sharing'], - available: ['zeroIsSignificant', 'categoryCombo'], - }, -} - -const mergeArraysUnique = (...arrays: T[][]): T[] => - Array.from(new Set(arrays.flat())) - -const mergeColumnsConfig = () => { - const merged: MergedColumnsForSection = {} - - Object.entries(columnsForSection).forEach( - ([sectionName, sectionColumnsMeta]) => { - const mergedAvailable = mergeArraysUnique( - sectionColumnsMeta.default, - sectionColumnsMeta.available || [], - sectionColumnsMeta.overrideDefaultAvailable - ? [] - : columnsDefault.available - ) - - merged[sectionName] = { - available: mergedAvailable, - default: sectionColumnsMeta.default - ? sectionColumnsMeta.default - : columnsDefault.default, - } - } - ) - return merged -} - -const mergedColumns = mergeColumnsConfig() - -export const getColumnsForSection = ( - sectionName: string -): MergedColumnConfig => { - if (mergedColumns[sectionName]) { - return mergedColumns[sectionName] - } - return columnsDefault -} diff --git a/src/constants/sectionListViews.ts b/src/constants/sectionListViews.ts new file mode 100644 index 00000000..51ec33dd --- /dev/null +++ b/src/constants/sectionListViews.ts @@ -0,0 +1,119 @@ +import { uniq } from 'remeda' +import { SectionName } from './sections' + +interface ViewConfigPart { + available?: string[] + overrideDefaultAvailable?: boolean + default: string[] +} + +type ViewConfig = { + columns: ViewConfigPart + filters: ViewConfigPart +} + +type ResolvedViewConfigPart = { + available: string[] + default: string[] +} +type ResolvedViewConfig = { + columns: ResolvedViewConfigPart + filters: ResolvedViewConfigPart +} + +// generic here is just used for "satisfies" below, for code-completion of future customizations +type SectionListViewConfig = { + [key in Key]?: ResolvedViewConfig +} + +// This is the default views, and can be overriden per section in modelListViewsConfig below +const defaultModelViewConfig = { + columns: { + available: [ + 'name', + 'shortName', + 'code', + 'created', + 'createdBy', + 'href', + 'id', + 'lastUpdatedBy', + ], + default: ['name', 'sharing', 'lastUpdated'], + }, + filters: { + available: ['name'], + default: ['name'], + }, +} satisfies ViewConfig + +// this is the default views (eg. which columns and filters) to show in the List-page for each section +// Note: by default, the available columns are merged with columnsDefault.available above. +// If it's needed to override this for a section, set overrideDefaultAvailable to true +// and list all available columns in the available array below. +// Default-list will NOT be merged with columnsDefault.default - list all explicitly. +// elements in the default array implies they are also available, no need to list them in both. + +const modelListViewsConfig = { + dataElement: { + columns: { + default: [ + 'name', + 'domainType', + 'valueType', + 'lastUpdated', + 'sharing', + ], + available: ['zeroIsSignificant', 'categoryCombo'], + }, + filters: { + default: ['name', 'domainType', 'valueType'], + available: ['zeroIsSignificant', 'categoryCombo'], + }, + }, +} satisfies SectionListViewConfig + +const mergeArraysUnique = (...arrays: T[][]): T[] => uniq(arrays.flat()) + +const resolveViewPart = (part: ViewConfigPart, type: keyof ViewConfig) => { + const mergedAvailable = mergeArraysUnique( + part.default, + part.available || [], + part.overrideDefaultAvailable + ? [] + : defaultModelViewConfig[type].available + ) + return { + available: mergedAvailable, + default: part.default || defaultModelViewConfig[type].default, + } +} +// merge the default modelViewConfig with the modelViewsConfig for each section +const resolveListViewsConfig = () => { + const merged: SectionListViewConfig = {} + + Object.entries(modelListViewsConfig).forEach((viewConfig) => { + const [sectionName, sectionViewConfig] = viewConfig + merged[sectionName as SectionName] = { + columns: resolveViewPart(sectionViewConfig.columns, 'columns'), + filters: resolveViewPart(sectionViewConfig.filters, 'filters'), + } + }) + return merged +} + +const mergedModelViewsConfig = resolveListViewsConfig() + +export const getViewForSection = (sectionName: string): ResolvedViewConfig => { + if (mergedModelViewsConfig[sectionName]) { + return mergedModelViewsConfig[sectionName] as ResolvedViewConfig + } + return defaultModelViewConfig +} + +export const getColumnsForSection = ( + sectionName: string +): ResolvedViewConfig['columns'] => { + const view = getViewForSection(sectionName) + return view.columns +} diff --git a/src/lib/index.ts b/src/lib/index.ts index f73e46d2..31d48b26 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -8,4 +8,3 @@ export * from './sections' export * from './useDebounce' export * from './routeUtils' export * from './systemSettings' -export * from './utils' diff --git a/src/lib/user/index.ts b/src/lib/user/index.ts index 98f4e126..fd647932 100644 --- a/src/lib/user/index.ts +++ b/src/lib/user/index.ts @@ -4,3 +4,4 @@ export { useCurrentUserAuthorities, } from './currentUserStore' export * from './authorities' +// asf diff --git a/src/pages/dataElements/List.tsx b/src/pages/dataElements/List.tsx index 2b515659..8111a6fd 100644 --- a/src/pages/dataElements/List.tsx +++ b/src/pages/dataElements/List.tsx @@ -6,7 +6,7 @@ import { useQueryParamsForModelGist, useSectionListParamsRefetch, } from '../../components' -import { useSelectedColumns } from '../../components/sectionList/manageColumns/useSelectedColumns' +import { useSelectedColumns } from '../../components/sectionList/listView/useSelectedColumns' import { useModelGist } from '../../lib/' import { DataElement, GistCollectionResponse } from '../../types/models'