From 06936c3c2ab0a4049c4a158a9ecd0c906d7c2e51 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 7 Dec 2023 12:08:48 +0100 Subject: [PATCH] Feat/multi relation filter (#2858) * WIP * Finished multi select filter * Cleaned console log * Fix naming * Fixed naming --- .../hooks/useObjectMetadataItem.ts | 40 +++++ .../MultipleRecordSelectDropdown.tsx | 85 ++++++++++ .../select/hooks/useRecordsForSelect.ts | 159 ++++++++++++++++++ .../select/types/SelectableRecord.ts | 10 ++ .../select/utils/getObjectFilterFields.ts | 11 ++ .../select/utils/getObjectOrderByField.ts | 11 ++ .../types/ObjectRecordIdentifier.ts | 8 + .../MultipleFiltersDropdownContent.tsx | 13 +- .../ObjectFilterDropdownEntitySearchInput.tsx | 2 +- .../ObjectFilterDropdownEntitySelect.tsx | 58 ------- .../ObjectFilterDropdownRecordSelect.tsx | 85 ++++++++++ ...SingleEntityObjectFilterDropdownButton.tsx | 8 +- .../hooks/useFilterDropdown.ts | 6 + .../hooks/useFilterDropdownStates.ts | 11 ++ ...terDropdownSelectedRecordIdsScopedState.ts | 7 + .../utils/turnFiltersIntoWhereClause.ts | 53 +++--- .../components/RecordTableBodyEffect.tsx | 7 +- .../views/components/ViewBarFilterEffect.tsx | 35 +++- 18 files changed, 516 insertions(+), 93 deletions(-) create mode 100644 front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx create mode 100644 front/src/modules/object-record/select/hooks/useRecordsForSelect.ts create mode 100644 front/src/modules/object-record/select/types/SelectableRecord.ts create mode 100644 front/src/modules/object-record/select/utils/getObjectFilterFields.ts create mode 100644 front/src/modules/object-record/select/utils/getObjectOrderByField.ts create mode 100644 front/src/modules/object-record/types/ObjectRecordIdentifier.ts delete mode 100644 front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx create mode 100644 front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx create mode 100644 front/src/modules/ui/object/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts diff --git a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 2503893cc..2cdeb9196 100644 --- a/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -12,7 +12,9 @@ import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerate import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation'; import { useGetRecordFromCache } from '@/object-record/hooks/useGetRecordFromCache'; import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache'; +import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; import { generateDeleteOneRecordMutation } from '@/object-record/utils/generateDeleteOneRecordMutation'; +import { getLogoUrlFromDomainName } from '~/utils'; import { isDefined } from '~/utils/isDefined'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; @@ -61,6 +63,43 @@ export const useObjectMetadataItem = ( ); } + const mapToObjectRecordIdentifier = (record: any): ObjectRecordIdentifier => { + if (objectNameSingular === 'company') { + return { + id: record.id, + name: record.name, + avatarUrl: getLogoUrlFromDomainName(record.domainName ?? ''), + avatarType: 'squared', + }; + } + + if (['workspaceMember', 'person'].includes(objectNameSingular)) { + return { + id: record.id, + name: + (record.name?.firstName ?? '') + ' ' + (record.name?.lastName ?? ''), + avatarUrl: record.avatarUrl, + avatarType: 'rounded', + }; + } + + if (['opportunity'].includes(objectNameSingular)) { + return { + id: record.id, + name: record?.company?.name, + avatarUrl: record.avatarUrl, + avatarType: 'rounded', + }; + } + + return { + id: record.id, + name: record.name, + avatarUrl: record.avatarUrl, + avatarType: 'rounded', + }; + }; + const getRecordFromCache = useGetRecordFromCache({ objectMetadataItem, }); @@ -108,5 +147,6 @@ export const useObjectMetadataItem = ( createOneRecordMutation, updateOneRecordMutation, deleteOneRecordMutation, + mapToObjectRecordIdentifier, }; }; diff --git a/front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx b/front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx new file mode 100644 index 000000000..15914204d --- /dev/null +++ b/front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react'; + +import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; +import { Avatar } from '@/users/components/Avatar'; + +export const MultipleRecordSelectDropdown = ({ + recordsToSelect, + loadingRecords, + filteredSelectedRecords, + onChange, + searchFilter, +}: { + recordsToSelect: SelectableRecord[]; + filteredSelectedRecords: SelectableRecord[]; + selectedRecords: SelectableRecord[]; + searchFilter: string; + onChange: ( + changedRecordToSelect: SelectableRecord, + newSelectedValue: boolean, + ) => void; + loadingRecords: boolean; +}) => { + const handleRecordSelectChange = ( + recordToSelect: SelectableRecord, + newSelectedValue: boolean, + ) => { + onChange( + { + ...recordToSelect, + isSelected: newSelectedValue, + }, + newSelectedValue, + ); + }; + + const [recordsInDropdown, setRecordInDropdown] = useState([ + ...(filteredSelectedRecords ?? []), + ...(recordsToSelect ?? []), + ]); + + useEffect(() => { + if (!loadingRecords) { + setRecordInDropdown([ + ...(filteredSelectedRecords ?? []), + ...(recordsToSelect ?? []), + ]); + } + }, [recordsToSelect, filteredSelectedRecords, loadingRecords]); + + const showNoResult = + recordsToSelect?.length === 0 && + searchFilter !== '' && + filteredSelectedRecords?.length === 0 && + !loadingRecords; + + return ( + + {recordsInDropdown?.map((record) => ( + + handleRecordSelectChange(record, newCheckedValue) + } + avatar={ + + } + text={record.name} + /> + ))} + {showNoResult && } + {loadingRecords && } + + ); +}; diff --git a/front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/front/src/modules/object-record/select/hooks/useRecordsForSelect.ts new file mode 100644 index 000000000..de5ae39e1 --- /dev/null +++ b/front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -0,0 +1,159 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields'; +import { getObjectOrderByField } from '@/object-record/select/utils/getObjectOrderByField'; +import { isDefined } from '~/utils/isDefined'; + +export type OrderBy = + | 'AscNullsLast' + | 'DescNullsLast' + | 'AscNullsFirst' + | 'DescNullsFirst'; + +export const DEFAULT_SEARCH_REQUEST_LIMIT = 60; + +export const useRecordsForSelect = ({ + searchFilterText, + sortOrder = 'AscNullsLast', + selectedIds, + limit, + excludeEntityIds = [], + objectNameSingular, +}: { + searchFilterText: string; + sortOrder?: OrderBy; + selectedIds: string[]; + limit?: number; + excludeEntityIds?: string[]; + objectNameSingular: string; +}) => { + const { mapToObjectRecordIdentifier } = useObjectMetadataItem({ + objectNameSingular, + }); + + const filters = [ + { + fieldNames: getObjectFilterFields(objectNameSingular) ?? [], + filter: searchFilterText, + }, + ]; + + const orderByField = getObjectOrderByField(objectNameSingular); + + const { loading: selectedRecordsLoading, records: selectedRecordsData } = + useFindManyRecords({ + filter: { + id: { + in: selectedIds, + }, + }, + orderBy: { + [orderByField]: sortOrder, + }, + objectNameSingular, + }); + + const searchFilter = filters + .map(({ fieldNames, filter }) => { + if (!isNonEmptyString(filter)) { + return undefined; + } + + return { + or: fieldNames.map((fieldName) => { + const fieldNameParts = fieldName.split('.'); + + if (fieldNameParts.length > 1) { + // Composite field + + return { + [fieldNameParts[0]]: { + [fieldNameParts[1]]: { + ilike: `%${filter}%`, + }, + }, + }; + } + return { + [fieldName]: { + ilike: `%${filter}%`, + }, + }; + }), + }; + }) + .filter(isDefined); + + const { + loading: filteredSelectedRecordsLoading, + records: filteredSelectedRecordsData, + } = useFindManyRecords({ + filter: { + and: [ + { + and: searchFilter, + }, + { + id: { + in: selectedIds, + }, + }, + ], + }, + orderBy: { + [orderByField]: sortOrder, + }, + objectNameSingular, + }); + + const { loading: recordsToSelectLoading, records: recordsToSelectData } = + useFindManyRecords({ + filter: { + and: [ + { + and: searchFilter, + }, + { + not: { + id: { + in: [...selectedIds, ...excludeEntityIds], + }, + }, + }, + ], + }, + limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, + orderBy: { + [orderByField]: sortOrder, + }, + objectNameSingular, + }); + + return { + selectedRecords: selectedRecordsData + .map(mapToObjectRecordIdentifier) + .map((record) => ({ + ...record, + isSelected: true, + })) as SelectableRecord[], + filteredSelectedRecords: filteredSelectedRecordsData + .map(mapToObjectRecordIdentifier) + .map((record) => ({ + ...record, + isSelected: true, + })) as SelectableRecord[], + recordsToSelect: recordsToSelectData + .map(mapToObjectRecordIdentifier) + .map((record) => ({ + ...record, + isSelected: false, + })) as SelectableRecord[], + loading: + recordsToSelectLoading || + filteredSelectedRecordsLoading || + selectedRecordsLoading, + }; +}; diff --git a/front/src/modules/object-record/select/types/SelectableRecord.ts b/front/src/modules/object-record/select/types/SelectableRecord.ts new file mode 100644 index 000000000..202a565e0 --- /dev/null +++ b/front/src/modules/object-record/select/types/SelectableRecord.ts @@ -0,0 +1,10 @@ +import { AvatarType } from '@/users/components/Avatar'; + +export type SelectableRecord = { + id: string; + name: string; + avatarUrl?: string; + avatarType?: AvatarType; + record: any; + isSelected: boolean; +}; diff --git a/front/src/modules/object-record/select/utils/getObjectFilterFields.ts b/front/src/modules/object-record/select/utils/getObjectFilterFields.ts new file mode 100644 index 000000000..6ad643eaa --- /dev/null +++ b/front/src/modules/object-record/select/utils/getObjectFilterFields.ts @@ -0,0 +1,11 @@ +export const getObjectFilterFields = (objectSingleName: string) => { + if (objectSingleName === 'company') { + return ['name']; + } + + if (['workspaceMember', 'person'].includes(objectSingleName)) { + return ['name.firstName', 'name.lastName']; + } + + return ['name']; +}; diff --git a/front/src/modules/object-record/select/utils/getObjectOrderByField.ts b/front/src/modules/object-record/select/utils/getObjectOrderByField.ts new file mode 100644 index 000000000..0f33e3fec --- /dev/null +++ b/front/src/modules/object-record/select/utils/getObjectOrderByField.ts @@ -0,0 +1,11 @@ +export const getObjectOrderByField = (objectSingleName: string): string => { + if (objectSingleName === 'company') { + return 'name'; + } + + if (['workspaceMember', 'person'].includes(objectSingleName)) { + return 'name.firstName'; + } + + return 'createdAt'; +}; diff --git a/front/src/modules/object-record/types/ObjectRecordIdentifier.ts b/front/src/modules/object-record/types/ObjectRecordIdentifier.ts new file mode 100644 index 000000000..2401982b3 --- /dev/null +++ b/front/src/modules/object-record/types/ObjectRecordIdentifier.ts @@ -0,0 +1,8 @@ +import { AvatarType } from '@/users/components/Avatar'; + +export type ObjectRecordIdentifier = { + id: string; + name: string; + avatarUrl?: string; + avatarType?: AvatarType; +}; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 468f29010..ddd9bd14e 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,14 +1,14 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { ObjectFilterDropdownRecordSearchInput } from '@/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput'; import { useFilterDropdown } from '@/ui/object/object-filter-dropdown/hooks/useFilterDropdown'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownDateSearchInput } from './ObjectFilterDropdownDateSearchInput'; -import { ObjectFilterDropdownEntitySearchInput } from './ObjectFilterDropdownEntitySearchInput'; -import { ObjectFilterDropdownEntitySelect } from './ObjectFilterDropdownEntitySelect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; import { ObjectFilterDropdownNumberSearchInput } from './ObjectFilterDropdownNumberSearchInput'; import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton'; import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect'; +import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; export const MultipleFiltersDropdownContent = () => { @@ -39,10 +39,11 @@ export const MultipleFiltersDropdownContent = () => { )} {filterDefinitionUsedInDropdown.type === 'RELATION' && ( - - )} - {filterDefinitionUsedInDropdown.type === 'RELATION' && ( - + <> + + + + )} ) diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx index 69be5af68..82d5f1bf0 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput.tsx @@ -3,7 +3,7 @@ import { ChangeEvent } from 'react'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { useFilterDropdown } from '@/ui/object/object-filter-dropdown/hooks/useFilterDropdown'; -export const ObjectFilterDropdownEntitySearchInput = () => { +export const ObjectFilterDropdownRecordSearchInput = () => { const { filterDefinitionUsedInDropdown, selectedOperandInDropdown, diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx deleted file mode 100644 index a946d2cbc..000000000 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useQuery } from '@apollo/client'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; -import { useRelationPicker } from '@/ui/input/components/internal/relation-picker/hooks/useRelationPicker'; -import { ObjectFilterDropdownEntitySearchSelect } from '@/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect'; -import { useFilterDropdown } from '@/ui/object/object-filter-dropdown/hooks/useFilterDropdown'; - -export const ObjectFilterDropdownEntitySelect = () => { - const { - filterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, - objectFilterDropdownSelectedEntityId, - } = useFilterDropdown(); - - const objectMetadataNameSingular = - filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular ?? ''; - - // TODO: refactor useFilteredSearchEntityQuery - const { findManyRecordsQuery } = useObjectMetadataItem({ - objectNameSingular: objectMetadataNameSingular, - }); - - const useFindManyQuery = (options: any) => - useQuery(findManyRecordsQuery, options); - - const { identifiersMapper, searchQuery } = useRelationPicker(); - - const filteredSearchEntityResults = useFilteredSearchEntityQuery({ - queryHook: useFindManyQuery, - filters: [ - { - fieldNames: - searchQuery?.computeFilterFields?.(objectMetadataNameSingular) ?? [], - filter: objectFilterDropdownSearchInput, - }, - ], - orderByField: 'createdAt', - selectedIds: objectFilterDropdownSelectedEntityId - ? [objectFilterDropdownSelectedEntityId] - : [], - mappingFunction: (record: any) => - identifiersMapper?.(record, objectMetadataNameSingular), - objectNameSingular: objectMetadataNameSingular, - }); - - if (filterDefinitionUsedInDropdown?.type !== 'RELATION') { - return null; - } - - return ( - <> - - - ); -}; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx new file mode 100644 index 000000000..5c529c8d3 --- /dev/null +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -0,0 +1,85 @@ +import { MultipleRecordSelectDropdown } from '@/object-record/select/components/MultipleRecordSelectDropdown'; +import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; +import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { useFilterDropdown } from '@/ui/object/object-filter-dropdown/hooks/useFilterDropdown'; + +export const EMPTY_FILTER_VALUE = ''; +export const MAX_RECORDS_TO_DISPLAY = 3; + +export const ObjectFilterDropdownRecordSelect = () => { + const { + filterDefinitionUsedInDropdown, + objectFilterDropdownSearchInput, + selectedOperandInDropdown, + setObjectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedRecordIds, + selectFilter, + } = useFilterDropdown(); + + const objectNameSingular = + filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular ?? ''; + + const { loading, filteredSelectedRecords, recordsToSelect, selectedRecords } = + useRecordsForSelect({ + searchFilterText: objectFilterDropdownSearchInput, + selectedIds: objectFilterDropdownSelectedRecordIds, + objectNameSingular, + limit: 10, + }); + + const handleMultipleRecordSelectChange = ( + recordToSelect: SelectableRecord, + newSelectedValue: boolean, + ) => { + const newSelectedRecordIds = newSelectedValue + ? [...objectFilterDropdownSelectedRecordIds, recordToSelect.id] + : objectFilterDropdownSelectedRecordIds.filter( + (id) => id !== recordToSelect.id, + ); + + setObjectFilterDropdownSelectedRecordIds(newSelectedRecordIds); + + const selectedRecordNames = [ + ...recordsToSelect, + ...selectedRecords, + ...filteredSelectedRecords, + ] + .filter( + (record, index, self) => + self.findIndex((r) => r.id === record.id) === index, + ) + .filter((record) => newSelectedRecordIds.includes(record.id)) + .map((record) => record.name); + + const filterDisplayValue = + selectedRecordNames.length > MAX_RECORDS_TO_DISPLAY + ? `${selectedRecordNames.length} companies` + : selectedRecordNames.join(', '); + + if (filterDefinitionUsedInDropdown && selectedOperandInDropdown) { + const newFilterValue = + newSelectedRecordIds.length > 0 + ? JSON.stringify(newSelectedRecordIds) + : EMPTY_FILTER_VALUE; + + selectFilter({ + definition: filterDefinitionUsedInDropdown, + operand: selectedOperandInDropdown, + displayValue: filterDisplayValue, + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value: newFilterValue, + }); + } + }; + + return ( + + ); +}; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index 3d10be112..15ccfff0a 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -12,8 +12,8 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; import { GenericEntityFilterChip } from './GenericEntityFilterChip'; -import { ObjectFilterDropdownEntitySearchInput } from './ObjectFilterDropdownEntitySearchInput'; -import { ObjectFilterDropdownEntitySelect } from './ObjectFilterDropdownEntitySelect'; +import { ObjectFilterDropdownRecordSearchInput } from './ObjectFilterDropdownEntitySearchInput'; +import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; export const SingleEntityObjectFilterDropdownButton = ({ hotkeyScope, @@ -65,8 +65,8 @@ export const SingleEntityObjectFilterDropdownButton = ({ } dropdownComponents={ <> - - + + } /> diff --git a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdown.ts b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdown.ts index 07cb65569..eca5f810f 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -25,6 +25,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setObjectFilterDropdownSearchInput, objectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId, + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownUnfolded, @@ -48,6 +50,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { const resetFilter = useCallback(() => { setObjectFilterDropdownSearchInput(''); setObjectFilterDropdownSelectedEntityId(null); + setObjectFilterDropdownSelectedRecordIds([]); setSelectedFilter(undefined); setFilterDefinitionUsedInDropdown(null); setSelectedOperandInDropdown(null); @@ -55,6 +58,7 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setFilterDefinitionUsedInDropdown, setObjectFilterDropdownSearchInput, setObjectFilterDropdownSelectedEntityId, + setObjectFilterDropdownSelectedRecordIds, setSelectedFilter, setSelectedOperandInDropdown, ]); @@ -69,6 +73,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { setObjectFilterDropdownSearchInput, objectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId, + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownUnfolded, diff --git a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdownStates.ts b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdownStates.ts index 37acb6428..b9b156890 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdownStates.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/hooks/useFilterDropdownStates.ts @@ -1,3 +1,4 @@ +import { objectFilterDropdownSelectedRecordIdsScopedState } from '@/ui/object/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState'; import { onFilterSelectScopedState } from '@/ui/object/object-filter-dropdown/states/onFilterSelectScopedState'; import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2'; @@ -28,6 +29,14 @@ export const useFilterDropdownStates = (scopeId: string) => { scopeId, ); + const [ + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, + ] = useRecoilScopedStateV2( + objectFilterDropdownSelectedRecordIdsScopedState, + scopeId, + ); + const [ isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, @@ -61,6 +70,8 @@ export const useFilterDropdownStates = (scopeId: string) => { setObjectFilterDropdownSearchInput, objectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId, + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownUnfolded, diff --git a/front/src/modules/ui/object/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts b/front/src/modules/ui/object/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts new file mode 100644 index 000000000..70d897971 --- /dev/null +++ b/front/src/modules/ui/object/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts @@ -0,0 +1,7 @@ +import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState'; + +export const objectFilterDropdownSelectedRecordIdsScopedState = + createScopedState({ + key: 'objectFilterDropdownSelectedRecordIdsScopedState', + defaultValue: [], + }); diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause.ts index 1f471b70c..c320afa77 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClause.ts @@ -94,26 +94,41 @@ export const turnFiltersIntoWhereClause = ( ); } case 'RELATION': - switch (filter.operand) { - case ViewFilterOperand.Is: - whereClause.push({ - [correspondingField.name + 'Id']: { - eq: filter.value, - }, - }); - return; - case ViewFilterOperand.IsNot: - whereClause.push({ - [correspondingField.name + 'Id']: { - neq: filter.value, - }, - }); - return; - default: - throw new Error( - `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, - ); + try { + JSON.parse(filter.value); + } catch (e) { + throw new Error( + `Cannot parse filter value for RELATION filter : "${filter.value}"`, + ); } + + const parsedRecordIds = JSON.parse(filter.value) as string[]; + + if (parsedRecordIds.length > 0) { + switch (filter.operand) { + case ViewFilterOperand.Is: + whereClause.push({ + [correspondingField.name + 'Id']: { + in: parsedRecordIds, + }, + }); + return; + case ViewFilterOperand.IsNot: + whereClause.push({ + not: { + [correspondingField.name + 'Id']: { + in: parsedRecordIds, + }, + }, + }); + return; + default: + throw new Error( + `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, + ); + } + } + break; case 'CURRENCY': switch (filter.operand) { case ViewFilterOperand.GreaterThan: diff --git a/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx b/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx index e92e1d109..764b820e2 100644 --- a/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx +++ b/front/src/modules/ui/object/record-table/components/RecordTableBodyEffect.tsx @@ -11,6 +11,7 @@ export const RecordTableBodyEffect = () => { records, setRecordTableData, queryStateIdentifier, + loading, } = useObjectRecordTable(); const { tableLastRowVisibleState } = useRecordTableScopedStates(); const [tableLastRowVisible, setTableLastRowVisible] = useRecoilState( @@ -22,8 +23,10 @@ export const RecordTableBodyEffect = () => { ); useEffect(() => { - setRecordTableData(records); - }, [records, setRecordTableData]); + if (!loading) { + setRecordTableData(records); + } + }, [records, setRecordTableData, loading]); useEffect(() => { if (tableLastRowVisible && !isFetchingMoreObjects) { diff --git a/front/src/modules/views/components/ViewBarFilterEffect.tsx b/front/src/modules/views/components/ViewBarFilterEffect.tsx index 32600fad2..8b1424586 100644 --- a/front/src/modules/views/components/ViewBarFilterEffect.tsx +++ b/front/src/modules/views/components/ViewBarFilterEffect.tsx @@ -14,13 +14,19 @@ export const ViewBarFilterEffect = ({ filterDropdownId, onFilterSelect, }: ViewBarFilterEffectProps) => { - const { availableFilterDefinitionsState } = useViewScopedStates(); + const { availableFilterDefinitionsState, currentViewFiltersState } = + useViewScopedStates(); const availableFilterDefinitions = useRecoilValue( availableFilterDefinitionsState, ); - const { setAvailableFilterDefinitions, setOnFilterSelect } = - useFilterDropdown({ filterDropdownId: filterDropdownId }); + const { + setAvailableFilterDefinitions, + setOnFilterSelect, + filterDefinitionUsedInDropdown, + setObjectFilterDropdownSelectedRecordIds, + isObjectFilterDropdownUnfolded, + } = useFilterDropdown({ filterDropdownId: filterDropdownId }); useEffect(() => { if (availableFilterDefinitions) { @@ -37,5 +43,28 @@ export const ViewBarFilterEffect = ({ setOnFilterSelect, ]); + const currentViewFilters = useRecoilValue(currentViewFiltersState); + + useEffect(() => { + if (filterDefinitionUsedInDropdown?.type === 'RELATION') { + const viewFilterUsedInDropdown = currentViewFilters.find( + (filter) => + filter.fieldMetadataId === + filterDefinitionUsedInDropdown.fieldMetadataId, + ); + + const viewFilterSelectedRecordIds = JSON.parse( + viewFilterUsedInDropdown?.value ?? '[]', + ); + + setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds); + } + }, [ + filterDefinitionUsedInDropdown, + currentViewFilters, + setObjectFilterDropdownSelectedRecordIds, + isObjectFilterDropdownUnfolded, + ]); + return <>; };