Feat/multi relation filter (#2858)
* WIP * Finished multi select filter * Cleaned console log * Fix naming * Fixed naming
This commit is contained in:
@ -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 (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{recordsInDropdown?.map((record) => (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={record.id}
|
||||
selected={record.isSelected}
|
||||
onSelectChange={(newCheckedValue) =>
|
||||
handleRecordSelectChange(record, newCheckedValue)
|
||||
}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={record.avatarUrl}
|
||||
colorId={record.id}
|
||||
placeholder={record.name}
|
||||
size="md"
|
||||
type={record.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={record.name}
|
||||
/>
|
||||
))}
|
||||
{showNoResult && <MenuItem text="No result" />}
|
||||
{loadingRecords && <DropdownMenuSkeletonItem />}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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'];
|
||||
};
|
||||
@ -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';
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { AvatarType } from '@/users/components/Avatar';
|
||||
|
||||
export type ObjectRecordIdentifier = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: AvatarType;
|
||||
};
|
||||
Reference in New Issue
Block a user