Relation picker module (#335)
- Created a relation picker module - Added a CustomeEntityForSelect type
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user