Skip to content

Commit

Permalink
feat: support string ids for modelmulti-select
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Nov 18, 2024
1 parent 4b916b6 commit 4bc8ec3
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
cursor: pointer;
}

.multiSelect {
min-width: 150px;
}
.searchField {
display: flex;
gap: var(--spacers-dp8);
Expand Down
10 changes: 2 additions & 8 deletions src/components/SearchableMultiSelect/SearchableMultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const SearchableMultiSelect = ({

return (
<MultiSelect
className={classes.multiSelect}
selected={selected}
disabled={disabled}
error={!!error}
Expand All @@ -95,16 +96,9 @@ export const SearchableMultiSelect = ({
value={filter}
onChange={({ value }) => setFilterValue(value ?? '')}
placeholder={i18n.t('Filter options')}
type="search"
/>
</div>
<button
className={classes.clearButton}
disabled={!filter}
onClick={() => setFilterValue('')}
type="button"
>
clear
</button>
</div>

{options.map(({ value, label }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export const BaseModelMultiSelect = <TModel extends DisplayableModel>({
if (!selected) {
return
}

const selectedModels = selected
.map((s) => allModelsMap.get(s))
.filter((s) => !!s)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import React, { useMemo, useRef, useState } from 'react'
import { useInfiniteQuery, useQuery } 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 {
BaseModelMultiSelect,
BaseModelMultiSelectProps,
} from './BaseModelMultiSelect'

type Response<Model> = PagedResponse<Model, string>
import { useModelMultiSelectQuery } from './useModelMultiSelectQuery'

const defaultQuery = {
params: {
Expand Down Expand Up @@ -43,9 +39,9 @@ export const ModelMultiSelect = <TModel extends DisplayableModel>({
select,
...baseModelSingleSelectProps
}: ModelMultiSelectProps<TModel>) => {
const queryFn = useBoundResourceQueryFn()
// keep select in ref, so we dont recompute for inline selects
const selectRef = useRef(select)
select = selectRef.current
const [searchTerm, setSearchTerm] = useState('')
const searchFilter = `identifiable:token:${searchTerm}`
const filter: string[] = searchTerm ? [searchFilter] : []
Expand All @@ -59,61 +55,24 @@ export const ModelMultiSelect = <TModel extends DisplayableModel>({
filter: filter.concat(params?.filter || []),
},
}
const modelName = query.resource

const queryResult = useInfiniteQuery({
queryKey: [queryObject] as const,
queryFn: queryFn<Response<TModel>>,
keepPreviousData: true,
getNextPageParam: (lastPage) =>
lastPage.pager.nextPage ? lastPage.pager.page + 1 : undefined,
getPreviousPageParam: (firstPage) =>
firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined,
const {
selected: selectedData,
available: availableData,
isLoading,
error,
availableQuery,
} = useModelMultiSelectQuery({
query: queryObject,
selected,
})

const allDataMap = useMemo(() => {
const flatData =
queryResult.data?.pages.flatMap((page) => page[modelName]) ?? []
if (selectRef.current) {
return selectRef.current(flatData)
const resolvedAvailable = useMemo(() => {
if (select) {
return select(availableData)
}
return flatData
}, [queryResult.data, modelName])

const selectedWithoutData = selected.filter(
(s) => allDataMap.find((d) => d.id === s) === undefined
)
return availableData
}, [availableData])

const selectedQuery = useQuery({
queryKey: [
{
resource: modelName,
params: { filter: [`id:in:[${selected?.join()}]`] },
},
] as const,
queryFn: queryFn<Response<TModel>>,
enabled:
typeof selected?.[0] === 'string' && selectedWithoutData.length > 0,
})

const resolvedSelected = useMemo(() => {
if (selectedQuery.data) {
return selectedQuery.data[modelName]
}
if (selectedWithoutData.length === 0) {
return selected.map(
(s) => allDataMap.find((d) => d.id === s) as TModel
)
}
return selected as TModel[]
}, [
selectedQuery.data,
selected,
modelName,
allDataMap,
selectedWithoutData.length,
])
console.log({ resolvedSelected, selected })
const handleFilterChange = useDebouncedCallback(({ value }) => {
if (value != undefined) {
setSearchTerm(value)
Expand All @@ -124,14 +83,14 @@ export const ModelMultiSelect = <TModel extends DisplayableModel>({
return (
<BaseModelMultiSelect
{...baseModelSingleSelectProps}
selected={resolvedSelected}
available={allDataMap}
selected={selectedData}
available={resolvedAvailable}
onFilterChange={handleFilterChange}
onRetryClick={queryResult.refetch}
showEndLoader={!!queryResult.hasNextPage}
onEndReached={queryResult.fetchNextPage}
loading={queryResult.isLoading}
error={queryResult.error as string | undefined}
onRetryClick={availableQuery.refetch}
showEndLoader={!!availableQuery.hasNextPage}
onEndReached={() => !isLoading && availableQuery.fetchNextPage()}
loading={isLoading}
error={error?.toString()}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function ModelMultiSelectField<TModel extends DisplayableModel>({

return (
<Field
dataTest={`formfields-modelsingleselect-${name}`}
dataTest={`formfields-modelmultiselect-${name}`}
error={meta.invalid}
validationText={(meta.touched && meta.error?.toString()) || ''}
name={name}
Expand Down
3 changes: 3 additions & 0 deletions src/components/metadataFormControls/ModelMultiSelect/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './ModelMultiSelect'
export * from './ModelMultiSelectField'
export * from './BaseModelMultiSelect'
export * from './useRefreshMultiSelect'
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useMemo } from 'react'
import { useInfiniteQuery, useQuery } from 'react-query'
import { useBoundResourceQueryFn } from '../../../lib/query/useBoundQueryFn'
import { PlainResourceQuery } from '../../../types'
import { PagedResponse } from '../../../types/generated'
import { DisplayableModel } from '../../../types/models'

type Response<Model> = PagedResponse<Model, string>

const defaultQuery = {
params: {
order: 'displayName:asc',
fields: ['id', 'displayName'],
pageSize: 2,
},
} satisfies Omit<PlainResourceQuery, 'resource'>

type UseModelMultiSelectQueryOptions<TModel extends DisplayableModel> = {
query: Omit<PlainResourceQuery, 'id'>
selected: TModel[] | string[]
}
export const useModelMultiSelectQuery = <TModel extends DisplayableModel>({
query,
selected,
}: UseModelMultiSelectQueryOptions<TModel>) => {
const queryFn = useBoundResourceQueryFn()
const modelName = query.resource
const queryResult = useInfiniteQuery({
queryKey: [query],
queryFn: queryFn<Response<TModel>>,
keepPreviousData: true,
getNextPageParam: (lastPage) =>
lastPage.pager.nextPage ? lastPage.pager.page + 1 : undefined,
getPreviousPageParam: (firstPage) =>
firstPage.pager.prevPage ? firstPage.pager.page - 1 : undefined,
})
const flatData = useMemo(
() => queryResult.data?.pages.flatMap((page) => page[modelName]) ?? [],
[queryResult.data, modelName]
)
// in case selected are string (eg. a modelId) and are not in available data- resolve ids to a displayable model
const selectedWithoutData = selected.filter(
(s) =>
typeof s === 'string' &&
flatData.length > 0 &&
!flatData.find((d) => d.id === s)
)

const selectedQuery = useQuery({
queryKey: [
{
resource: modelName,
params: {
filter: [`id:in:[${selectedWithoutData?.join()}]`],
order: defaultQuery.params.order,
fields: defaultQuery.params.fields,
paging: false, // this should be OK since selected should be a limited list...
},
},
] as const,
queryFn: queryFn<Response<TModel>>,
enabled: selectedWithoutData.length > 0,
})
const resolvedSelected = selected.map((s) => {
if (typeof s === 'string') {
return (
flatData.find((d) => d.id === s) ||
selectedQuery.data?.[modelName].find((d) => d.id === s) ||
({
id: s,
displayName: s,
} as TModel)
)
}
return s
})

return {
selected: resolvedSelected,
available: flatData,
isLoading: queryResult.isLoading || selectedQuery.isLoading,
error: queryResult.error || selectedQuery.error,
availableQuery: queryResult,
selectedQuery,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import { useCallback } from 'react'
import { useQueryClient, InvalidateQueryFilters } from 'react-query'
import { PlainResourceQuery } from '../../../types'

export const useRefreshModelSingleSelect = (
export const useRefreshModelMultiSelect = (
query: Omit<PlainResourceQuery, 'id'>
) => {
const queryClient = useQueryClient()

return useCallback(
(invalidateFilters?: InvalidateQueryFilters) => {
console.log('invalidate', query)
queryClient.invalidateQueries({
queryKey: [query],
// ...invalidateFilters,
...invalidateFilters,
})
},
[queryClient, query]
Expand Down
3 changes: 2 additions & 1 deletion src/types/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ type QueryParams = {
page?: number
fields?: string | string[]
filter?: string | string[]
[key: string]: unknown
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}

export type PlainResourceQuery = {
Expand Down

0 comments on commit 4bc8ec3

Please sign in to comment.