Relation picker module (#335)

- Created a relation picker module
- Added a CustomeEntityForSelect type
This commit is contained in:
Lucas Bordeau
2023-06-20 11:06:53 +02:00
committed by GitHub
parent c120903a45
commit e2eb40c1ea
9 changed files with 64 additions and 50 deletions
@@ -0,0 +1,86 @@
import { debounce } from 'lodash';
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
import { DropdownMenu } from '@/ui/components/menu/DropdownMenu';
import { DropdownMenuCheckableItem } from '@/ui/components/menu/DropdownMenuCheckableItem';
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
import { DropdownMenuSearch } from '@/ui/components/menu/DropdownMenuSearch';
import { DropdownMenuSeparator } from '@/ui/components/menu/DropdownMenuSeparator';
import { Avatar } from '@/users/components/Avatar';
export type EntitiesForMultipleEntitySelect<
CustomEntityForSelect extends EntityForSelect,
> = {
selectedEntities: CustomEntityForSelect[];
filteredSelectedEntities: CustomEntityForSelect[];
entitiesToSelect: CustomEntityForSelect[];
};
export function MultipleEntitySelect<
CustomEntityForSelect extends EntityForSelect,
>({
entities,
onItemCheckChange,
onSearchFilterChange,
searchFilter,
}: {
entities: EntitiesForMultipleEntitySelect<CustomEntityForSelect>;
searchFilter: string;
onSearchFilterChange: (newSearchFilter: string) => void;
onItemCheckChange: (
newCheckedValue: boolean,
entity: CustomEntityForSelect,
) => void;
}) {
const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, {
leading: true,
});
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
debouncedSetSearchFilter(event.currentTarget.value);
onSearchFilterChange(event.currentTarget.value);
}
const entitiesInDropdown = [
...(entities.filteredSelectedEntities ?? []),
...(entities.entitiesToSelect ?? []),
];
return (
<DropdownMenu>
<DropdownMenuSearch
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemContainer>
{entitiesInDropdown?.map((entity) => (
<DropdownMenuCheckableItem
key={entity.id}
checked={
entities.selectedEntities
?.map((selectedEntity) => selectedEntity.id)
?.includes(entity.id) ?? false
}
onChange={(newCheckedValue) =>
onItemCheckChange(newCheckedValue, entity)
}
>
<Avatar
avatarUrl={entity.avatarUrl}
placeholder={entity.name}
size={16}
type={entity.avatarType ?? 'rounded'}
/>
{entity.name}
</DropdownMenuCheckableItem>
))}
{entitiesInDropdown?.length === 0 && (
<DropdownMenuItem>No result</DropdownMenuItem>
)}
</DropdownMenuItemContainer>
</DropdownMenu>
);
}
@@ -0,0 +1,144 @@
import * as Apollo from '@apollo/client';
import { EntitiesForMultipleEntitySelect } from '@/relation-picker/components/MultipleEntitySelect';
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
import {
Exact,
InputMaybe,
QueryMode,
Scalars,
SortOrder,
} from '~/generated/graphql';
type SelectStringKeys<T> = NonNullable<
{
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T]
>;
type ExtractEntityTypeFromQueryResponse<T> = T extends {
searchResults: Array<infer U>;
}
? U
: never;
const DEFAULT_SEARCH_REQUEST_LIMIT = 10;
export function useFilteredSearchEntityQuery<
EntityType extends ExtractEntityTypeFromQueryResponse<QueryResponseForExtract> & {
id: string;
},
EntityStringField extends SelectStringKeys<EntityType>,
OrderByField extends EntityStringField,
SearchOnField extends EntityStringField,
QueryResponseForExtract,
QueryResponse extends {
searchResults: EntityType[];
},
EntityWhereInput,
EntityOrderByWithRelationInput,
QueryVariables extends Exact<{
where?: InputMaybe<EntityWhereInput>;
limit?: InputMaybe<Scalars['Int']>;
orderBy?: InputMaybe<
Array<EntityOrderByWithRelationInput> | EntityOrderByWithRelationInput
>;
}>,
CustomEntityForSelect extends EntityForSelect,
>({
queryHook,
searchOnFields,
orderByField,
sortOrder = SortOrder.Asc,
selectedIds,
mappingFunction,
limit,
searchFilter, // TODO: put in a scoped recoil state
}: {
queryHook: (
queryOptions?: Apollo.QueryHookOptions<
QueryResponseForExtract,
QueryVariables
>,
) => Apollo.QueryResult<QueryResponse, QueryVariables>;
searchOnFields: SearchOnField[];
orderByField: OrderByField;
sortOrder?: SortOrder;
selectedIds: string[];
mappingFunction: (entity: EntityType) => CustomEntityForSelect;
limit?: number;
searchFilter: string;
}): EntitiesForMultipleEntitySelect<CustomEntityForSelect> {
const { data: selectedEntitiesData } = queryHook({
variables: {
where: {
id: {
in: selectedIds,
},
},
orderBy: {
[orderByField]: sortOrder,
},
} as QueryVariables,
});
const searchFilterByField = searchOnFields.map((field) => ({
[field]: {
contains: `%${searchFilter}%`,
mode: QueryMode.Insensitive,
},
}));
const { data: filteredSelectedEntitiesData } = queryHook({
variables: {
where: {
AND: [
{
OR: searchFilterByField,
},
{
id: {
in: selectedIds,
},
},
],
},
orderBy: {
[orderByField]: sortOrder,
},
} as QueryVariables,
});
const { data: entitiesToSelectData } = queryHook({
variables: {
where: {
AND: [
{
OR: searchFilterByField,
},
{
id: {
notIn: selectedIds,
},
},
],
},
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: {
[orderByField]: sortOrder,
},
} as QueryVariables,
});
return {
selectedEntities: (selectedEntitiesData?.searchResults ?? []).map(
mappingFunction,
),
filteredSelectedEntities: (
filteredSelectedEntitiesData?.searchResults ?? []
).map(mappingFunction),
entitiesToSelect: (entitiesToSelectData?.searchResults ?? []).map(
mappingFunction,
),
};
}
@@ -0,0 +1,11 @@
import { AvatarType } from '@/users/components/Avatar';
import { EntityTypeForSelect } from './EntityTypeForSelect';
export type EntityForSelect = {
id: string;
entityType: EntityTypeForSelect;
name: string;
avatarUrl?: string;
avatarType?: AvatarType;
};
@@ -0,0 +1,3 @@
import { CommentableType, PipelineProgressableType } from '~/generated/graphql';
export type EntityTypeForSelect = CommentableType | PipelineProgressableType;