From 4bc8ec3fd01baad2dbd30e6a2f5ec3025df275e0 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Mon, 18 Nov 2024 17:32:17 +0100 Subject: [PATCH] feat: support string ids for modelmulti-select --- .../SearchableMultiSelect.module.css | 3 + .../SearchableMultiSelect.tsx | 10 +-- .../ModelMultiSelect/BaseModelMultiSelect.tsx | 1 - .../ModelMultiSelect/ModelMultiSelect.tsx | 87 +++++-------------- .../ModelMultiSelectField.tsx | 2 +- .../ModelMultiSelect/index.ts | 3 + .../useModelMultiSelectQuery.tsx | 86 ++++++++++++++++++ .../useRefreshMultiSelect.tsx | 5 +- src/types/query.ts | 3 +- 9 files changed, 122 insertions(+), 78 deletions(-) create mode 100644 src/components/metadataFormControls/ModelMultiSelect/useModelMultiSelectQuery.tsx diff --git a/src/components/SearchableMultiSelect/SearchableMultiSelect.module.css b/src/components/SearchableMultiSelect/SearchableMultiSelect.module.css index d78ac769..e691512c 100644 --- a/src/components/SearchableMultiSelect/SearchableMultiSelect.module.css +++ b/src/components/SearchableMultiSelect/SearchableMultiSelect.module.css @@ -37,6 +37,9 @@ cursor: pointer; } +.multiSelect { + min-width: 150px; +} .searchField { display: flex; gap: var(--spacers-dp8); diff --git a/src/components/SearchableMultiSelect/SearchableMultiSelect.tsx b/src/components/SearchableMultiSelect/SearchableMultiSelect.tsx index 8c4c108a..e5943849 100644 --- a/src/components/SearchableMultiSelect/SearchableMultiSelect.tsx +++ b/src/components/SearchableMultiSelect/SearchableMultiSelect.tsx @@ -76,6 +76,7 @@ export const SearchableMultiSelect = ({ return ( setFilterValue(value ?? '')} placeholder={i18n.t('Filter options')} + type="search" /> - {options.map(({ value, label }) => ( diff --git a/src/components/metadataFormControls/ModelMultiSelect/BaseModelMultiSelect.tsx b/src/components/metadataFormControls/ModelMultiSelect/BaseModelMultiSelect.tsx index eddb113d..8b7d50cb 100644 --- a/src/components/metadataFormControls/ModelMultiSelect/BaseModelMultiSelect.tsx +++ b/src/components/metadataFormControls/ModelMultiSelect/BaseModelMultiSelect.tsx @@ -62,7 +62,6 @@ export const BaseModelMultiSelect = ({ if (!selected) { return } - const selectedModels = selected .map((s) => allModelsMap.get(s)) .filter((s) => !!s) diff --git a/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelect.tsx b/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelect.tsx index 547e2f6d..10eeb58f 100644 --- a/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelect.tsx +++ b/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelect.tsx @@ -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 = PagedResponse +import { useModelMultiSelectQuery } from './useModelMultiSelectQuery' const defaultQuery = { params: { @@ -43,9 +39,9 @@ export const ModelMultiSelect = ({ select, ...baseModelSingleSelectProps }: ModelMultiSelectProps) => { - 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] : [] @@ -59,61 +55,24 @@ export const ModelMultiSelect = ({ 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 { + 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>, - 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) @@ -124,14 +83,14 @@ export const ModelMultiSelect = ({ return ( !isLoading && availableQuery.fetchNextPage()} + loading={isLoading} + error={error?.toString()} /> ) } diff --git a/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelectField.tsx b/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelectField.tsx index f10d8cb1..63880fc8 100644 --- a/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelectField.tsx +++ b/src/components/metadataFormControls/ModelMultiSelect/ModelMultiSelectField.tsx @@ -36,7 +36,7 @@ export function ModelMultiSelectField({ return ( = PagedResponse + +const defaultQuery = { + params: { + order: 'displayName:asc', + fields: ['id', 'displayName'], + pageSize: 2, + }, +} satisfies Omit + +type UseModelMultiSelectQueryOptions = { + query: Omit + selected: TModel[] | string[] +} +export const useModelMultiSelectQuery = ({ + query, + selected, +}: UseModelMultiSelectQueryOptions) => { + const queryFn = useBoundResourceQueryFn() + const modelName = query.resource + const queryResult = useInfiniteQuery({ + queryKey: [query], + 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 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>, + 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, + } +} diff --git a/src/components/metadataFormControls/ModelMultiSelect/useRefreshMultiSelect.tsx b/src/components/metadataFormControls/ModelMultiSelect/useRefreshMultiSelect.tsx index d1b53384..eb550acb 100644 --- a/src/components/metadataFormControls/ModelMultiSelect/useRefreshMultiSelect.tsx +++ b/src/components/metadataFormControls/ModelMultiSelect/useRefreshMultiSelect.tsx @@ -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 ) => { const queryClient = useQueryClient() return useCallback( (invalidateFilters?: InvalidateQueryFilters) => { - console.log('invalidate', query) queryClient.invalidateQueries({ queryKey: [query], - // ...invalidateFilters, + ...invalidateFilters, }) }, [queryClient, query] diff --git a/src/types/query.ts b/src/types/query.ts index 26887911..19a60a27 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -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 = {