Feat/multi relation filter (#2858)

* WIP

* Finished multi select filter

* Cleaned console log

* Fix naming

* Fixed naming
This commit is contained in:
Lucas Bordeau
2023-12-07 12:08:48 +01:00
committed by GitHub
parent b2912f4b4b
commit 06936c3c2a
18 changed files with 516 additions and 93 deletions
@@ -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 = () => {
<ObjectFilterDropdownDateSearchInput />
)}
{filterDefinitionUsedInDropdown.type === 'RELATION' && (
<ObjectFilterDropdownEntitySearchInput />
)}
{filterDefinitionUsedInDropdown.type === 'RELATION' && (
<ObjectFilterDropdownEntitySelect />
<>
<ObjectFilterDropdownRecordSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect />
</>
)}
</>
)
@@ -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,
@@ -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 (
<>
<ObjectFilterDropdownEntitySearchSelect
entitiesForSelect={filteredSearchEntityResults}
/>
</>
);
};
@@ -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 (
<MultipleRecordSelectDropdown
recordsToSelect={recordsToSelect}
filteredSelectedRecords={filteredSelectedRecords}
selectedRecords={selectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingRecords={loading}
/>
);
};
@@ -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={
<>
<ObjectFilterDropdownEntitySearchInput />
<ObjectFilterDropdownEntitySelect />
<ObjectFilterDropdownRecordSearchInput />
<ObjectFilterDropdownRecordSelect />
</>
}
/>
@@ -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,
@@ -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,
@@ -0,0 +1,7 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const objectFilterDropdownSelectedRecordIdsScopedState =
createScopedState<string[]>({
key: 'objectFilterDropdownSelectedRecordIdsScopedState',
defaultValue: [],
});
@@ -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:
@@ -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) {