Make full name editable on People page (#100)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Charles Bochet 2023-05-04 18:38:29 +02:00 committed by GitHub
parent f6b691945c
commit 89dc5b4d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 26 deletions

View File

@ -0,0 +1,75 @@
import styled from '@emotion/styled';
import { ChangeEvent, useRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper';
import PersonChip from '../../chips/PersonChip';
type OwnProps = {
firstname: string;
lastname: string;
changeHandler: (firstname: string, lastname: string) => void;
};
const StyledContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
& > input:last-child {
padding-left: ${(props) => props.theme.spacing(2)};
border-left: 1px solid ${(props) => props.theme.primaryBorder};
}
`;
const StyledEditInplaceInput = styled.input`
width: 45%;
border: none;
outline: none;
height: 18px;
&::placeholder {
font-weight: bold;
color: ${(props) => props.theme.text20};
}
`;
function EditableFullName({ firstname, lastname, changeHandler }: OwnProps) {
const firstnameInputRef = useRef<HTMLInputElement>(null);
const [firstnameValue, setFirstnameValue] = useState(firstname);
const [lastnameValue, setLastnameValue] = useState(lastname);
const [isEditMode, setIsEditMode] = useState(false);
return (
<EditableCellWrapper
onEditModeChange={(editMode: boolean) => setIsEditMode(editMode)}
>
{isEditMode ? (
<StyledContainer>
<StyledEditInplaceInput
autoFocus
placeholder="Firstname"
ref={firstnameInputRef}
value={firstnameValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFirstnameValue(event.target.value);
changeHandler(event.target.value, lastnameValue);
}}
/>
<StyledEditInplaceInput
autoFocus
placeholder={'Lastname'}
ref={firstnameInputRef}
value={lastnameValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setLastnameValue(event.target.value);
changeHandler(firstnameValue, event.target.value);
}}
/>
</StyledContainer>
) : (
<PersonChip name={firstnameValue + ' ' + lastnameValue} />
)}
</EditableCellWrapper>
);
}
export default EditableFullName;

View File

@ -41,9 +41,6 @@ function EditableCell({
const onEditModeChange = (isEditMode: boolean) => { const onEditModeChange = (isEditMode: boolean) => {
setIsEditMode(isEditMode); setIsEditMode(isEditMode);
if (isEditMode) {
inputRef.current?.focus();
}
}; };
return ( return (
@ -55,6 +52,7 @@ function EditableCell({
<StyledInplaceInput <StyledInplaceInput
isEditMode={isEditMode} isEditMode={isEditMode}
placeholder={placeholder || ''} placeholder={placeholder || ''}
autoFocus
ref={inputRef} ref={inputRef}
value={inputValue} value={inputValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {

View File

@ -0,0 +1,39 @@
import EditableFullName from '../EditableFullName';
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: 'EditableFullName',
component: EditableFullName,
};
type OwnProps = {
firstname: string;
lastname: string;
changeHandler: (firstname: string, lastname: string) => void;
};
export default component;
const Template: StoryFn<typeof EditableFullName> = (args: OwnProps) => {
return (
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableFullName {...args} />
</div>
</ThemeProvider>
</MemoryRouter>
);
};
export const EditableFullNameStory = Template.bind({});
EditableFullNameStory.args = {
firstname: 'John',
lastname: 'Doe',
changeHandler: () => {
console.log('changed');
},
};

View File

@ -0,0 +1,41 @@
import { fireEvent, render } from '@testing-library/react';
import { EditableFullNameStory } from '../__stories__/EditableFullName.stories';
it('Checks the EditableFullName editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(
<EditableFullNameStory
firstname="Jone"
lastname="Doe"
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 firstnameInput = parent.querySelector('input:first-child');
if (!firstnameInput) {
throw new Error('Editable input not found');
}
fireEvent.change(firstnameInput, { target: { value: 'Jo' } });
expect(func).toBeCalledWith('Jo', 'Doe');
const lastnameInput = parent.querySelector('input:last-child');
if (!lastnameInput) {
throw new Error('Editable input not found');
}
fireEvent.change(lastnameInput, { target: { value: 'Do' } });
expect(func).toBeCalledWith('Jo', 'Do');
});

View File

@ -18,13 +18,14 @@ describe('mapPerson', () => {
}, },
__typename: '', __typename: '',
}); });
expect(person.fullName).toBe('John Doe'); expect(person.firstname).toBe('John');
}); });
it('should map person back', () => { it('should map person back', () => {
const person = mapGqlPerson({ const person = mapGqlPerson({
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
fullName: 'John Doe', firstname: 'John',
lastname: 'Doe',
email: '', email: '',
phone: '', phone: '',
city: '', city: '',

View File

@ -3,7 +3,8 @@ import { Pipe } from './pipe.interface';
export type Person = { export type Person = {
id: string; id: string;
fullName: string; firstname: string;
lastname: string;
picture?: string; picture?: string;
email: string; email: string;
company: Omit< company: Omit<
@ -47,14 +48,15 @@ export type GraphqlMutationPerson = {
}; };
export const mapPerson = (person: GraphqlQueryPerson): Person => ({ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
fullName: `${person.firstname} ${person.lastname}`, ...person,
firstname: person.firstname,
lastname: person.lastname,
creationDate: new Date(person.created_at), creationDate: new Date(person.created_at),
pipe: { pipe: {
name: 'coucou', name: 'coucou',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
icon: '💰', icon: '💰',
}, },
...person,
company: { company: {
id: person.company.id, id: person.company.id,
name: person.company.name, name: person.company.name,
@ -64,10 +66,10 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
}); });
export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({ export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({
firstname: person.fullName.split(' ').shift() || '', ...(person as Omit<Person, 'company'>),
lastname: person.fullName.split(' ').slice(1).join(' '), firstname: person.firstname,
lastname: person.lastname,
created_at: person.creationDate.toUTCString(), created_at: person.creationDate.toUTCString(),
company_id: person.company.id, company_id: person.company.id,
...(person as Omit<Person, 'company'>),
__typename: 'People', __typename: 'People',
}); });

View File

@ -13,9 +13,7 @@ import { createColumnHelper } from '@tanstack/react-table';
import ClickableCell from '../../components/table/ClickableCell'; import ClickableCell from '../../components/table/ClickableCell';
import ColumnHead from '../../components/table/ColumnHead'; import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox'; import Checkbox from '../../components/form/Checkbox';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import CompanyChip from '../../components/chips/CompanyChip'; import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface'; import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
import PipeChip from '../../components/chips/PipeChip'; import PipeChip from '../../components/chips/PipeChip';
import EditableText from '../../components/table/editable-cell/EditableText'; import EditableText from '../../components/table/editable-cell/EditableText';
@ -31,6 +29,7 @@ import {
} from '../../services/search/search'; } from '../../services/search/search';
import { GraphqlQueryCompany } from '../../interfaces/company.interface'; import { GraphqlQueryCompany } from '../../interfaces/company.interface';
import EditablePhone from '../../components/table/editable-cell/EditablePhone'; import EditablePhone from '../../components/table/editable-cell/EditablePhone';
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
export const availableSorts = [ export const availableSorts = [
{ {
@ -132,21 +131,30 @@ export const availableFilters = [
const columnHelper = createColumnHelper<Person>(); const columnHelper = createColumnHelper<Person>();
export const peopleColumns = [ export const peopleColumns = [
columnHelper.accessor('fullName', { columnHelper.accessor('id', {
header: () => (
<Checkbox id={`person-select-all`} name={`person-select-all`} />
),
cell: (props) => (
<Checkbox
id={`person-selected-${props.row.original.email}`}
name={`person-selected-${props.row.original.email}`}
/>
),
}),
columnHelper.accessor('firstname', {
header: () => <ColumnHead viewName="People" viewIcon={<FaRegUser />} />, header: () => <ColumnHead viewName="People" viewIcon={<FaRegUser />} />,
cell: (props) => ( cell: (props) => (
<> <EditableFullName
<HorizontalyAlignedContainer> firstname={props.row.original.firstname}
<Checkbox lastname={props.row.original.lastname}
id={`person-selected-${props.row.original.email}`} changeHandler={(firstName: string, lastName: string) => {
name={`person-selected-${props.row.original.email}`} const person = props.row.original;
/> person.firstname = firstName;
<PersonChip person.lastname = lastName;
name={props.row.original.fullName} updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
picture={props.row.original.picture} }}
/> />
</HorizontalyAlignedContainer>
</>
), ),
}), }),
columnHelper.accessor('email', { columnHelper.accessor('email', {