Skip to content

Commit

Permalink
feat(filters): rework config for better type safety for dynamic filters
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Jan 15, 2024
1 parent 22ecd56 commit c132fd6
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 14 deletions.
Empty file.
Empty file.
31 changes: 25 additions & 6 deletions src/components/sectionList/filters/useSectionListFilter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { useCallback, useMemo } from 'react'
import { useQueryParam, ObjectParam, UrlUpdateType } from 'use-query-params'
import { Schema, useSchemaFromHandle, CustomObjectParam } from '../../../lib'
import {
Schema,
useSchemaFromHandle,
CustomObjectParam,
SectionListFilterObjectParamType,
getViewConfigForSection,
IDENTIFIABLE_KEY,
} from '../../../lib'
import { usePaginationQueryParams } from '../SectionListPagination'

type ObjectParamType = typeof ObjectParam.default

type Filters = Record<string, string | undefined>

// special key for handling search for identifiable objects
// eg. searches for name, code, id and shortname
// this would translate to "token" in the old API, but does not exist in GIST-API
export const IDENTIFIABLE_KEY = 'identifiable'

const getVerifiedFiltersForSchema = (
filters: ObjectParamType,
schema: Schema
Expand All @@ -26,6 +28,23 @@ const getVerifiedFiltersForSchema = (
return Object.fromEntries(relevantFilters)
}

const getRelevantFiltersForSchema = (

Check warning on line 31 in src/components/sectionList/filters/useSectionListFilter.ts

View workflow job for this annotation

GitHub Actions / lint

'getRelevantFiltersForSchema' is assigned a value but never used
filters: SectionListFilterObjectParamType,
schema: Schema
): Filters => {
if (!filters) {
return {}
}
const viewConfig = getViewConfigForSection(schema.singular)
const relevantFilterKeys =

Check warning on line 39 in src/components/sectionList/filters/useSectionListFilter.ts

View workflow job for this annotation

GitHub Actions / lint

'relevantFilterKeys' is assigned a value but never used
viewConfig.filters.available.concat('identifiable')
/* TODO: verify values for filters */
const relevantFilters = Object.entries(filters).filter(([key]) => {
return key === IDENTIFIABLE_KEY || schema.properties[key]
})
return Object.fromEntries(relevantFilters)
}

const useFilterQueryParam = () => {
return useQueryParam('filter', CustomObjectParam)
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './sectionListViewsConfig'
export * from './sections'
export * from './translatedModelConstants'
export * from './translatedModelProperties'
export * from './sectionListView'

Check failure on line 5 in src/lib/constants/index.ts

View workflow job for this annotation

GitHub Actions / lint

Multiple exports of name 'getViewConfigForSection'

Check failure on line 5 in src/lib/constants/index.ts

View workflow job for this annotation

GitHub Actions / lint

Multiple exports of name 'getColumnsForSection'
3 changes: 3 additions & 0 deletions src/lib/constants/sectionListView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './viewConfigResolver'
export * from './sectionListViewFilterKeys'
// export * from './sectionListViewsConfig'
25 changes: 25 additions & 0 deletions src/lib/constants/sectionListView/sectionListViewFilterKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// special key for handling search for identifiable objects
// eg. searches for name, code, id and shortname
// this would translate to "token" in the old API, but does not exist in GIST-API
export const IDENTIFIABLE_KEY = 'identifiable'

/* Allowed "keys" to filter by
Used to specify the allowed filters in the query-Params as well
as mapping to the correct filter component */
export const validFilterKeys = [
IDENTIFIABLE_KEY,
'domainType',
'valueType',
'dataSet',
'categoryCombo',
] as const

const filterKeysSet = new Set(validFilterKeys)

export type FilterKeys = typeof validFilterKeys
//export const filterKeys = Object.keys(filterKeys)

export type FilterKey = FilterKeys[number]

// Identifiable is not configurable, and is always shown in the list
export type ConfigurableFilterKey = Exclude<FilterKey, typeof IDENTIFIABLE_KEY>
91 changes: 91 additions & 0 deletions src/lib/constants/sectionListView/sectionListViewsConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import i18n from '@dhis2/d2-i18n'
import type { ConfigurableFilterKey } from './sectionListViewFilterKeys'

export interface ModelPropertyDescriptor {
label: string
path: string
}

export interface FilterDescriptor {
label: string
filterKey: ConfigurableFilterKey
}

/* Configs can either define the label and filterKey, or a string
If config is a string, getTranslatedProperty will be used to get the label. */

export type FilterConfig = ConfigurableFilterKey | FilterDescriptor

export type ModelPropertyConfig = string | ModelPropertyDescriptor

export interface ViewConfigPart<TEntry> {
available?: ReadonlyArray<TEntry>
overrideDefaultAvailable?: boolean
default?: ReadonlyArray<TEntry>
}

export interface ViewConfig {
columns: ViewConfigPart<ModelPropertyConfig>
filters: ViewConfigPart<FilterConfig>
}

// generic here is just used for "satisfies" below, for code-completion of future customizations
// cant use "[key in SectionName]" here, because section.name might be a "string"
export type SectionListViewConfig<Key extends string = string> = {
[key in Key]?: ViewConfig
}

const DESCRIPTORS = {
publicAccess: { path: 'sharing.public', label: i18n.t('Public access') },
} satisfies Record<string, ModelPropertyDescriptor>

// This is the default views, and can be overriden per section in modelListViewsConfig below
export const defaultModelViewConfig = {
columns: {
available: [
'name',
'shortName',
'code',
'created',
'createdBy',
'href',
'id',
'lastUpdatedBy',
DESCRIPTORS.publicAccess,
],
default: ['name', DESCRIPTORS.publicAccess, 'lastUpdated'],
},
filters: {
available: [],
default: [
// NOTE: Identifiable is special, and is always included in the default filters
// It should not be handled the same way as "configurable" filters
],
},
} 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. */

export const modelListViewsConfig = {
dataElement: {
columns: {
available: ['zeroIsSignificant', 'categoryCombo'],
default: [
'name',
{ label: i18n.t('Domain'), path: 'domainType' },
{ label: i18n.t('Value type'), path: 'valueType' },
'lastUpdated',
'sharing.public',
],
},
filters: {
default: ['domainType', 'valueType'],
available: ['categoryCombo'],
},
},
} satisfies SectionListViewConfig
142 changes: 142 additions & 0 deletions src/lib/constants/sectionListView/viewConfigResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { uniqueBy } from '../../utils'
import { getTranslatedProperty } from '../translatedModelProperties'
import {
defaultModelViewConfig,
modelListViewsConfig,
ViewConfigPart,
ModelPropertyConfig,
ModelPropertyDescriptor,
FilterDescriptor,
FilterConfig,
} from './sectionListViewsConfig'

interface ResolvedViewConfigPart<TEntry> {
available: ReadonlyArray<TEntry>
default: ReadonlyArray<TEntry>
}
interface ResolvedViewConfig {
columns: ResolvedViewConfigPart<ModelPropertyDescriptor>
filters: ResolvedViewConfigPart<FilterDescriptor>
}

interface ResolvedSectionListView {
[key: string]: ResolvedViewConfig
}

const toModelPropertyDescriptor = (
propertyConfig: ModelPropertyConfig
): ModelPropertyDescriptor => {
if (typeof propertyConfig === 'string') {
return {
label: getTranslatedProperty(propertyConfig),
path: propertyConfig,
}
}
return propertyConfig
}

const toFilterDescriptor = (propertyConfig: FilterConfig): FilterDescriptor => {
if (typeof propertyConfig === 'string') {
return {
label: getTranslatedProperty(propertyConfig),
filterKey: propertyConfig,
}
}
return propertyConfig
}

const resolveFilterConfig = (
part: ViewConfigPart<FilterConfig>
): ResolvedViewConfigPart<FilterDescriptor> => {
const { default: defaultFilters, available: defaultAvailableFilers } =
defaultModelViewConfig.filters

const mergedAvailableDescriptors = uniqueBy(
[
part.available || [],
part.overrideDefaultAvailable ? [] : defaultAvailableFilers || [],
part.default || [],
]
.flat()
.map((propConfig) => toFilterDescriptor(propConfig)),
(prop) => prop.filterKey
)
const defaultPropConfig = part.default || defaultFilters || []
const defaultDescriptors = defaultPropConfig.map((propConfig) =>
toFilterDescriptor(propConfig)
)
return {
available: mergedAvailableDescriptors,
default: defaultDescriptors,
}
}

const resolveColumnConfig = (
part: ViewConfigPart<ModelPropertyConfig>
): ResolvedViewConfigPart<ModelPropertyDescriptor> => {
const { default: defaultFilters, available: defaultAvailableFilers } =
defaultModelViewConfig.columns

const mergedAvailableDescriptors = uniqueBy(
[
part.available || [],
part.overrideDefaultAvailable ? [] : defaultAvailableFilers || [],
part.default || [],
]
.flat()
.map((propConfig) => toModelPropertyDescriptor(propConfig)),
(prop) => prop.path
)
const defaultPropConfig = part.default || defaultFilters || []
const defaultDescriptors = defaultPropConfig.map((propConfig) =>
toModelPropertyDescriptor(propConfig)
)
return {
available: mergedAvailableDescriptors,
default: defaultDescriptors,
}
}

// merge the default modelViewConfig with the modelViewsConfig for each section
const resolveListViewsConfig = () => {
const merged: ResolvedSectionListView = {}

Object.entries(modelListViewsConfig).forEach((viewConfig) => {
const [sectionName, sectionViewConfig] = viewConfig
merged[sectionName] = {
columns: resolveColumnConfig(sectionViewConfig.columns),
filters: resolveFilterConfig(sectionViewConfig.filters),
}
})
return merged
}

const mergedModelViewsConfig = resolveListViewsConfig()
const resolvedDefaultConfig = {
columns: resolveColumnConfig(defaultModelViewConfig.columns),
filters: resolveFilterConfig(defaultModelViewConfig.filters),
}

export const getViewConfigForSection = (
sectionName: string
): ResolvedViewConfig => {
const resolvedConfig = mergedModelViewsConfig[sectionName]
if (resolvedConfig) {
return resolvedConfig
}
return resolvedDefaultConfig
}

export const getColumnsForSection = (
sectionName: string
): ResolvedViewConfig['columns'] => {
const view = getViewConfigForSection(sectionName)
return view.columns
}

export const getFiltersForSection = (
sectionName: string
): ResolvedViewConfig['filters'] => {
const view = getViewConfigForSection(sectionName)
return view.filters
}
21 changes: 15 additions & 6 deletions src/lib/constants/sectionListViewsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type SectionListViewConfig<Key extends string = string> = {
[key in Key]?: ViewConfig
}

const DESCRIPTORS = {
publicAccess: { path: 'sharing.public', label: i18n.t('Public access') },
} satisfies Record<string, ModelPropertyDescriptor>

// This is the default views, and can be overriden per section in modelListViewsConfig below
const defaultModelViewConfig = {
columns: {
Expand All @@ -46,10 +50,7 @@ const defaultModelViewConfig = {
'href',
'id',
'lastUpdatedBy',
{
label: i18n.t('Public access'),
path: 'sharing.public',
},
DESCRIPTORS.publicAccess,
],
default: ['name', 'sharing.public', 'lastUpdated'],
},
Expand All @@ -69,13 +70,21 @@ const defaultModelViewConfig = {
const modelListViewsConfig = {
dataElement: {
columns: {
available: ['zeroIsSignificant', 'categoryCombo'],
available: [
'zeroIsSignificant',
'categoryCombo',
// {
// label: i18n.t('Hello available public'),
// path: 'sharing.public',
// },
],
default: [
'name',
{ label: i18n.t('Domain'), path: 'domainType' },
{ label: i18n.t('Value type'), path: 'valueType' },
'lastUpdated',
'sharing.public',
// 'sharing.public',
{ label: i18n.t('Hello public'), path: 'sharing.public' },
],
},
filters: {
Expand Down
Loading

0 comments on commit c132fd6

Please sign in to comment.