diff --git a/front/src/components/chips/CompanyChip.tsx b/front/src/components/chips/CompanyChip.tsx index 5aa3a32354..d1037133b9 100644 --- a/front/src/components/chips/CompanyChip.tsx +++ b/front/src/components/chips/CompanyChip.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import styled from '@emotion/styled'; -type OwnProps = { +export type CompanyChipPropsType = { name: string; picture?: string; }; @@ -26,7 +26,7 @@ const StyledContainer = styled.span` } `; -function CompanyChip({ name, picture }: OwnProps) { +function CompanyChip({ name, picture }: CompanyChipPropsType) { return ( {picture && ( diff --git a/front/src/components/form/DatePicker.tsx b/front/src/components/form/DatePicker.tsx index 03bd607d0c..f4f1d9a59b 100644 --- a/front/src/components/form/DatePicker.tsx +++ b/front/src/components/form/DatePicker.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { forwardRef, useState } from 'react'; +import React, { ReactElement, forwardRef, useState } from 'react'; import ReactDatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; @@ -8,6 +8,7 @@ export type DatePickerProps = { isOpen?: boolean; date: Date; onChangeHandler: (date: Date) => void; + customInput?: ReactElement; }; const StyledContainer = styled.div` @@ -34,11 +35,12 @@ const StyledContainer = styled.div` } `; -function DatePicker({ date, onChangeHandler, isOpen }: DatePickerProps) { +function DatePicker({ date, onChangeHandler, customInput }: DatePickerProps) { const [startDate, setStartDate] = useState(date); type DivProps = React.HTMLProps; - const DateDisplay = forwardRef( + + const DefaultDateDisplay = forwardRef( ({ value, onClick }, ref) => (
{value && @@ -54,30 +56,13 @@ function DatePicker({ date, onChangeHandler, isOpen }: DatePickerProps) { return ( { setStartDate(date); onChangeHandler(date); }} - popperPlacement="bottom" - popperModifiers={[ - { - name: 'offset', - options: { - offset: [55, 0], - }, - }, - { - name: 'preventOverflow', - options: { - rootBoundary: 'viewport', - tether: false, - altAxis: true, - }, - }, - ]} - customInput={} + customInput={customInput ? customInput : } /> ); diff --git a/front/src/components/form/__tests__/Datepicker.test.tsx b/front/src/components/form/__tests__/Datepicker.test.tsx index 323cd99459..d84c64adc0 100644 --- a/front/src/components/form/__tests__/Datepicker.test.tsx +++ b/front/src/components/form/__tests__/Datepicker.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react'; import { DatePickerStory } from '../__stories__/Datepicker.stories'; import { act } from 'react-dom/test-utils'; -it('Checks the datepicker renders', () => { +it('Checks the datepicker renders', async () => { const changeHandler = jest.fn(); const { getByText } = render( { onChangeHandler={changeHandler} />, ); + + await act(async () => { + expect(getByText('Mar 3, 2021')).toBeInTheDocument(); + }); + act(() => { fireEvent.click(getByText('Mar 3, 2021')); }); diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index b2eff0c614..2bb6a5ad47 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -151,28 +151,6 @@ function Table({ ))} - {table - .getFooterGroups() - .flatMap((group) => group.headers) - .filter((header) => !!header.column.columnDef.footer).length > - 0 && ( - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.footer, - header.getContext(), - )} - - ))} - - ))} - - )} diff --git a/front/src/components/table/editable-cell/EditableCellWrapper.tsx b/front/src/components/table/editable-cell/EditableCellWrapper.tsx index 88bdcb78fb..a5f0773db7 100644 --- a/front/src/components/table/editable-cell/EditableCellWrapper.tsx +++ b/front/src/components/table/editable-cell/EditableCellWrapper.tsx @@ -1,13 +1,15 @@ -import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { ReactElement, useRef, useState } from 'react'; +import { ReactElement, useRef } from 'react'; import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter'; -import { ThemeType } from '../../../layout/styles/themes'; type OwnProps = { - children: ReactElement; - onEditModeChange: (isEditMode: boolean) => void; - shouldAlignRight?: boolean; + editModeContent: ReactElement; + nonEditModeContent: ReactElement; + editModeHorizontalAlign?: 'left' | 'right'; + editModeVerticalPosition?: 'over' | 'below'; + isEditMode?: boolean; + onOutsideClick?: () => void; + onInsideClick?: () => void; }; const StyledWrapper = styled.div` @@ -19,63 +21,74 @@ const StyledWrapper = styled.div` width: 100%; `; -type styledEditModeWrapperProps = { - isEditMode: boolean; - shouldAlignRight?: boolean; +type StyledEditModeContainerProps = { + editModeHorizontalAlign?: 'left' | 'right'; + editModeVerticalPosition?: 'over' | 'below'; }; -const styledEditModeWrapper = ( - props: styledEditModeWrapperProps & { theme: ThemeType }, -) => - css` - position: absolute; - left: ${props.shouldAlignRight ? 'auto' : '0'}; - right: ${props.shouldAlignRight ? '0' : 'auto'}; - width: 260px; - height: 100%; - - display: flex; - padding-left: ${props.theme.spacing(2)}; - padding-right: ${props.theme.spacing(2)}; - background: ${props.theme.primaryBackground}; - border: 1px solid ${props.theme.primaryBorder}; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16); - z-index: 1; - border-radius: 4px; - backdrop-filter: blur(20px); - `; - -const Container = styled.div` +const StyledNonEditModeContainer = styled.div` + display: flex; + align-items: center; width: 100%; + height: 100%; padding-left: ${(props) => props.theme.spacing(2)}; padding-right: ${(props) => props.theme.spacing(2)}; - ${(props) => props.isEditMode && styledEditModeWrapper(props)} +`; + +const StyledEditModeContainer = styled.div` + display: flex; + align-items: center; + min-width: 100%; + min-height: 100%; + padding-left: ${(props) => props.theme.spacing(2)}; + padding-right: ${(props) => props.theme.spacing(2)}; + position: absolute; + left: ${(props) => + props.editModeHorizontalAlign === 'right' ? 'auto' : '0'}; + right: ${(props) => + props.editModeHorizontalAlign === 'right' ? '0' : 'auto'}; + top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')}; + + background: ${(props) => props.theme.primaryBackground}; + border: 1px solid ${(props) => props.theme.primaryBorder}; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16); + z-index: 1; + border-radius: 4px; + backdrop-filter: blur(20px); `; function EditableCellWrapper({ - children, - onEditModeChange, - shouldAlignRight, + editModeContent, + nonEditModeContent, + editModeHorizontalAlign = 'left', + editModeVerticalPosition = 'over', + isEditMode = false, + onOutsideClick, + onInsideClick, }: OwnProps) { - const [isEditMode, setIsEditMode] = useState(false); - const wrapperRef = useRef(null); useOutsideAlerter(wrapperRef, () => { - setIsEditMode(false); - onEditModeChange(false); + onOutsideClick && onOutsideClick(); }); return ( { - setIsEditMode(true); - onEditModeChange(true); + onInsideClick && onInsideClick(); }} > - - {children} - + + {nonEditModeContent} + + {isEditMode && ( + + {editModeContent} + + )} ); } diff --git a/front/src/components/table/editable-cell/EditableChip.tsx b/front/src/components/table/editable-cell/EditableChip.tsx index fe4c42132c..fcb35accef 100644 --- a/front/src/components/table/editable-cell/EditableChip.tsx +++ b/front/src/components/table/editable-cell/EditableChip.tsx @@ -7,23 +7,18 @@ export type EditableChipProps = { value: string; picture: string; changeHandler: (updated: string) => void; - shouldAlignRight?: boolean; + editModeHorizontalAlign?: 'left' | 'right'; ChipComponent: ComponentType<{ name: string; picture: string }>; }; -type StyledEditModeProps = { - isEditMode: boolean; -}; - -const StyledInplaceInput = styled.input` +const StyledInplaceInput = styled.input` width: 100%; border: none; outline: none; &::placeholder { - font-weight: ${(props) => (props.isEditMode ? 'bold' : 'normal')}; - color: ${(props) => - props.isEditMode ? props.theme.text20 : 'transparent'}; + font-weight: 'bold'; + color: props.theme.text20; } `; function EditableChip({ @@ -31,25 +26,21 @@ function EditableChip({ placeholder, changeHandler, picture, - shouldAlignRight, + editModeHorizontalAlign, ChipComponent, }: EditableChipProps) { const inputRef = useRef(null); const [inputValue, setInputValue] = useState(value); const [isEditMode, setIsEditMode] = useState(false); - const onEditModeChange = (isEditMode: boolean) => { - setIsEditMode(isEditMode); - }; - return ( - {isEditMode ? ( + onOutsideClick={() => setIsEditMode(false)} + onInsideClick={() => setIsEditMode(true)} + isEditMode={isEditMode} + editModeHorizontalAlign={editModeHorizontalAlign} + editModeContent={ - ) : ( - - )} - + } + nonEditModeContent={} + > ); } diff --git a/front/src/components/table/editable-cell/EditableDate.tsx b/front/src/components/table/editable-cell/EditableDate.tsx index 4653c1c851..89a8b79dde 100644 --- a/front/src/components/table/editable-cell/EditableDate.tsx +++ b/front/src/components/table/editable-cell/EditableDate.tsx @@ -1,12 +1,12 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { forwardRef, useState } from 'react'; import EditableCellWrapper from './EditableCellWrapper'; import DatePicker from '../../form/DatePicker'; export type EditableDateProps = { value: Date; changeHandler: (date: Date) => void; - shouldAlignRight?: boolean; + editModeHorizontalAlign?: 'left' | 'right'; }; const StyledContainer = styled.div` @@ -16,31 +16,57 @@ const StyledContainer = styled.div` function EditableDate({ value, changeHandler, - shouldAlignRight, + editModeHorizontalAlign, }: EditableDateProps) { const [inputValue, setInputValue] = useState(value); const [isEditMode, setIsEditMode] = useState(false); - const onEditModeChange = (isEditMode: boolean) => { - setIsEditMode(isEditMode); - }; + type DivProps = React.HTMLProps; + + const DateDisplay = forwardRef( + ({ value, onClick }, ref) => ( +
+ {value && + new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(new Date(value as string))} +
+ ), + ); return ( - - { - changeHandler(date); - setInputValue(date); - }} - /> - - + isEditMode={isEditMode} + onOutsideClick={() => setIsEditMode(false)} + onInsideClick={() => setIsEditMode(true)} + editModeHorizontalAlign={editModeHorizontalAlign} + editModeContent={ + + { + changeHandler(date); + setInputValue(date); + }} + customInput={} + /> + + } + nonEditModeContent={ + +
+ {inputValue && + new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(inputValue)} +
+
+ } + > ); } diff --git a/front/src/components/table/editable-cell/EditableFullName.tsx b/front/src/components/table/editable-cell/EditableFullName.tsx index 17e0ffd3e3..7b158fb689 100644 --- a/front/src/components/table/editable-cell/EditableFullName.tsx +++ b/front/src/components/table/editable-cell/EditableFullName.tsx @@ -40,9 +40,10 @@ function EditableFullName({ firstname, lastname, changeHandler }: OwnProps) { return ( setIsEditMode(editMode)} - > - {isEditMode ? ( + onOutsideClick={() => setIsEditMode(false)} + onInsideClick={() => setIsEditMode(true)} + isEditMode={isEditMode} + editModeContent={ - ) : ( + } + nonEditModeContent={ - )} - + } + > ); } diff --git a/front/src/components/table/editable-cell/EditablePhone.tsx b/front/src/components/table/editable-cell/EditablePhone.tsx index 38048c8bc0..a038e9be1e 100644 --- a/front/src/components/table/editable-cell/EditablePhone.tsx +++ b/front/src/components/table/editable-cell/EditablePhone.tsx @@ -30,16 +30,12 @@ function EditablePhone({ value, placeholder, changeHandler }: OwnProps) { const [inputValue, setInputValue] = useState(value); const [isEditMode, setIsEditMode] = useState(false); - const onEditModeChange = (isEditMode: boolean) => { - setIsEditMode(isEditMode); - if (isEditMode) { - inputRef.current?.focus(); - } - }; - return ( - - {isEditMode ? ( + setIsEditMode(false)} + onInsideClick={() => setIsEditMode(true)} + editModeContent={ - ) : ( + } + nonEditModeContent={
{isValidPhoneNumber(inputValue) ? ( {inputValue} )}
- )} -
+ } + /> ); } diff --git a/front/src/components/table/editable-cell/EditableRelation.tsx b/front/src/components/table/editable-cell/EditableRelation.tsx new file mode 100644 index 0000000000..e933611891 --- /dev/null +++ b/front/src/components/table/editable-cell/EditableRelation.tsx @@ -0,0 +1,135 @@ +import { ChangeEvent, ComponentType, useState } from 'react'; +import EditableCellWrapper from './EditableCellWrapper'; +import styled from '@emotion/styled'; +import { useSearch } from '../../../services/search/search'; +import { FilterType } from '../table-header/interface'; +import { People_Bool_Exp } from '../../../generated/graphql'; + +const StyledEditModeContainer = styled.div` + width: 200px; + margin-left: calc(-1 * ${(props) => props.theme.spacing(2)}); + margin-right: calc(-1 * ${(props) => props.theme.spacing(2)}); +`; +const StyledEditModeSelectedContainer = styled.div` + height: 31px; + display: flex; + align-items: center; + padding-left: ${(props) => props.theme.spacing(2)}; + padding-right: ${(props) => props.theme.spacing(2)}; +`; +const StyledEditModeSearchContainer = styled.div` + height: 32px; + display: flex; + align-items: center; + border-top: 1px solid ${(props) => props.theme.primaryBorder}; +`; +const StyledEditModeSearchInput = styled.input` + width: 100%; + border: none; + outline: none; + padding-left: ${(props) => props.theme.spacing(2)}; + padding-right: ${(props) => props.theme.spacing(2)}; + + &::placeholder { + font-weight: 'bold'; + color: ${(props) => props.theme.text20}; + } +`; + +const StyledEditModeResults = styled.div` + border-top: 1px solid ${(props) => props.theme.primaryBorder}; + padding-left: ${(props) => props.theme.spacing(2)}; + padding-right: ${(props) => props.theme.spacing(2)}; +`; +const StyledEditModeResultItem = styled.div` + height: 32px; + display: flex; + align-items: center; + cursor: pointer; +`; + +export type EditableRelationProps = { + relation: RelationType; + searchPlaceholder: string; + searchFilter: FilterType; + changeHandler: (relation: RelationType) => void; + editModeHorizontalAlign?: 'left' | 'right'; + ChipComponent: ComponentType; + chipComponentPropsMapper: ( + relation: RelationType, + ) => ChipComponentPropsType & JSX.IntrinsicAttributes; +}; + +function EditableRelation({ + relation, + searchPlaceholder, + searchFilter, + changeHandler, + editModeHorizontalAlign, + ChipComponent, + chipComponentPropsMapper, +}: EditableRelationProps) { + const [selectedRelation, setSelectedRelation] = useState(relation); + const [isEditMode, setIsEditMode] = useState(false); + + const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch(); + + return ( + setIsEditMode(false)} + onInsideClick={() => { + if (!isEditMode) { + setIsEditMode(true); + } + }} + editModeContent={ + + + {selectedRelation ? ( + + ) : ( + <> + )} + + + ) => { + setFilterSearch(searchFilter); + setSearchInput(event.target.value); + }} + /> + + + {filterSearchResults.results && + filterSearchResults.results.map((result) => ( + { + setSelectedRelation(result.value); + changeHandler(result.value); + setIsEditMode(false); + }} + > + + + ))} + + + } + nonEditModeContent={ +
+ {selectedRelation ? ( + + ) : ( + <> + )} +
+ } + /> + ); +} + +export default EditableRelation; diff --git a/front/src/components/table/editable-cell/EditableText.tsx b/front/src/components/table/editable-cell/EditableText.tsx index 3a52221ce8..40787fe51f 100644 --- a/front/src/components/table/editable-cell/EditableText.tsx +++ b/front/src/components/table/editable-cell/EditableText.tsx @@ -6,7 +6,7 @@ type OwnProps = { placeholder?: string; content: string; changeHandler: (updated: string) => void; - shouldAlignRight?: boolean; + editModeHorizontalAlign?: 'left' | 'right'; }; type StyledEditModeProps = { @@ -33,22 +33,19 @@ function EditableText({ content, placeholder, changeHandler, - shouldAlignRight, + editModeHorizontalAlign, }: OwnProps) { const inputRef = useRef(null); const [inputValue, setInputValue] = useState(content); const [isEditMode, setIsEditMode] = useState(false); - const onEditModeChange = (isEditMode: boolean) => { - setIsEditMode(isEditMode); - }; - return ( - {isEditMode ? ( + isEditMode={isEditMode} + onOutsideClick={() => setIsEditMode(false)} + onInsideClick={() => setIsEditMode(true)} + editModeHorizontalAlign={editModeHorizontalAlign} + editModeContent={ - ) : ( - {inputValue} - )} - + } + nonEditModeContent={{inputValue}} + >
); } diff --git a/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx b/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx new file mode 100644 index 0000000000..6bbf910200 --- /dev/null +++ b/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx @@ -0,0 +1,112 @@ +import EditableRelation, { EditableRelationProps } from '../EditableRelation'; +import { ThemeProvider } from '@emotion/react'; +import { lightTheme } from '../../../../layout/styles/themes'; +import { StoryFn } from '@storybook/react'; +import CompanyChip, { CompanyChipPropsType } from '../../../chips/CompanyChip'; +import { + GraphqlQueryCompany, + PartialCompany, +} from '../../../../interfaces/company.interface'; +import { MockedProvider } from '@apollo/client/testing'; +import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search'; +import styled from '@emotion/styled'; +import { People_Bool_Exp } from '../../../../generated/graphql'; +import { FilterType } from '../../table-header/interface'; +import { FaBuilding } from 'react-icons/fa'; + +const component = { + title: 'editable-cell/EditableRelation', + component: EditableRelation, +}; + +export default component; + +const StyledParent = styled.div` + height: 400px; +`; + +const mocks = [ + { + request: { + query: SEARCH_COMPANY_QUERY, + variables: { + where: undefined, + }, + }, + result: { + data: { + companies: [], + }, + }, + }, + { + request: { + query: SEARCH_COMPANY_QUERY, + variables: { + where: { name: { _ilike: '%%' } }, + limit: 5, + }, + }, + result: { + data: { + searchResults: [ + { id: 'abnb', name: 'Airbnb', domain_name: 'abnb.com' }, + ], + }, + }, + }, +]; + +const Template: StoryFn< + typeof EditableRelation +> = (args: EditableRelationProps) => { + return ( + + + + {...args} /> + + + + ); +}; + +export const EditableRelationStory = Template.bind({}); +EditableRelationStory.args = { + relation: { + id: '123', + name: 'Heroku', + domain_name: 'heroku.com', + } as PartialCompany, + ChipComponent: CompanyChip, + chipComponentPropsMapper: (company: PartialCompany): CompanyChipPropsType => { + return { + name: company.name, + picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, + }; + }, + changeHandler: (relation: PartialCompany) => { + console.log('changed', relation); + }, + searchFilter: { + key: 'company_name', + label: 'Company', + icon: , + whereTemplate: () => { + return {}; + }, + searchQuery: SEARCH_COMPANY_QUERY, + searchTemplate: (searchInput: string) => ({ + name: { _ilike: `%${searchInput}%` }, + }), + searchResultMapper: (company: GraphqlQueryCompany) => ({ + displayValue: company.name, + value: { + id: company.id, + name: company.name, + domain_name: company.domain_name, + }, + }), + operands: [], + } satisfies FilterType, +}; diff --git a/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx b/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx new file mode 100644 index 0000000000..18f119ff2d --- /dev/null +++ b/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent, render, waitFor } from '@testing-library/react'; + +import { EditableRelationStory } from '../__stories__/EditableRelation.stories'; +import { CompanyChipPropsType } from '../../../chips/CompanyChip'; +import { PartialCompany } from '../../../../interfaces/company.interface'; + +import { EditableRelationProps } from '../EditableRelation'; +import { act } from 'react-dom/test-utils'; + +it('Checks the EditableRelation editing event bubbles up', async () => { + const func = jest.fn(() => null); + const { getByTestId, getByText } = render( + )} + changeHandler={func} + />, + ); + + const parent = getByTestId('content-editable-parent'); + + const wrapper = parent.querySelector('div'); + + await waitFor(() => { + expect(getByText('Heroku')).toBeInTheDocument(); + }); + + if (!wrapper) { + throw new Error('Editable relation not found'); + } + fireEvent.click(wrapper); + + const input = parent.querySelector('input'); + if (!input) { + throw new Error('Search input not found'); + } + act(() => { + fireEvent.change(input, { target: { value: 'Ai' } }); + }); + + await waitFor(() => { + expect(getByText('Airbnb')).toBeInTheDocument(); + }); + + act(() => { + fireEvent.click(getByText('Airbnb')); + }); + + await waitFor(() => { + expect(func).toBeCalledWith({ + domain_name: 'abnb.com', + id: 'abnb', + name: 'Airbnb', + }); + }); +}); diff --git a/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx b/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx index 36bbbfe87e..2c2dd12b83 100644 --- a/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx +++ b/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render } from '@testing-library/react'; import { RegularSortDropdownButton } from '../__stories__/SortDropdownButton.stories'; -import { FaEnvelope } from 'react-icons/fa'; +import { FaEnvelope, FaRegBuilding } from 'react-icons/fa'; it('Checks the default top option is Ascending', async () => { const setSorts = jest.fn(); @@ -49,3 +49,26 @@ it('Checks the selection of Descending', async () => { _type: 'default_sort', }); }); + +it('Checks custom_sort is working', async () => { + const setSorts = jest.fn(); + const { getByText } = render( + , + ); + + const sortDropdownButton = getByText('Sort'); + fireEvent.click(sortDropdownButton); + + const sortByCompany = getByText('Company'); + fireEvent.click(sortByCompany); + + expect(setSorts).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'company_name', + label: 'Company', + icon: , + _type: 'custom_sort', + order: 'asc', + }), + ); +}); diff --git a/front/src/interfaces/company.interface.ts b/front/src/interfaces/company.interface.ts index 6b80908a74..c19f51e698 100644 --- a/front/src/interfaces/company.interface.ts +++ b/front/src/interfaces/company.interface.ts @@ -6,7 +6,7 @@ export interface Opportunity { icon: string; } -export interface Company { +export type Company = { id: string; name: string; domain_name: string; @@ -15,7 +15,10 @@ export interface Company { opportunities: Opportunity[]; accountOwner?: User; creationDate: Date; -} +}; + +export type PartialCompany = Partial & + Pick; export type GraphqlQueryCompany = { id: string; diff --git a/front/src/interfaces/person.interface.test.ts b/front/src/interfaces/person.interface.test.ts index c704314c13..dad7124cb1 100644 --- a/front/src/interfaces/person.interface.test.ts +++ b/front/src/interfaces/person.interface.test.ts @@ -31,8 +31,12 @@ describe('mapPerson', () => { city: '', company: { id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', - name: '', + name: 'Test', domain_name: '', + opportunities: [], + employees: 0, + address: '', + creationDate: new Date(), }, creationDate: new Date(), pipe: { diff --git a/front/src/interfaces/person.interface.ts b/front/src/interfaces/person.interface.ts index df70f631f7..249849fc07 100644 --- a/front/src/interfaces/person.interface.ts +++ b/front/src/interfaces/person.interface.ts @@ -1,4 +1,4 @@ -import { Company } from './company.interface'; +import { PartialCompany } from './company.interface'; import { Pipe } from './pipe.interface'; export type Person = { @@ -7,10 +7,7 @@ export type Person = { lastname: string; picture?: string; email: string; - company: Omit< - Company, - 'employees' | 'address' | 'opportunities' | 'accountOwner' | 'creationDate' - >; + company: PartialCompany; phone: string; creationDate: Date; pipe: Pipe; diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index dc2a47f1c3..979322f7f3 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -31,7 +31,7 @@ const StyledPeopleContainer = styled.div` function People() { const [orderBy, setOrderBy] = useState(defaultOrderBy); const [where, setWhere] = useState({}); - const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch(); + const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch(); const updateSorts = useCallback((sorts: Array) => { setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy); @@ -61,7 +61,7 @@ function People() { onSortsUpdate={updateSorts} onFiltersUpdate={updateFilters} onFilterSearch={(filter, searchValue) => { - setSearhInput(searchValue); + setSearchInput(searchValue); setFilterSearch(filter); }} /> diff --git a/front/src/pages/people/__tests__/People.test.tsx b/front/src/pages/people/__tests__/People.test.tsx index 140004316e..dadb19b7cb 100644 --- a/front/src/pages/people/__tests__/People.test.tsx +++ b/front/src/pages/people/__tests__/People.test.tsx @@ -1,12 +1,85 @@ -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import { PeopleDefault } from '../__stories__/People.stories'; +import { act } from 'react-dom/test-utils'; +import { + GraphqlMutationPerson, + GraphqlQueryPerson, +} from '../../../interfaces/person.interface'; -it('Checks the People page render', async () => { - const { getByTestId } = render(); +jest.mock('../../../apollo', () => { + const personInterface = jest.requireActual( + '../../../interfaces/person.interface', + ); + return { + apiClient: { + mutate: (arg: { + mutation: unknown; + variables: GraphqlMutationPerson; + }) => { + const gqlPerson = arg.variables as unknown as GraphqlQueryPerson; + return { data: personInterface.mapPerson(gqlPerson) }; + }, + }, + }; +}); + +it('Checks people full name edit is updating data', async () => { + const { getByText, getByDisplayValue } = render(); await waitFor(() => { - const personChip = getByTestId('row-id-0'); - expect(personChip).toBeDefined(); + expect(getByText('John Doe')).toBeDefined(); + }); + + act(() => { + fireEvent.click(getByText('John Doe')); + }); + + await waitFor(() => { + expect(getByDisplayValue('John')).toBeInTheDocument(); + }); + + act(() => { + const nameInput = getByDisplayValue('John'); + + if (!nameInput) { + throw new Error('firstNameInput is null'); + } + fireEvent.change(nameInput, { target: { value: 'Jo' } }); + fireEvent.click(getByText('All People')); // Click outside + }); + + await waitFor(() => { + expect(getByText('Jo Doe')).toBeInTheDocument(); + }); +}); + +it('Checks people email edit is updating data', async () => { + const { getByText, getByDisplayValue } = render(); + + await waitFor(() => { + expect(getByText('john@linkedin.com')).toBeDefined(); + }); + + act(() => { + fireEvent.click(getByText('john@linkedin.com')); + }); + + await waitFor(() => { + expect(getByDisplayValue('john@linkedin.com')).toBeInTheDocument(); + }); + + act(() => { + const emailInput = getByDisplayValue('john@linkedin.com'); + + if (!emailInput) { + throw new Error('emailInput is null'); + } + fireEvent.change(emailInput, { target: { value: 'john@linkedin.c' } }); + fireEvent.click(getByText('All People')); // Click outside + }); + + await waitFor(() => { + expect(getByText('john@linkedin.c')).toBeInTheDocument(); }); }); diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index db1509bd5d..c55750c8f5 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -13,11 +13,12 @@ import { createColumnHelper } from '@tanstack/react-table'; import ClickableCell from '../../components/table/ClickableCell'; import ColumnHead from '../../components/table/ColumnHead'; import Checkbox from '../../components/form/Checkbox'; -import CompanyChip from '../../components/chips/CompanyChip'; +import CompanyChip, { + CompanyChipPropsType, +} from '../../components/chips/CompanyChip'; import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface'; import PipeChip from '../../components/chips/PipeChip'; import EditableText from '../../components/table/editable-cell/EditableText'; -import { updatePerson } from '../../services/people'; import { FilterType, SortType, @@ -31,10 +32,15 @@ import { SEARCH_COMPANY_QUERY, SEARCH_PEOPLE_QUERY, } from '../../services/search/search'; -import { GraphqlQueryCompany } from '../../interfaces/company.interface'; +import { + GraphqlQueryCompany, + PartialCompany, +} from '../../interfaces/company.interface'; import EditablePhone from '../../components/table/editable-cell/EditablePhone'; import EditableFullName from '../../components/table/editable-cell/EditableFullName'; import EditableDate from '../../components/table/editable-cell/EditableDate'; +import EditableRelation from '../../components/table/editable-cell/EditableRelation'; +import { updatePerson } from '../../services/people'; export const availableSorts = [ { @@ -261,7 +267,7 @@ export const peopleColumns = [ const person = props.row.original; person.firstname = firstName; person.lastname = lastName; - updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + updatePerson(person); }} /> ), @@ -275,7 +281,7 @@ export const peopleColumns = [ changeHandler={(value: string) => { const person = props.row.original; person.email = value; - updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + updatePerson(person); }} /> ), @@ -285,12 +291,47 @@ export const peopleColumns = [ } /> ), cell: (props) => ( - - - + + relation={props.row.original.company} + searchPlaceholder="Company" + ChipComponent={CompanyChip} + chipComponentPropsMapper={( + company: PartialCompany, + ): CompanyChipPropsType => { + return { + name: company.name, + picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, + }; + }} + changeHandler={(relation: PartialCompany) => { + const person = props.row.original; + person.company.id = relation.id; + updatePerson(person); + }} + searchFilter={ + { + key: 'company_name', + label: 'Company', + icon: , + whereTemplate: () => { + return {}; + }, + searchQuery: SEARCH_COMPANY_QUERY, + searchTemplate: (searchInput: string) => ({ + name: { _ilike: `%${searchInput}%` }, + }), + searchResultMapper: (company: GraphqlQueryCompany) => ({ + displayValue: company.name, + value: { + id: company.id, + name: company.name, + domain_name: company.domain_name, + }, + }), + operands: [], + } satisfies FilterType + } + /> ), }), columnHelper.accessor('phone', { @@ -302,7 +343,7 @@ export const peopleColumns = [ changeHandler={(value: string) => { const person = props.row.original; person.phone = value; - updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + updatePerson(person); }} /> ), @@ -315,7 +356,7 @@ export const peopleColumns = [ changeHandler={(value: Date) => { const person = props.row.original; person.creationDate = value; - updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + updatePerson(person); }} /> ), @@ -332,13 +373,13 @@ export const peopleColumns = [ header: () => } />, cell: (props) => ( { const person = props.row.original; person.city = value; - updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + updatePerson(person); }} /> ), diff --git a/front/src/services/people/__tests__/update.test.ts b/front/src/services/people/__tests__/update.test.ts index 95a57a08df..8b170a2804 100644 --- a/front/src/services/people/__tests__/update.test.ts +++ b/front/src/services/people/__tests__/update.test.ts @@ -23,7 +23,8 @@ jest.mock('../../../apollo', () => { it('updates a person', async () => { const result = await updatePerson({ - fullName: 'John Doe', + firstname: 'John', + lastname: 'Doe', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c', email: 'john@example.com', company: {