mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-29 23:34:50 +03:00
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:
parent
7f25f16766
commit
d13ceb98fa
@ -1675,35 +1675,36 @@ export type DeletePeopleMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type DeletePeopleMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } };
|
export type DeletePeopleMutation = { __typename?: 'Mutation', deleteManyPerson: { __typename?: 'AffectedRows', count: number } };
|
||||||
|
|
||||||
export type SearchPeopleQueryQueryVariables = Exact<{
|
export type SearchPeopleQueryVariables = Exact<{
|
||||||
where?: InputMaybe<PersonWhereInput>;
|
where?: InputMaybe<PersonWhereInput>;
|
||||||
limit?: InputMaybe<Scalars['Int']>;
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
|
orderBy?: InputMaybe<Array<PersonOrderByWithRelationInput> | PersonOrderByWithRelationInput>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SearchPeopleQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: string }> };
|
export type SearchPeopleQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: string }> };
|
||||||
|
|
||||||
export type SearchUserQueryQueryVariables = Exact<{
|
export type SearchUserQueryVariables = Exact<{
|
||||||
where?: InputMaybe<UserWhereInput>;
|
where?: InputMaybe<UserWhereInput>;
|
||||||
limit?: InputMaybe<Scalars['Int']>;
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SearchUserQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', id: string, email: string, displayName: string }> };
|
export type SearchUserQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', id: string, email: string, displayName: string }> };
|
||||||
|
|
||||||
export type EmptyQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
export type EmptyQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type EmptyQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', id: string }> };
|
export type EmptyQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', id: string }> };
|
||||||
|
|
||||||
export type SearchCompanyQueryQueryVariables = Exact<{
|
export type SearchCompanyQueryVariables = Exact<{
|
||||||
where?: InputMaybe<CompanyWhereInput>;
|
where?: InputMaybe<CompanyWhereInput>;
|
||||||
limit?: InputMaybe<Scalars['Int']>;
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SearchCompanyQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Company', id: string, name: string, domainName: string }> };
|
export type SearchCompanyQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Company', id: string, name: string, domainName: string }> };
|
||||||
|
|
||||||
export type GetCurrentUserQueryVariables = Exact<{
|
export type GetCurrentUserQueryVariables = Exact<{
|
||||||
uuid?: InputMaybe<Scalars['String']>;
|
uuid?: InputMaybe<Scalars['String']>;
|
||||||
@ -2433,9 +2434,9 @@ export function useDeletePeopleMutation(baseOptions?: Apollo.MutationHookOptions
|
|||||||
export type DeletePeopleMutationHookResult = ReturnType<typeof useDeletePeopleMutation>;
|
export type DeletePeopleMutationHookResult = ReturnType<typeof useDeletePeopleMutation>;
|
||||||
export type DeletePeopleMutationResult = Apollo.MutationResult<DeletePeopleMutation>;
|
export type DeletePeopleMutationResult = Apollo.MutationResult<DeletePeopleMutation>;
|
||||||
export type DeletePeopleMutationOptions = Apollo.BaseMutationOptions<DeletePeopleMutation, DeletePeopleMutationVariables>;
|
export type DeletePeopleMutationOptions = Apollo.BaseMutationOptions<DeletePeopleMutation, DeletePeopleMutationVariables>;
|
||||||
export const SearchPeopleQueryDocument = gql`
|
export const SearchPeopleDocument = gql`
|
||||||
query SearchPeopleQuery($where: PersonWhereInput, $limit: Int) {
|
query SearchPeople($where: PersonWhereInput, $limit: Int, $orderBy: [PersonOrderByWithRelationInput!]) {
|
||||||
searchResults: findManyPerson(where: $where, take: $limit) {
|
searchResults: findManyPerson(where: $where, take: $limit, orderBy: $orderBy) {
|
||||||
id
|
id
|
||||||
phone
|
phone
|
||||||
email
|
email
|
||||||
@ -2448,35 +2449,36 @@ export const SearchPeopleQueryDocument = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useSearchPeopleQueryQuery__
|
* __useSearchPeopleQuery__
|
||||||
*
|
*
|
||||||
* To run a query within a React component, call `useSearchPeopleQueryQuery` and pass it any options that fit your needs.
|
* To run a query within a React component, call `useSearchPeopleQuery` and pass it any options that fit your needs.
|
||||||
* When your component renders, `useSearchPeopleQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
* When your component renders, `useSearchPeopleQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
* you can use to render your UI.
|
* you can use to render your UI.
|
||||||
*
|
*
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useSearchPeopleQueryQuery({
|
* const { data, loading, error } = useSearchPeopleQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* where: // value for 'where'
|
* where: // value for 'where'
|
||||||
* limit: // value for 'limit'
|
* limit: // value for 'limit'
|
||||||
|
* orderBy: // value for 'orderBy'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useSearchPeopleQueryQuery(baseOptions?: Apollo.QueryHookOptions<SearchPeopleQueryQuery, SearchPeopleQueryQueryVariables>) {
|
export function useSearchPeopleQuery(baseOptions?: Apollo.QueryHookOptions<SearchPeopleQuery, SearchPeopleQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useQuery<SearchPeopleQueryQuery, SearchPeopleQueryQueryVariables>(SearchPeopleQueryDocument, options);
|
return Apollo.useQuery<SearchPeopleQuery, SearchPeopleQueryVariables>(SearchPeopleDocument, options);
|
||||||
}
|
}
|
||||||
export function useSearchPeopleQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchPeopleQueryQuery, SearchPeopleQueryQueryVariables>) {
|
export function useSearchPeopleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchPeopleQuery, SearchPeopleQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useLazyQuery<SearchPeopleQueryQuery, SearchPeopleQueryQueryVariables>(SearchPeopleQueryDocument, options);
|
return Apollo.useLazyQuery<SearchPeopleQuery, SearchPeopleQueryVariables>(SearchPeopleDocument, options);
|
||||||
}
|
}
|
||||||
export type SearchPeopleQueryQueryHookResult = ReturnType<typeof useSearchPeopleQueryQuery>;
|
export type SearchPeopleQueryHookResult = ReturnType<typeof useSearchPeopleQuery>;
|
||||||
export type SearchPeopleQueryLazyQueryHookResult = ReturnType<typeof useSearchPeopleQueryLazyQuery>;
|
export type SearchPeopleLazyQueryHookResult = ReturnType<typeof useSearchPeopleLazyQuery>;
|
||||||
export type SearchPeopleQueryQueryResult = Apollo.QueryResult<SearchPeopleQueryQuery, SearchPeopleQueryQueryVariables>;
|
export type SearchPeopleQueryResult = Apollo.QueryResult<SearchPeopleQuery, SearchPeopleQueryVariables>;
|
||||||
export const SearchUserQueryDocument = gql`
|
export const SearchUserDocument = gql`
|
||||||
query SearchUserQuery($where: UserWhereInput, $limit: Int) {
|
query SearchUser($where: UserWhereInput, $limit: Int) {
|
||||||
searchResults: findManyUser(where: $where, take: $limit) {
|
searchResults: findManyUser(where: $where, take: $limit) {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
@ -2486,33 +2488,33 @@ export const SearchUserQueryDocument = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useSearchUserQueryQuery__
|
* __useSearchUserQuery__
|
||||||
*
|
*
|
||||||
* To run a query within a React component, call `useSearchUserQueryQuery` and pass it any options that fit your needs.
|
* To run a query within a React component, call `useSearchUserQuery` and pass it any options that fit your needs.
|
||||||
* When your component renders, `useSearchUserQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
* When your component renders, `useSearchUserQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
* you can use to render your UI.
|
* you can use to render your UI.
|
||||||
*
|
*
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useSearchUserQueryQuery({
|
* const { data, loading, error } = useSearchUserQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* where: // value for 'where'
|
* where: // value for 'where'
|
||||||
* limit: // value for 'limit'
|
* limit: // value for 'limit'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useSearchUserQueryQuery(baseOptions?: Apollo.QueryHookOptions<SearchUserQueryQuery, SearchUserQueryQueryVariables>) {
|
export function useSearchUserQuery(baseOptions?: Apollo.QueryHookOptions<SearchUserQuery, SearchUserQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useQuery<SearchUserQueryQuery, SearchUserQueryQueryVariables>(SearchUserQueryDocument, options);
|
return Apollo.useQuery<SearchUserQuery, SearchUserQueryVariables>(SearchUserDocument, options);
|
||||||
}
|
}
|
||||||
export function useSearchUserQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchUserQueryQuery, SearchUserQueryQueryVariables>) {
|
export function useSearchUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchUserQuery, SearchUserQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useLazyQuery<SearchUserQueryQuery, SearchUserQueryQueryVariables>(SearchUserQueryDocument, options);
|
return Apollo.useLazyQuery<SearchUserQuery, SearchUserQueryVariables>(SearchUserDocument, options);
|
||||||
}
|
}
|
||||||
export type SearchUserQueryQueryHookResult = ReturnType<typeof useSearchUserQueryQuery>;
|
export type SearchUserQueryHookResult = ReturnType<typeof useSearchUserQuery>;
|
||||||
export type SearchUserQueryLazyQueryHookResult = ReturnType<typeof useSearchUserQueryLazyQuery>;
|
export type SearchUserLazyQueryHookResult = ReturnType<typeof useSearchUserLazyQuery>;
|
||||||
export type SearchUserQueryQueryResult = Apollo.QueryResult<SearchUserQueryQuery, SearchUserQueryQueryVariables>;
|
export type SearchUserQueryResult = Apollo.QueryResult<SearchUserQuery, SearchUserQueryVariables>;
|
||||||
export const EmptyQueryDocument = gql`
|
export const EmptyQueryDocument = gql`
|
||||||
query EmptyQuery {
|
query EmptyQuery {
|
||||||
searchResults: findManyUser {
|
searchResults: findManyUser {
|
||||||
@ -2547,8 +2549,8 @@ export function useEmptyQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
|||||||
export type EmptyQueryQueryHookResult = ReturnType<typeof useEmptyQueryQuery>;
|
export type EmptyQueryQueryHookResult = ReturnType<typeof useEmptyQueryQuery>;
|
||||||
export type EmptyQueryLazyQueryHookResult = ReturnType<typeof useEmptyQueryLazyQuery>;
|
export type EmptyQueryLazyQueryHookResult = ReturnType<typeof useEmptyQueryLazyQuery>;
|
||||||
export type EmptyQueryQueryResult = Apollo.QueryResult<EmptyQueryQuery, EmptyQueryQueryVariables>;
|
export type EmptyQueryQueryResult = Apollo.QueryResult<EmptyQueryQuery, EmptyQueryQueryVariables>;
|
||||||
export const SearchCompanyQueryDocument = gql`
|
export const SearchCompanyDocument = gql`
|
||||||
query SearchCompanyQuery($where: CompanyWhereInput, $limit: Int, $orderBy: [CompanyOrderByWithRelationInput!]) {
|
query SearchCompany($where: CompanyWhereInput, $limit: Int, $orderBy: [CompanyOrderByWithRelationInput!]) {
|
||||||
searchResults: findManyCompany(where: $where, take: $limit, orderBy: $orderBy) {
|
searchResults: findManyCompany(where: $where, take: $limit, orderBy: $orderBy) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -2558,16 +2560,16 @@ export const SearchCompanyQueryDocument = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __useSearchCompanyQueryQuery__
|
* __useSearchCompanyQuery__
|
||||||
*
|
*
|
||||||
* To run a query within a React component, call `useSearchCompanyQueryQuery` and pass it any options that fit your needs.
|
* To run a query within a React component, call `useSearchCompanyQuery` and pass it any options that fit your needs.
|
||||||
* When your component renders, `useSearchCompanyQueryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
* When your component renders, `useSearchCompanyQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
* you can use to render your UI.
|
* you can use to render your UI.
|
||||||
*
|
*
|
||||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const { data, loading, error } = useSearchCompanyQueryQuery({
|
* const { data, loading, error } = useSearchCompanyQuery({
|
||||||
* variables: {
|
* variables: {
|
||||||
* where: // value for 'where'
|
* where: // value for 'where'
|
||||||
* limit: // value for 'limit'
|
* limit: // value for 'limit'
|
||||||
@ -2575,17 +2577,17 @@ export const SearchCompanyQueryDocument = gql`
|
|||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function useSearchCompanyQueryQuery(baseOptions?: Apollo.QueryHookOptions<SearchCompanyQueryQuery, SearchCompanyQueryQueryVariables>) {
|
export function useSearchCompanyQuery(baseOptions?: Apollo.QueryHookOptions<SearchCompanyQuery, SearchCompanyQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useQuery<SearchCompanyQueryQuery, SearchCompanyQueryQueryVariables>(SearchCompanyQueryDocument, options);
|
return Apollo.useQuery<SearchCompanyQuery, SearchCompanyQueryVariables>(SearchCompanyDocument, options);
|
||||||
}
|
}
|
||||||
export function useSearchCompanyQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchCompanyQueryQuery, SearchCompanyQueryQueryVariables>) {
|
export function useSearchCompanyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchCompanyQuery, SearchCompanyQueryVariables>) {
|
||||||
const options = {...defaultOptions, ...baseOptions}
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
return Apollo.useLazyQuery<SearchCompanyQueryQuery, SearchCompanyQueryQueryVariables>(SearchCompanyQueryDocument, options);
|
return Apollo.useLazyQuery<SearchCompanyQuery, SearchCompanyQueryVariables>(SearchCompanyDocument, options);
|
||||||
}
|
}
|
||||||
export type SearchCompanyQueryQueryHookResult = ReturnType<typeof useSearchCompanyQueryQuery>;
|
export type SearchCompanyQueryHookResult = ReturnType<typeof useSearchCompanyQuery>;
|
||||||
export type SearchCompanyQueryLazyQueryHookResult = ReturnType<typeof useSearchCompanyQueryLazyQuery>;
|
export type SearchCompanyLazyQueryHookResult = ReturnType<typeof useSearchCompanyLazyQuery>;
|
||||||
export type SearchCompanyQueryQueryResult = Apollo.QueryResult<SearchCompanyQueryQuery, SearchCompanyQueryQueryVariables>;
|
export type SearchCompanyQueryResult = Apollo.QueryResult<SearchCompanyQuery, SearchCompanyQueryVariables>;
|
||||||
export const GetCurrentUserDocument = gql`
|
export const GetCurrentUserDocument = gql`
|
||||||
query GetCurrentUser($uuid: String) {
|
query GetCurrentUser($uuid: String) {
|
||||||
users: findManyUser(where: {id: {equals: $uuid}}) {
|
users: findManyUser(where: {id: {equals: $uuid}}) {
|
||||||
|
@ -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,
|
useGetCommentThreadsByTargetsQuery,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
import { commentableEntityArrayState } from '../../states/commentableEntityArrayState';
|
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
|
||||||
|
|
||||||
import { CommentThread } from './CommentThread';
|
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 { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
|
||||||
import EditableChip from '@/ui/components/editable-cell/types/EditableChip';
|
import EditableChip from '@/ui/components/editable-cell/types/EditableChip';
|
||||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
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 { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
|
||||||
import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText';
|
import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText';
|
||||||
import { CommentableType } from '~/generated/graphql';
|
import { CommentableType } from '~/generated/graphql';
|
||||||
|
@ -7,8 +7,16 @@ import { AnyEntity, UnknownType } from '@/utils/interfaces/generic.interface';
|
|||||||
import { SearchConfigType } from '../interfaces/interface';
|
import { SearchConfigType } from '../interfaces/interface';
|
||||||
|
|
||||||
export const SEARCH_PEOPLE_QUERY = gql`
|
export const SEARCH_PEOPLE_QUERY = gql`
|
||||||
query SearchPeopleQuery($where: PersonWhereInput, $limit: Int) {
|
query SearchPeople(
|
||||||
searchResults: findManyPerson(where: $where, take: $limit) {
|
$where: PersonWhereInput
|
||||||
|
$limit: Int
|
||||||
|
$orderBy: [PersonOrderByWithRelationInput!]
|
||||||
|
) {
|
||||||
|
searchResults: findManyPerson(
|
||||||
|
where: $where
|
||||||
|
take: $limit
|
||||||
|
orderBy: $orderBy
|
||||||
|
) {
|
||||||
id
|
id
|
||||||
phone
|
phone
|
||||||
email
|
email
|
||||||
@ -21,7 +29,7 @@ export const SEARCH_PEOPLE_QUERY = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SEARCH_USER_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) {
|
searchResults: findManyUser(where: $where, take: $limit) {
|
||||||
id
|
id
|
||||||
email
|
email
|
||||||
@ -39,7 +47,7 @@ export const EMPTY_QUERY = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SEARCH_COMPANY_QUERY = gql`
|
export const SEARCH_COMPANY_QUERY = gql`
|
||||||
query SearchCompanyQuery(
|
query SearchCompany(
|
||||||
$where: CompanyWhereInput
|
$where: CompanyWhereInput
|
||||||
$limit: Int
|
$limit: Int
|
||||||
$orderBy: [CompanyOrderByWithRelationInput!]
|
$orderBy: [CompanyOrderByWithRelationInput!]
|
||||||
|
@ -17,6 +17,4 @@ export const DropdownMenu = styled.div`
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
width: 200px;
|
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 { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { RightDrawerComments } from '@/comments/components/comments/RightDrawerComments';
|
import { RightDrawerComments } from '@/comments/components/RightDrawerComments';
|
||||||
import { RightDrawerCreateCommentThread } from '@/comments/components/comments/RightDrawerCreateCommentThread';
|
import { RightDrawerCreateCommentThread } from '@/comments/components/RightDrawerCreateCommentThread';
|
||||||
import { isDefined } from '@/utils/type-guards/isDefined';
|
import { isDefined } from '@/utils/type-guards/isDefined';
|
||||||
|
|
||||||
import { rightDrawerPageState } from '../states/rightDrawerPageState';
|
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';
|
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||||
|
|
||||||
|
export type AvatarType = 'squared' | 'rounded';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
avatarUrl: string | null | undefined;
|
avatarUrl: string | null | undefined;
|
||||||
size: number;
|
size: number;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
type?: 'squared' | 'rounded';
|
type?: AvatarType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
|
export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
|
||||||
|
@ -27,7 +27,7 @@ export const mocks = [
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
graphql.query('SearchUserQuery', (req, res, ctx) => {
|
graphql.query('SearchUser', (req, res, ctx) => {
|
||||||
const returnedMockedData = filterAndSortData<GraphqlQueryUser>(
|
const returnedMockedData = filterAndSortData<GraphqlQueryUser>(
|
||||||
mockedUsersData,
|
mockedUsersData,
|
||||||
req.variables.where,
|
req.variables.where,
|
||||||
|
@ -23,7 +23,7 @@ export const graphqlMocks = [
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
graphql.query('SearchCompanyQuery', (req, res, ctx) => {
|
graphql.query('SearchCompany', (req, res, ctx) => {
|
||||||
const returnedMockedData = filterAndSortData<GraphqlQueryCompany>(
|
const returnedMockedData = filterAndSortData<GraphqlQueryCompany>(
|
||||||
mockedCompaniesData,
|
mockedCompaniesData,
|
||||||
req.variables.where,
|
req.variables.where,
|
||||||
@ -36,7 +36,7 @@ export const graphqlMocks = [
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
graphql.query('SearchUserQuery', (req, res, ctx) => {
|
graphql.query('SearchUser', (req, res, ctx) => {
|
||||||
const returnedMockedData = filterAndSortData<GraphqlQueryUser>(
|
const returnedMockedData = filterAndSortData<GraphqlQueryUser>(
|
||||||
mockedUsersData,
|
mockedUsersData,
|
||||||
req.variables.where,
|
req.variables.where,
|
||||||
|
Loading…
Reference in New Issue
Block a user