Skip to content

Commit

Permalink
fix(list): resolve reference names (#360)
Browse files Browse the repository at this point in the history
* feat(datastore): add hook for accessing datastore

* fix(types): fix types for section handle

* feat(constants): add constants for columns and translated properties

* fix: update types for sectionlistcolumns

* fix: add selector to usedatastore

* feat(managecolumns): manage columns implementation

* fix: integrate selectedcoluns with list

* fix: mergecolumns

* fix: managecolumns style

* refactor: managecolumns

* chore: add remeda util lib

* refactor: refactor to config for listviews

* refactor: manage list view

* fix: respect order of selected columns

* fix(datastore): always use put for datastore

* refactor: allow arbitrary path as column

* refactor: remove remeda, implement uniqueBy

* fix: add uniqueBy

* fix: add test for path

* fix: add maxDepth to getFieldFilterFromPath

* fix: fix some tests

* fix: remove typepath file

* refactor: cleanup

* fix(constants): prevent circular dependency (#354)

* fix(managelistview): enforce at least one column

* refactor: rename DataStoreModelListView schema

* refactor: rename useDataStoreValuesQuery

* refactor(modelvale): simplify modelvalue

* fix: fix imports

* refactor: remove component from list config

* fix: fix tests

* refactor: minor cleanup

* feat(detailpanel): initial detailsPanel implementation

* refactor(detailspanel): cleanup details panel implementation, add error-boundary

* fix: responsive layout with detailspanel

* refactor: fix wrong name of sectionlistheader

* refactor: minor cleanup

* fix: fix id missing in details response

* fix: add ui to resolution for alpha version

* fix: fix type errors

* refactor(details): make detailspanelcontent more composable

* fix(list): rename to value type in list

* fix: add type fro modelcollectionresponse

* fix(list): resolve displayName for reference columns

* fix(details): add created by field

* fix: conlict in i18n

* refactor: cleanup ts-ignore

* fix: fix bad merge

* fix(list): parse query filter

* refactor: remove useModelGist

* fix(test): fix list tests

* fix(test): add missing mock files

* refactor: some cleanup

* fix(modelview): validate prevdata before saving

---------

Co-authored-by: Jan-Gerke Salomon <[email protected]>
  • Loading branch information
Birkbjo and Mohammer5 authored Dec 20, 2023
1 parent 2ae9299 commit ccd226c
Show file tree
Hide file tree
Showing 18 changed files with 427 additions and 337 deletions.
4 changes: 2 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ msgstr "Public access"
msgid "Domain"
msgstr "Domain"

msgid "Value"
msgstr "Value"
msgid "Value type"
msgstr "Value type"

msgid "Category"
msgstr "Category"
Expand Down
34 changes: 15 additions & 19 deletions src/components/sectionList/SectionListPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import {
NumericObjectParam,
withDefault,
} from 'use-query-params'
import { GistPaginator } from '../../lib/'
import { GistCollectionResponse } from '../../types/generated'
import { Pager } from '../../types/generated'

type SectionListPaginationProps = {
data: GistCollectionResponse | undefined
pager: Pager | undefined
}

type PaginationQueryParams = {
export type PaginationQueryParams = {
page: number
pageSize: number
}
Expand Down Expand Up @@ -67,19 +66,15 @@ const validatePagerParams = (
}
}

function useUpdatePaginationParams(
data?: GistCollectionResponse
): GistPaginator {
const pager = data?.pager
const [, setParams] = usePaginationQueryParams()
type Paginator = {
changePageSize: (pageSize: number) => boolean
getPrevPage: () => boolean
goToPage: (page: number) => boolean
pager?: Pager
}

const getNextPage = useCallback(() => {
if (!pager?.nextPage) {
return false
}
setParams((prevPager) => ({ ...prevPager, page: pager.page + 1 }))
return true
}, [pager, setParams])
function useUpdatePaginationParams(pager?: Pager): Paginator {
const [, setParams] = usePaginationQueryParams()

const getPrevPage = useCallback(() => {
if (!pager?.prevPage) {
Expand Down Expand Up @@ -109,7 +104,6 @@ function useUpdatePaginationParams(
)

return {
getNextPage,
getPrevPage,
goToPage,
changePageSize,
Expand All @@ -123,9 +117,11 @@ function useUpdatePaginationParams(
const clamp = (value: number, min: number, max: number) =>
Math.max(min, Math.min(value, max))

export const SectionListPagination = ({ data }: SectionListPaginationProps) => {
export const SectionListPagination = ({
pager,
}: SectionListPaginationProps) => {
const [paginationParams] = usePaginationQueryParams()
const pagination = useUpdatePaginationParams(data)
const pagination = useUpdatePaginationParams(pager)

useEffect(() => {
// since page can be controlled by params
Expand Down
4 changes: 2 additions & 2 deletions src/components/sectionList/SectionListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import css from './SectionList.module.css'
import { SelectedColumns, SelectedColumn } from './types'

export type SectionListRowProps<Model extends IdentifiableObject> = {
modelData: GistModel<Model>
modelData: GistModel<Model> | Model
selectedColumns: SelectedColumns
onSelect: (modelId: string, checked: boolean) => void
selected: boolean
renderColumnValue: (column: SelectedColumn) => React.ReactNode
onClick?: (modelData: GistModel<Model>) => void
onClick?: (modelData: GistModel<Model> | Model) => void
active?: boolean
}

Expand Down
30 changes: 15 additions & 15 deletions src/components/sectionList/SectionListWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FetchError } from '@dhis2/app-runtime'
import React, { useMemo, useState } from 'react'
import { useSchemaFromHandle } from '../../lib'
import { IdentifiableObject, GistCollectionResponse } from '../../types/models'
import { Pager, ModelCollection } from '../../types/models'
import { DetailsPanel, DefaultDetailsPanelContent } from './detailsPanel'
import { FilterWrapper } from './filters/FilterWrapper'
import { useModelListView } from './listView'
Expand All @@ -15,17 +15,20 @@ import { SectionListPagination } from './SectionListPagination'
import { SectionListRow } from './SectionListRow'
import { SectionListTitle } from './SectionListTitle'

type SectionListWrapperProps<Model extends IdentifiableObject> = {
type SectionListWrapperProps = {
filterElement?: React.ReactElement
data: GistCollectionResponse<Model> | undefined
data: ModelCollection | undefined
pager: Pager | undefined
error: FetchError | undefined
}

export const SectionListWrapper = <Model extends IdentifiableObject>({
export const SectionListWrapper = ({
filterElement,
data,
error,
}: SectionListWrapperProps<Model>) => {
pager,
}: SectionListWrapperProps) => {
data
const { columns: headerColumns } = useModelListView()
const schema = useSchemaFromHandle()
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set())
Expand All @@ -44,7 +47,7 @@ export const SectionListWrapper = <Model extends IdentifiableObject>({
if (checked) {
setSelectedModels(
new Set(
data?.result?.map((model) => {
data?.map((model) => {
return model.id
})
)
Expand All @@ -55,21 +58,18 @@ export const SectionListWrapper = <Model extends IdentifiableObject>({
}

const allSelected = useMemo(() => {
return (
data?.result.length !== 0 &&
data?.result.length === selectedModels.size
)
}, [data?.result, selectedModels.size])
return data?.length !== 0 && data?.length === selectedModels.size
}, [data, selectedModels.size])

const SectionListMessage = () => {
if (error) {
console.log(error.details || error)
return <SectionListError />
}
if (!data?.result) {
if (!data) {
return <SectionListLoader />
}
if (data?.result?.length < 1) {
if (data.length < 1) {
return <SectionListEmpty />
}
return null
Expand All @@ -87,7 +87,7 @@ export const SectionListWrapper = <Model extends IdentifiableObject>({
allSelected={allSelected}
>
<SectionListMessage />
{data?.result.map((model) => (
{data?.map((model) => (
<SectionListRow
key={model.id}
modelData={model}
Expand All @@ -112,7 +112,7 @@ export const SectionListWrapper = <Model extends IdentifiableObject>({
/>
))}

<SectionListPagination data={data} />
<SectionListPagination pager={pager} />
</SectionList>
{detailsId && (
<DetailsPanel
Expand Down
47 changes: 7 additions & 40 deletions src/components/sectionList/filters/useSectionListFilter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { useCallback, useMemo } from 'react'
import { useQueryParam, ObjectParam, UrlUpdateType } from 'use-query-params'
import {
Schema,
useSchemaFromHandle,
CustomObjectParam,
GistParams,
} from '../../../lib'
import { Schema, useSchemaFromHandle, CustomObjectParam } from '../../../lib'
import { usePaginationQueryParams } from '../SectionListPagination'

type ObjectParamType = typeof ObjectParam.default
Expand All @@ -17,21 +12,6 @@ type Filters = Record<string, string | undefined>
// this would translate to "token" in the old API, but does not exist in GIST-API
export const IDENTIFIABLE_KEY = 'identifiable'

const IDENTIFIABLE_FIELDS = {
name: {
operator: 'ilike',
},
code: {
operator: 'ilike',
},
shortName: {
operator: 'ilike',
},
id: {
operator: 'eq',
},
}

const getVerifiedFiltersForSchema = (
filters: ObjectParamType,
schema: Schema
Expand Down Expand Up @@ -105,29 +85,16 @@ export const useSectionListFilter = (
return [filters?.[filterKey] ?? undefined, boundSetFilter]
}

const parseToGistQueryFilter = (filters: Filters): string[] => {
const parseToQueryFilter = (filters: Filters): string[] => {
const { [IDENTIFIABLE_KEY]: identifiableValue, ...restFilters } = filters
const queryFilters: string[] = []

// Groups are a powerful way to combine filters,
// here we use them for identifiable filters, to group them with "OR" and
// rest of the filters with "AND".
// see https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-239/metadata-gist.html#gist_parameters_filter
if (identifiableValue) {
const identifiableFilterGroup = `0:`
Object.entries(IDENTIFIABLE_FIELDS).forEach(([key, { operator }]) => {
queryFilters.push(
`${identifiableFilterGroup}${key}:${operator}:${identifiableValue}`
)
})
}
let restFilterGroup: number | undefined
const identifiableFilter = `identifiable:token:${identifiableValue}`
if (identifiableValue) {
restFilterGroup = 1
queryFilters.push(identifiableFilter)
}
Object.entries(restFilters).forEach(([key, value]) => {
const group = restFilterGroup ? `${restFilterGroup++}:` : ''
queryFilters.push(`${group}${key}:eq:${value}`)
queryFilters.push(`${key}:eq:${value}`)
})
return queryFilters
}
Expand All @@ -136,11 +103,11 @@ export const useSectionListQueryFilter = () => {
const [filters] = useSectionListFilters()

return useMemo(() => {
return parseToGistQueryFilter(filters)
return parseToQueryFilter(filters)
}, [filters])
}

export const useQueryParamsForModelGist = (): GistParams => {
export const useQueryParamsForModelGist = () => {
const [paginationParams] = usePaginationQueryParams()
const filterParams = useSectionListQueryFilter()

Expand Down
8 changes: 6 additions & 2 deletions src/components/sectionList/listView/useModelListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const useModelListView = () => {
})

// 404 errors are expected when user havent saved any views
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (query.error && (query.error as any).details?.httpStatusCode !== 404) {
console.error(query.error)
}
Expand Down Expand Up @@ -163,11 +164,14 @@ export const useMutateModelListViews = () => {
// it's exact data as we got from the request
const prevData: WrapInResult<DataStoreModelListViews> | undefined =
queryClient.getQueryData(valuesQueryKey)
if (!prevData) {

// need to validate here since we're not using a selector
const validView = modelListViewsSchema.safeParse(prevData?.result)
if (!validView.success) {
return {}
}

return prevData.result
return validView.data
}, [queryClient])

const saveView = useCallback(
Expand Down
21 changes: 21 additions & 0 deletions src/components/sectionList/modelValue/ModelReference.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import i18n from '@dhis2/d2-i18n'
import React from 'react'

type ModelReference = {
id: string
displayName: string
}

export const isModelReference = (value: unknown): value is ModelReference => {
const asModelReference = value as ModelReference
return !!asModelReference.displayName && !!asModelReference.id
}

export const ModelReference = ({ value }: { value?: ModelReference }) => {
let displayValue = value?.displayName
// default categoryCombos should display as None
if (displayValue === 'default') {
displayValue = i18n.t('None')
}
return <span>{displayValue}</span>
}
10 changes: 9 additions & 1 deletion src/components/sectionList/modelValue/ModelValueRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react'
import { SchemaFieldProperty } from '../../../lib'
import { SchemaFieldProperty, SchemaFieldPropertyType } from '../../../lib'
import { BooleanValue } from './BooleanValue'
import { ConstantValue } from './ConstantValue'
import { DateValue } from './DateValue'
import { ModelReference, isModelReference } from './ModelReference'
import { PublicAccessValue } from './PublicAccess'
import { TextValue } from './TextValue'

Expand All @@ -29,6 +30,13 @@ export const ModelValueRenderer = ({
return <DateValue value={value as string} />
}

if (
schemaProperty.propertyType === SchemaFieldPropertyType.REFERENCE &&
isModelReference(value)
) {
return <ModelReference value={value} />
}

if (typeof value === 'boolean') {
return <BooleanValue value={value} />
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants/sectionListViewsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const modelListViewsConfig = {
default: [
'name',
{ label: i18n.t('Domain'), path: 'domainType' },
{ label: i18n.t('Value'), path: 'valueType' },
{ label: i18n.t('Value type'), path: 'valueType' },
'lastUpdated',
'sharing.public',
],
Expand Down
7 changes: 0 additions & 7 deletions src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
export { useModelGist } from './useModelGist'
export type {
GistPaginator,
GistParams,
UseModelGistResultPaginated,
UseModelGistResult,
} from './useModelGist'
export { isValidUid } from './uid'
export { parsePublicAccessString } from './parsePublicAccess'
export { getIn, stringToPathArray, getFieldFilterFromPath } from './path'
29 changes: 29 additions & 0 deletions src/lib/models/path.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { SchemaFieldPropertyType, type SchemaFieldProperty } from '../schemas'
import type { Schema } from '../useLoadApp'

export const stringToPathArray = (str: string): string[] => str.split('.')

const resolvePath = (path: string | string[]): string[] => {
Expand Down Expand Up @@ -55,3 +58,29 @@ export const getFieldFilterFromPath = (

return recur(resolvePath(path), 0)
}

export const getSchemaPropertyForPath = (
schema: Schema,
path: string
): SchemaFieldProperty | undefined => {
const pathParts = stringToPathArray(path).map((part) => {
if (part === 'id') {
return 'uid' // fieldName for 'id' is "uid" in schema.properties
}
return part
})
const rootPath = pathParts[0]

const schemaProperty = schema.properties[rootPath]
return schemaProperty
}

export const getFieldFilter = (schema: Schema, path: string) => {
const schemaProperty = getSchemaPropertyForPath(schema, path)

if (schemaProperty?.propertyType === SchemaFieldPropertyType.REFERENCE) {
return `${schemaProperty.fieldName}[id, displayName]`
}

return getFieldFilterFromPath(path)
}
Loading

0 comments on commit ccd226c

Please sign in to comment.