306 implement multi relation picker for person and try to factorize relation picker (#319)
* Removed useless folder * First working version * Refactored MultipleEntitySelect and splitted into 2 components * Added TODO * Removed useless Query * Fixed refetch * Fixed naming * Fix tests --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,235 @@
|
||||
import { useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { IconArrowUpRight } from '@tabler/icons-react';
|
||||
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import CompanyChip from '@/companies/components/CompanyChip';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { useFilteredSearchEntityQuery } from '@/ui/hooks/menu/useFilteredSearchEntityQuery';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
CommentableType,
|
||||
useSearchCompanyQuery,
|
||||
useSearchPeopleQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange';
|
||||
|
||||
import { MultipleEntitySelect } from './MultipleEntitySelect';
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: CommentThreadForDrawer;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
|
||||
padding-bottom: ${(props) => props.theme.spacing(2)};
|
||||
padding-top: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRelationLabel = styled.div`
|
||||
color: ${(props) => props.theme.text60};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledRelationContainer = styled.div`
|
||||
--horizontal-padding: ${(props) => props.theme.spacing(1)};
|
||||
--vertical-padding: ${(props) => props.theme.spacing(1.5)};
|
||||
|
||||
border: 1px solid transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.secondaryBackground};
|
||||
border: 1px solid ${(props) => props.theme.lightBorder};
|
||||
}
|
||||
|
||||
min-height: calc(32px - 2 * var(--vertical-padding));
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||
width: calc(100% - 2 * var(--horizontal-padding));
|
||||
`;
|
||||
|
||||
const StyledMenuWrapper = styled.div`
|
||||
z-index: ${(props) => props.theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const peopleIds =
|
||||
commentThread.commentThreadTargets
|
||||
?.filter((relation) => relation.commentableType === 'Person')
|
||||
.map((relation) => relation.commentableId) ?? [];
|
||||
|
||||
const companyIds =
|
||||
commentThread.commentThreadTargets
|
||||
?.filter((relation) => relation.commentableType === 'Company')
|
||||
.map((relation) => relation.commentableId) ?? [];
|
||||
|
||||
const personsForMultiSelect = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchPeopleQuery,
|
||||
searchOnFields: ['firstname', 'lastname'],
|
||||
orderByField: 'lastname',
|
||||
selectedIds: peopleIds,
|
||||
mappingFunction: (entity) => ({
|
||||
id: entity.id,
|
||||
entityType: CommentableType.Person,
|
||||
name: `${entity.firstname} ${entity.lastname}`,
|
||||
avatarType: 'rounded',
|
||||
}),
|
||||
searchFilter,
|
||||
});
|
||||
|
||||
const companiesForMultiSelect = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchCompanyQuery,
|
||||
searchOnFields: ['name'],
|
||||
orderByField: 'name',
|
||||
selectedIds: companyIds,
|
||||
mappingFunction: (company) => ({
|
||||
id: company.id,
|
||||
entityType: CommentableType.Company,
|
||||
name: company.name,
|
||||
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
||||
avatarType: 'squared',
|
||||
}),
|
||||
searchFilter,
|
||||
});
|
||||
|
||||
function handleRelationContainerClick() {
|
||||
setIsMenuOpen((isOpen) => !isOpen);
|
||||
}
|
||||
|
||||
// TODO: Place in a scoped recoil atom family
|
||||
function handleFilterChange(newSearchFilter: string) {
|
||||
setSearchFilter(newSearchFilter);
|
||||
}
|
||||
|
||||
const handleCheckItemChange = useHandleCheckableCommentThreadTargetChange({
|
||||
commentThread,
|
||||
});
|
||||
|
||||
function exitEditMode() {
|
||||
setIsMenuOpen(false);
|
||||
setSearchFilter('');
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
['esc', 'enter'],
|
||||
() => {
|
||||
exitEditMode();
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[exitEditMode],
|
||||
);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
strategy: 'absolute',
|
||||
middleware: [offset(), flip(), size()],
|
||||
whileElementsMounted: autoUpdate,
|
||||
open: isMenuOpen,
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
useListenClickOutsideArrayOfRef([refs.floating, refs.domReference], () => {
|
||||
exitEditMode();
|
||||
});
|
||||
|
||||
const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([
|
||||
personsForMultiSelect.selectedEntities,
|
||||
companiesForMultiSelect.selectedEntities,
|
||||
]);
|
||||
|
||||
const filteredSelectedEntities =
|
||||
flatMapAndSortEntityForSelectArrayOfArrayByName([
|
||||
personsForMultiSelect.filteredSelectedEntities,
|
||||
companiesForMultiSelect.filteredSelectedEntities,
|
||||
]);
|
||||
|
||||
const entitiesToSelect = flatMapAndSortEntityForSelectArrayOfArrayByName([
|
||||
personsForMultiSelect.entitiesToSelect,
|
||||
companiesForMultiSelect.entitiesToSelect,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabelContainer>
|
||||
<IconArrowUpRight size={16} color={theme.text40} />
|
||||
<StyledRelationLabel>Relations</StyledRelationLabel>
|
||||
</StyledLabelContainer>
|
||||
<StyledRelationContainer
|
||||
ref={refs.setReference}
|
||||
onClick={handleRelationContainerClick}
|
||||
>
|
||||
{selectedEntities?.map((entity) =>
|
||||
entity.entityType === CommentableType.Company ? (
|
||||
<CompanyChip
|
||||
key={entity.id}
|
||||
name={entity.name}
|
||||
picture={entity.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonChip key={entity.id} name={entity.name} />
|
||||
),
|
||||
)}
|
||||
</StyledRelationContainer>
|
||||
{isMenuOpen && (
|
||||
<StyledMenuWrapper ref={refs.setFloating} style={floatingStyles}>
|
||||
<MultipleEntitySelect
|
||||
entities={{
|
||||
entitiesToSelect,
|
||||
filteredSelectedEntities,
|
||||
selectedEntities,
|
||||
}}
|
||||
onItemCheckChange={handleCheckItemChange}
|
||||
onSearchFilterChange={handleFilterChange}
|
||||
searchFilter={searchFilter}
|
||||
/>
|
||||
</StyledMenuWrapper>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
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, AvatarType } from '@/users/components/Avatar';
|
||||
import { CommentableType } from '~/generated/graphql';
|
||||
|
||||
export type EntitiesForMultipleEntitySelect = {
|
||||
selectedEntities: EntityForSelect[];
|
||||
filteredSelectedEntities: EntityForSelect[];
|
||||
entitiesToSelect: EntityForSelect[];
|
||||
};
|
||||
|
||||
export type EntityTypeForSelect = CommentableType; // TODO: derivate from all usable entity types
|
||||
|
||||
export type EntityForSelect = {
|
||||
id: string;
|
||||
entityType: EntityTypeForSelect;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: AvatarType;
|
||||
};
|
||||
|
||||
export function MultipleEntitySelect({
|
||||
entities,
|
||||
onItemCheckChange,
|
||||
onSearchFilterChange,
|
||||
searchFilter,
|
||||
}: {
|
||||
entities: EntitiesForMultipleEntitySelect;
|
||||
searchFilter: string;
|
||||
onSearchFilterChange: (newSearchFilter: string) => void;
|
||||
onItemCheckChange: (
|
||||
newCheckedValue: boolean,
|
||||
entity: EntityForSelect,
|
||||
) => 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} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -9,7 +9,7 @@ import {
|
||||
useGetCommentThreadsByTargetsQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { commentableEntityArrayState } from '../../states/commentableEntityArrayState';
|
||||
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
|
||||
|
||||
import { CommentThread } from './CommentThread';
|
||||
|
||||
@ -1,308 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { debounce } from 'lodash';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import CompanyChip from '@/companies/components/CompanyChip';
|
||||
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 { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { IconArrowUpRight } from '@/ui/icons';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
CommentableType,
|
||||
QueryMode,
|
||||
SortOrder,
|
||||
useAddCommentThreadTargetOnCommentThreadMutation,
|
||||
useRemoveCommentThreadTargetOnCommentThreadMutation,
|
||||
useSearchCompanyQueryQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: CommentThreadForDrawer;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
|
||||
padding-bottom: ${(props) => props.theme.spacing(2)};
|
||||
padding-top: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRelationLabel = styled.div`
|
||||
color: ${(props) => props.theme.text60};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledRelationContainer = styled.div`
|
||||
--horizontal-padding: ${(props) => props.theme.spacing(1)};
|
||||
--vertical-padding: ${(props) => props.theme.spacing(1.5)};
|
||||
|
||||
border: 1px solid transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: ${(props) => props.theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.secondaryBackground};
|
||||
border: 1px solid ${(props) => props.theme.lightBorder};
|
||||
}
|
||||
|
||||
min-height: calc(32px - 2 * var(--vertical-padding));
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
padding: var(--vertical-padding) var(--horizontal-padding);
|
||||
width: calc(100% - 2 * var(--horizontal-padding));
|
||||
`;
|
||||
|
||||
export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const debouncedSetSearchFilter = debounce(setSearchFilter, 100, {
|
||||
leading: true,
|
||||
});
|
||||
|
||||
function exitEditMode() {
|
||||
setIsMenuOpen(false);
|
||||
setSearchFilter('');
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
['esc', 'enter'],
|
||||
() => {
|
||||
exitEditMode();
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[exitEditMode],
|
||||
);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
strategy: 'absolute',
|
||||
middleware: [offset(), flip(), size()],
|
||||
whileElementsMounted: autoUpdate,
|
||||
open: isMenuOpen,
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
useListenClickOutsideArrayOfRef([refs.floating, refs.domReference], () => {
|
||||
exitEditMode();
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const companyIds = commentThread.commentThreadTargets
|
||||
?.filter((relation) => relation.commentableType === 'Company')
|
||||
.map((relation) => relation.commentableId);
|
||||
|
||||
const { data: selectedCompaniesData } = useSearchCompanyQueryQuery({
|
||||
variables: {
|
||||
where: {
|
||||
id: {
|
||||
in: companyIds,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
name: SortOrder.Asc,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data: filteredSelectedCompaniesData } = useSearchCompanyQueryQuery({
|
||||
variables: {
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
name: {
|
||||
contains: `%${searchFilter}%`,
|
||||
mode: QueryMode.Insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: companyIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: {
|
||||
name: SortOrder.Asc,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { data: companiesToSelectData } = useSearchCompanyQueryQuery({
|
||||
variables: {
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
name: {
|
||||
contains: `%${searchFilter}%`,
|
||||
mode: QueryMode.Insensitive,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
notIn: companyIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: 10,
|
||||
orderBy: {
|
||||
name: SortOrder.Asc,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
}
|
||||
|
||||
function handleChangeRelationsClick() {
|
||||
setIsMenuOpen((isOpen) => !isOpen);
|
||||
}
|
||||
|
||||
const [addCommentThreadTargetOnCommentThread] =
|
||||
useAddCommentThreadTargetOnCommentThreadMutation({
|
||||
refetchQueries: ['GetCompanies'],
|
||||
});
|
||||
|
||||
const [removeCommentThreadTargetOnCommentThread] =
|
||||
useRemoveCommentThreadTargetOnCommentThreadMutation({
|
||||
refetchQueries: ['GetCompanies'],
|
||||
});
|
||||
|
||||
function handleCheckItemChange(newCheckedValue: boolean, itemId: string) {
|
||||
if (newCheckedValue) {
|
||||
addCommentThreadTargetOnCommentThread({
|
||||
variables: {
|
||||
commentableEntityId: itemId,
|
||||
commentableEntityType: CommentableType.Company,
|
||||
commentThreadId: commentThread.id,
|
||||
commentThreadTargetCreationDate: new Date().toISOString(),
|
||||
commentThreadTargetId: v4(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const foundCorrespondingTarget = commentThread.commentThreadTargets?.find(
|
||||
(target) => target.commentableId === itemId,
|
||||
);
|
||||
|
||||
if (foundCorrespondingTarget) {
|
||||
removeCommentThreadTargetOnCommentThread({
|
||||
variables: {
|
||||
commentThreadId: commentThread.id,
|
||||
commentThreadTargetId: foundCorrespondingTarget.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCompanies = selectedCompaniesData?.searchResults ?? [];
|
||||
|
||||
const filteredSelectedCompanies =
|
||||
filteredSelectedCompaniesData?.searchResults ?? [];
|
||||
const companiesToSelect = companiesToSelectData?.searchResults ?? [];
|
||||
|
||||
const companiesInDropdown = [
|
||||
...filteredSelectedCompanies,
|
||||
...companiesToSelect,
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabelContainer>
|
||||
<IconArrowUpRight size={16} color={theme.text40} />
|
||||
<StyledRelationLabel>Relations</StyledRelationLabel>
|
||||
</StyledLabelContainer>
|
||||
<StyledRelationContainer
|
||||
ref={refs.setReference}
|
||||
onClick={handleChangeRelationsClick}
|
||||
>
|
||||
{selectedCompanies?.map((company) => (
|
||||
<CompanyChip
|
||||
key={company.id}
|
||||
name={company.name}
|
||||
picture={getLogoUrlFromDomainName(company.domainName)}
|
||||
/>
|
||||
))}
|
||||
</StyledRelationContainer>
|
||||
{isMenuOpen && (
|
||||
<DropdownMenu ref={refs.setFloating} style={floatingStyles}>
|
||||
<DropdownMenuSearch
|
||||
value={searchFilter}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemContainer>
|
||||
{companiesInDropdown?.map((company) => (
|
||||
<DropdownMenuCheckableItem
|
||||
key={company.id}
|
||||
checked={
|
||||
selectedCompanies
|
||||
?.map((selectedCompany) => selectedCompany.id)
|
||||
?.includes(company.id) ?? false
|
||||
}
|
||||
onChange={(newCheckedValue) =>
|
||||
handleCheckItemChange(newCheckedValue, company.id)
|
||||
}
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={getLogoUrlFromDomainName(company.domainName)}
|
||||
placeholder={company.name}
|
||||
size={16}
|
||||
/>
|
||||
{company.name}
|
||||
</DropdownMenuCheckableItem>
|
||||
))}
|
||||
{companiesInDropdown?.length === 0 && (
|
||||
<DropdownMenuItem>No result</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
useAddCommentThreadTargetOnCommentThreadMutation,
|
||||
useRemoveCommentThreadTargetOnCommentThreadMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { EntityForSelect } from '../components/MultipleEntitySelect';
|
||||
import { CommentThreadForDrawer } from '../types/CommentThreadForDrawer';
|
||||
|
||||
export function useHandleCheckableCommentThreadTargetChange({
|
||||
commentThread,
|
||||
}: {
|
||||
commentThread: CommentThreadForDrawer;
|
||||
}) {
|
||||
const [addCommentThreadTargetOnCommentThread] =
|
||||
useAddCommentThreadTargetOnCommentThreadMutation({
|
||||
refetchQueries: ['GetCompanies', 'GetPeople'],
|
||||
});
|
||||
|
||||
const [removeCommentThreadTargetOnCommentThread] =
|
||||
useRemoveCommentThreadTargetOnCommentThreadMutation({
|
||||
refetchQueries: ['GetCompanies', 'GetPeople'],
|
||||
});
|
||||
|
||||
return function handleCheckItemChange(
|
||||
newCheckedValue: boolean,
|
||||
entity: EntityForSelect,
|
||||
) {
|
||||
if (newCheckedValue) {
|
||||
addCommentThreadTargetOnCommentThread({
|
||||
variables: {
|
||||
commentableEntityId: entity.id,
|
||||
commentableEntityType: entity.entityType,
|
||||
commentThreadId: commentThread.id,
|
||||
commentThreadTargetCreationDate: new Date().toISOString(),
|
||||
commentThreadTargetId: v4(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const foundCorrespondingTarget = commentThread.commentThreadTargets?.find(
|
||||
(target) => target.commentableId === entity.id,
|
||||
);
|
||||
|
||||
if (foundCorrespondingTarget) {
|
||||
removeCommentThreadTargetOnCommentThread({
|
||||
variables: {
|
||||
commentThreadId: commentThread.id,
|
||||
commentThreadTargetId: foundCorrespondingTarget.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
|
||||
import { CellCommentChip } from '@/comments/components/CellCommentChip';
|
||||
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
|
||||
import EditableChip from '@/ui/components/editable-cell/types/EditableChip';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
|
||||
import { CellCommentChip } from '@/comments/components/CellCommentChip';
|
||||
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
|
||||
import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText';
|
||||
import { CommentableType } from '~/generated/graphql';
|
||||
|
||||
@ -7,8 +7,16 @@ import { AnyEntity, UnknownType } from '@/utils/interfaces/generic.interface';
|
||||
import { SearchConfigType } from '../interfaces/interface';
|
||||
|
||||
export const SEARCH_PEOPLE_QUERY = gql`
|
||||
query SearchPeopleQuery($where: PersonWhereInput, $limit: Int) {
|
||||
searchResults: findManyPerson(where: $where, take: $limit) {
|
||||
query SearchPeople(
|
||||
$where: PersonWhereInput
|
||||
$limit: Int
|
||||
$orderBy: [PersonOrderByWithRelationInput!]
|
||||
) {
|
||||
searchResults: findManyPerson(
|
||||
where: $where
|
||||
take: $limit
|
||||
orderBy: $orderBy
|
||||
) {
|
||||
id
|
||||
phone
|
||||
email
|
||||
@ -21,7 +29,7 @@ export const SEARCH_PEOPLE_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const SEARCH_USER_QUERY = gql`
|
||||
query SearchUserQuery($where: UserWhereInput, $limit: Int) {
|
||||
query SearchUser($where: UserWhereInput, $limit: Int) {
|
||||
searchResults: findManyUser(where: $where, take: $limit) {
|
||||
id
|
||||
email
|
||||
@ -39,7 +47,7 @@ export const EMPTY_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const SEARCH_COMPANY_QUERY = gql`
|
||||
query SearchCompanyQuery(
|
||||
query SearchCompany(
|
||||
$where: CompanyWhereInput
|
||||
$limit: Int
|
||||
$orderBy: [CompanyOrderByWithRelationInput!]
|
||||
|
||||
@ -17,6 +17,4 @@ export const DropdownMenu = styled.div`
|
||||
height: fit-content;
|
||||
|
||||
width: 200px;
|
||||
|
||||
z-index: ${(props) => props.theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
145
front/src/modules/ui/hooks/menu/useFilteredSearchEntityQuery.ts
Normal file
145
front/src/modules/ui/hooks/menu/useFilteredSearchEntityQuery.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as Apollo from '@apollo/client';
|
||||
|
||||
import {
|
||||
EntitiesForMultipleEntitySelect,
|
||||
EntityForSelect,
|
||||
} from '@/comments/components/MultipleEntitySelect';
|
||||
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
|
||||
>;
|
||||
}>,
|
||||
>({
|
||||
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) => EntityForSelect;
|
||||
limit?: number;
|
||||
searchFilter: string;
|
||||
}): EntitiesForMultipleEntitySelect {
|
||||
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,
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { RightDrawerComments } from '@/comments/components/comments/RightDrawerComments';
|
||||
import { RightDrawerCreateCommentThread } from '@/comments/components/comments/RightDrawerCreateCommentThread';
|
||||
import { RightDrawerComments } from '@/comments/components/RightDrawerComments';
|
||||
import { RightDrawerCreateCommentThread } from '@/comments/components/RightDrawerCreateCommentThread';
|
||||
import { isDefined } from '@/utils/type-guards/isDefined';
|
||||
|
||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { EntityForSelect } from '@/comments/components/MultipleEntitySelect';
|
||||
|
||||
export function flatMapAndSortEntityForSelectArrayOfArrayByName(
|
||||
entityForSelectArray: EntityForSelect[][],
|
||||
) {
|
||||
const sortByName = (a: EntityForSelect, b: EntityForSelect) =>
|
||||
a.name.localeCompare(b.name);
|
||||
|
||||
return entityForSelectArray.flatMap((entity) => entity).sort(sortByName);
|
||||
}
|
||||
@ -2,11 +2,13 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
|
||||
export type AvatarType = 'squared' | 'rounded';
|
||||
|
||||
type OwnProps = {
|
||||
avatarUrl: string | null | undefined;
|
||||
size: number;
|
||||
placeholder: string;
|
||||
type?: 'squared' | 'rounded';
|
||||
type?: AvatarType;
|
||||
};
|
||||
|
||||
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
|
||||
|
||||
Reference in New Issue
Block a user