Make phone editable on people's page (#98)

* Make phone editable on people's page

* Make City editable

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Charles Bochet
2023-05-04 17:27:27 +02:00
committed by GitHub
parent e65fd3d6a5
commit f6b691945c
8 changed files with 242 additions and 35 deletions

View File

@ -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<HTMLElement>) => void;
};
const StyledClickable = styled.div`
display: flex;
a {
color: inherit;
text-decoration: none;
}
`;
function Link({ href, children, onClick }: OwnProps) {
return (
<StyledClickable>
<ReactLink onClick={onClick} to={href}>
{children}
</ReactLink>
</StyledClickable>
);
}
export default Link;

View File

@ -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<styledEditModeWrapperProps>`
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);
}}
>
<Container isEditMode={isEditMode}>{children}</Container>
<Container shouldAlignRight={shouldAlignRight} isEditMode={isEditMode}>
{children}
</Container>
</StyledWrapper>
);
}

View File

@ -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<StyledEditModeProps>`
width: 100%;
border: none;
outline: none;
&::placeholder {
font-weight: bold;
color: ${(props) => props.theme.text20};
}
`;
function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(value);
const [isEditMode, setIsEditMode] = useState(false);
const onEditModeChange = (isEditMode: boolean) => {
setIsEditMode(isEditMode);
if (isEditMode) {
inputRef.current?.focus();
}
};
return (
<EditableCellWrapper onEditModeChange={onEditModeChange}>
{isEditMode ? (
<StyledEditInplaceInput
autoFocus
isEditMode={isEditMode}
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
) : (
<div>
{isValidPhoneNumber(inputValue) ? (
<Link
href={parsePhoneNumber(inputValue, 'FR')?.getURI()}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{parsePhoneNumber(inputValue, 'FR')?.formatInternational() ||
inputValue}
</Link>
) : (
<Link href="#">{inputValue}</Link>
)}
</div>
)}
</EditableCellWrapper>
);
}
export default EditablePhone;

View File

@ -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<StyledEditModeProps>`
}
`;
function EditableCell({ content, placeholder, changeHandler }: OwnProps) {
const StyledNoEditText = styled.div`
max-width: 200px;
`;
function EditableCell({
content,
placeholder,
changeHandler,
shouldAlignRight,
}: OwnProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [inputValue, setInputValue] = useState(content);
const [isEditMode, setIsEditMode] = useState(false);
@ -37,17 +47,24 @@ function EditableCell({ content, placeholder, changeHandler }: OwnProps) {
};
return (
<EditableCellWrapper onEditModeChange={onEditModeChange}>
<StyledInplaceInput
isEditMode={isEditMode}
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
<EditableCellWrapper
onEditModeChange={onEditModeChange}
shouldAlignRight={shouldAlignRight}
>
{isEditMode ? (
<StyledInplaceInput
isEditMode={isEditMode}
placeholder={placeholder || ''}
ref={inputRef}
value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
changeHandler(event.target.value);
}}
/>
) : (
<StyledNoEditText>{inputValue}</StyledNoEditText>
)}
</EditableCellWrapper>
);
}

View File

@ -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<typeof EditablePhone> = (args: OwnProps) => {
return (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditablePhone {...args} />
</div>
</ThemeProvider>
</MemoryRouter>
);
};
export const EditablePhoneStory = Template.bind({});
EditablePhoneStory.args = {
placeholder: 'Test placeholder',
value: '+33657646543',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -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(
<EditablePhoneStory value="+33786405315" changeHandler={func} />,
);
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');
});

View File

@ -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(
<EditableTextStory content="test" changeHandler={func} />,

View File

@ -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 = [
<EditableText
placeholder="Email"
content={props.row.original.email}
changeHandler={(value) => {
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: () => <ColumnHead viewName="Phone" viewIcon={<FaPhone />} />,
cell: (props) => (
<ClickableCell
href={parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.getURI()}
>
{parsePhoneNumber(
props.row.original.phone,
props.row.original.countryCode as CountryCode,
)?.formatInternational() || props.row.original.phone}
</ClickableCell>
<EditablePhone
placeholder="Phone"
value={props.row.original.phone}
changeHandler={(value: string) => {
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: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
cell: (props) => (
<ClickableCell href="#">{props.row.original.city}</ClickableCell>
<EditableText
shouldAlignRight={true}
placeholder="City"
content={props.row.original.city}
changeHandler={(value: string) => {
const person = props.row.original;
person.city = value;
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
}}
/>
),
}),
];