From f6b691945c8fda6be3604d9667eaa211fc358087 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 4 May 2023 17:27:27 +0200 Subject: [PATCH] Make phone editable on people's page (#98) * Make phone editable on people's page * Make City editable --------- Co-authored-by: Charles Bochet --- front/src/components/link/Link.tsx | 30 ++++++++ .../editable-cell/EditableCellWrapper.tsx | 28 +++++-- .../table/editable-cell/EditablePhone.tsx | 75 +++++++++++++++++++ .../table/editable-cell/EditableText.tsx | 41 +++++++--- .../__stories__/EditablePhone.stories.tsx | 38 ++++++++++ .../__tests__/EditablePhone.test.tsx | 28 +++++++ .../__tests__/EditableText.test.tsx | 2 +- front/src/pages/people/people-table.tsx | 35 +++++---- 8 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 front/src/components/link/Link.tsx create mode 100644 front/src/components/table/editable-cell/EditablePhone.tsx create mode 100644 front/src/components/table/editable-cell/__stories__/EditablePhone.stories.tsx create mode 100644 front/src/components/table/editable-cell/__tests__/EditablePhone.test.tsx diff --git a/front/src/components/link/Link.tsx b/front/src/components/link/Link.tsx new file mode 100644 index 000000000..6e2f9e828 --- /dev/null +++ b/front/src/components/link/Link.tsx @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; +import * as React from 'react'; +import { Link as ReactLink } from 'react-router-dom'; + +type OwnProps = { + href: string; + children?: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; +}; + +const StyledClickable = styled.div` + display: flex; + + a { + color: inherit; + text-decoration: none; + } +`; + +function Link({ href, children, onClick }: OwnProps) { + return ( + + + {children} + + + ); +} + +export default Link; diff --git a/front/src/components/table/editable-cell/EditableCellWrapper.tsx b/front/src/components/table/editable-cell/EditableCellWrapper.tsx index 1f506c537..88bdcb78f 100644 --- a/front/src/components/table/editable-cell/EditableCellWrapper.tsx +++ b/front/src/components/table/editable-cell/EditableCellWrapper.tsx @@ -7,6 +7,7 @@ import { ThemeType } from '../../../layout/styles/themes'; type OwnProps = { children: ReactElement; onEditModeChange: (isEditMode: boolean) => void; + shouldAlignRight?: boolean; }; const StyledWrapper = styled.div` @@ -20,19 +21,24 @@ const StyledWrapper = styled.div` type styledEditModeWrapperProps = { isEditMode: boolean; + shouldAlignRight?: boolean; }; -const styledEditModeWrapper = (theme: ThemeType) => +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: ${theme.spacing(2)}; - padding-right: ${theme.spacing(2)}; - background: ${theme.primaryBackground}; - border: 1px solid ${theme.primaryBorder}; + 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; @@ -43,10 +49,14 @@ const Container = styled.div` width: 100%; padding-left: ${(props) => props.theme.spacing(2)}; padding-right: ${(props) => props.theme.spacing(2)}; - ${(props) => props.isEditMode && styledEditModeWrapper(props.theme)} + ${(props) => props.isEditMode && styledEditModeWrapper(props)} `; -function EditableCellWrapper({ children, onEditModeChange }: OwnProps) { +function EditableCellWrapper({ + children, + onEditModeChange, + shouldAlignRight, +}: OwnProps) { const [isEditMode, setIsEditMode] = useState(false); const wrapperRef = useRef(null); @@ -63,7 +73,9 @@ function EditableCellWrapper({ children, onEditModeChange }: OwnProps) { onEditModeChange(true); }} > - {children} + + {children} + ); } diff --git a/front/src/components/table/editable-cell/EditablePhone.tsx b/front/src/components/table/editable-cell/EditablePhone.tsx new file mode 100644 index 000000000..38048c8bc --- /dev/null +++ b/front/src/components/table/editable-cell/EditablePhone.tsx @@ -0,0 +1,75 @@ +import styled from '@emotion/styled'; +import { ChangeEvent, MouseEvent, useRef, useState } from 'react'; +import EditableCellWrapper from './EditableCellWrapper'; +import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'; +import Link from '../../link/Link'; + +type OwnProps = { + placeholder?: string; + value: string; + changeHandler: (updated: string) => void; +}; + +type StyledEditModeProps = { + isEditMode: boolean; +}; + +const StyledEditInplaceInput = styled.input` + width: 100%; + border: none; + outline: none; + + &::placeholder { + font-weight: bold; + color: ${(props) => props.theme.text20}; + } +`; + +function EditablePhone({ value, placeholder, changeHandler }: OwnProps) { + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(value); + const [isEditMode, setIsEditMode] = useState(false); + + const onEditModeChange = (isEditMode: boolean) => { + setIsEditMode(isEditMode); + if (isEditMode) { + inputRef.current?.focus(); + } + }; + + return ( + + {isEditMode ? ( + ) => { + setInputValue(event.target.value); + changeHandler(event.target.value); + }} + /> + ) : ( +
+ {isValidPhoneNumber(inputValue) ? ( + ) => { + event.stopPropagation(); + }} + > + {parsePhoneNumber(inputValue, 'FR')?.formatInternational() || + inputValue} + + ) : ( + {inputValue} + )} +
+ )} +
+ ); +} + +export default EditablePhone; diff --git a/front/src/components/table/editable-cell/EditableText.tsx b/front/src/components/table/editable-cell/EditableText.tsx index 4dac72758..1a4160a85 100644 --- a/front/src/components/table/editable-cell/EditableText.tsx +++ b/front/src/components/table/editable-cell/EditableText.tsx @@ -6,6 +6,7 @@ type OwnProps = { placeholder?: string; content: string; changeHandler: (updated: string) => void; + shouldAlignRight?: boolean; }; type StyledEditModeProps = { @@ -24,7 +25,16 @@ const StyledInplaceInput = styled.input` } `; -function EditableCell({ content, placeholder, changeHandler }: OwnProps) { +const StyledNoEditText = styled.div` + max-width: 200px; +`; + +function EditableCell({ + content, + placeholder, + changeHandler, + shouldAlignRight, +}: OwnProps) { const inputRef = useRef(null); const [inputValue, setInputValue] = useState(content); const [isEditMode, setIsEditMode] = useState(false); @@ -37,17 +47,24 @@ function EditableCell({ content, placeholder, changeHandler }: OwnProps) { }; return ( - - ) => { - setInputValue(event.target.value); - changeHandler(event.target.value); - }} - /> + + {isEditMode ? ( + ) => { + setInputValue(event.target.value); + changeHandler(event.target.value); + }} + /> + ) : ( + {inputValue} + )} ); } diff --git a/front/src/components/table/editable-cell/__stories__/EditablePhone.stories.tsx b/front/src/components/table/editable-cell/__stories__/EditablePhone.stories.tsx new file mode 100644 index 000000000..fa388cb62 --- /dev/null +++ b/front/src/components/table/editable-cell/__stories__/EditablePhone.stories.tsx @@ -0,0 +1,38 @@ +import EditablePhone from '../EditablePhone'; +import { ThemeProvider } from '@emotion/react'; +import { lightTheme } from '../../../../layout/styles/themes'; +import { StoryFn } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; + +const component = { + title: 'EditablePhone', + component: EditablePhone, +}; + +type OwnProps = { + value: string; + changeHandler: (updated: string) => void; +}; + +export default component; + +const Template: StoryFn = (args: OwnProps) => { + return ( + + +
+ +
+
+
+ ); +}; + +export const EditablePhoneStory = Template.bind({}); +EditablePhoneStory.args = { + placeholder: 'Test placeholder', + value: '+33657646543', + changeHandler: () => { + console.log('changed'); + }, +}; diff --git a/front/src/components/table/editable-cell/__tests__/EditablePhone.test.tsx b/front/src/components/table/editable-cell/__tests__/EditablePhone.test.tsx new file mode 100644 index 000000000..e3bf6455f --- /dev/null +++ b/front/src/components/table/editable-cell/__tests__/EditablePhone.test.tsx @@ -0,0 +1,28 @@ +import { fireEvent, render } from '@testing-library/react'; + +import { EditablePhoneStory } from '../__stories__/EditablePhone.stories'; + +it('Checks the EditablePhone editing event bubbles up', async () => { + const func = jest.fn(() => null); + const { getByTestId } = render( + , + ); + + const parent = getByTestId('content-editable-parent'); + + const wrapper = parent.querySelector('div'); + + if (!wrapper) { + throw new Error('Editable input not found'); + } + fireEvent.click(wrapper); + + const editableInput = parent.querySelector('input'); + + if (!editableInput) { + throw new Error('Editable input not found'); + } + + fireEvent.change(editableInput, { target: { value: '23' } }); + expect(func).toBeCalledWith('23'); +}); diff --git a/front/src/components/table/editable-cell/__tests__/EditableText.test.tsx b/front/src/components/table/editable-cell/__tests__/EditableText.test.tsx index eeab931a2..6a4c2b515 100644 --- a/front/src/components/table/editable-cell/__tests__/EditableText.test.tsx +++ b/front/src/components/table/editable-cell/__tests__/EditableText.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render } from '@testing-library/react'; import { EditableTextStory } from '../__stories__/EditableText.stories'; -it('Checks the EditableCell editing event bubbles up', async () => { +it('Checks the EditableText editing event bubbles up', async () => { const func = jest.fn(() => null); const { getByTestId } = render( , diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index 7f5a62265..abba205f1 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -12,7 +12,6 @@ import { import { createColumnHelper } from '@tanstack/react-table'; import ClickableCell from '../../components/table/ClickableCell'; import ColumnHead from '../../components/table/ColumnHead'; -import { parsePhoneNumber, CountryCode } from 'libphonenumber-js'; import Checkbox from '../../components/form/Checkbox'; import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer'; import CompanyChip from '../../components/chips/CompanyChip'; @@ -31,6 +30,7 @@ import { SEARCH_PEOPLE_QUERY, } from '../../services/search/search'; import { GraphqlQueryCompany } from '../../interfaces/company.interface'; +import EditablePhone from '../../components/table/editable-cell/EditablePhone'; export const availableSorts = [ { @@ -155,7 +155,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 @@ -179,17 +179,15 @@ export const peopleColumns = [ columnHelper.accessor('phone', { header: () => } />, cell: (props) => ( - - {parsePhoneNumber( - props.row.original.phone, - props.row.original.countryCode as CountryCode, - )?.formatInternational() || props.row.original.phone} - + { + const person = props.row.original; + person.phone = value; + updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + }} + /> ), }), columnHelper.accessor('creationDate', { @@ -215,7 +213,16 @@ export const peopleColumns = [ columnHelper.accessor('city', { header: () => } />, cell: (props) => ( - {props.row.original.city} + { + const person = props.row.original; + person.city = value; + updatePerson(person).catch((error) => console.error(error)); // TODO: handle error + }} + /> ), }), ];