mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 09:02:11 +03:00
Make full name editable on People page (#100)
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
f6b691945c
commit
89dc5b4d60
@ -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;
|
@ -41,9 +41,6 @@ function EditableCell({
|
||||
|
||||
const onEditModeChange = (isEditMode: boolean) => {
|
||||
setIsEditMode(isEditMode);
|
||||
if (isEditMode) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -55,6 +52,7 @@ function EditableCell({
|
||||
<StyledInplaceInput
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -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');
|
||||
},
|
||||
};
|
@ -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');
|
||||
});
|
@ -18,13 +18,14 @@ describe('mapPerson', () => {
|
||||
},
|
||||
__typename: '',
|
||||
});
|
||||
expect(person.fullName).toBe('John Doe');
|
||||
expect(person.firstname).toBe('John');
|
||||
});
|
||||
|
||||
it('should map person back', () => {
|
||||
const person = mapGqlPerson({
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
fullName: 'John Doe',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
email: '',
|
||||
phone: '',
|
||||
city: '',
|
||||
|
@ -3,7 +3,8 @@ import { Pipe } from './pipe.interface';
|
||||
|
||||
export type Person = {
|
||||
id: string;
|
||||
fullName: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
picture?: string;
|
||||
email: string;
|
||||
company: Omit<
|
||||
@ -47,14 +48,15 @@ export type GraphqlMutationPerson = {
|
||||
};
|
||||
|
||||
export const mapPerson = (person: GraphqlQueryPerson): Person => ({
|
||||
fullName: `${person.firstname} ${person.lastname}`,
|
||||
...person,
|
||||
firstname: person.firstname,
|
||||
lastname: person.lastname,
|
||||
creationDate: new Date(person.created_at),
|
||||
pipe: {
|
||||
name: 'coucou',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
icon: '💰',
|
||||
},
|
||||
...person,
|
||||
company: {
|
||||
id: person.company.id,
|
||||
name: person.company.name,
|
||||
@ -64,10 +66,10 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
|
||||
});
|
||||
|
||||
export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({
|
||||
firstname: person.fullName.split(' ').shift() || '',
|
||||
lastname: person.fullName.split(' ').slice(1).join(' '),
|
||||
...(person as Omit<Person, 'company'>),
|
||||
firstname: person.firstname,
|
||||
lastname: person.lastname,
|
||||
created_at: person.creationDate.toUTCString(),
|
||||
company_id: person.company.id,
|
||||
...(person as Omit<Person, 'company'>),
|
||||
__typename: 'People',
|
||||
});
|
||||
|
@ -13,9 +13,7 @@ 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 HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
|
||||
import CompanyChip from '../../components/chips/CompanyChip';
|
||||
import PersonChip from '../../components/chips/PersonChip';
|
||||
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
|
||||
import PipeChip from '../../components/chips/PipeChip';
|
||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||
@ -31,6 +29,7 @@ import {
|
||||
} from '../../services/search/search';
|
||||
import { GraphqlQueryCompany } from '../../interfaces/company.interface';
|
||||
import EditablePhone from '../../components/table/editable-cell/EditablePhone';
|
||||
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
|
||||
|
||||
export const availableSorts = [
|
||||
{
|
||||
@ -132,21 +131,30 @@ export const availableFilters = [
|
||||
|
||||
const columnHelper = createColumnHelper<Person>();
|
||||
export const peopleColumns = [
|
||||
columnHelper.accessor('fullName', {
|
||||
header: () => <ColumnHead viewName="People" viewIcon={<FaRegUser />} />,
|
||||
columnHelper.accessor('id', {
|
||||
header: () => (
|
||||
<Checkbox id={`person-select-all`} name={`person-select-all`} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<>
|
||||
<HorizontalyAlignedContainer>
|
||||
<Checkbox
|
||||
id={`person-selected-${props.row.original.email}`}
|
||||
name={`person-selected-${props.row.original.email}`}
|
||||
/>
|
||||
<PersonChip
|
||||
name={props.row.original.fullName}
|
||||
picture={props.row.original.picture}
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('firstname', {
|
||||
header: () => <ColumnHead viewName="People" viewIcon={<FaRegUser />} />,
|
||||
cell: (props) => (
|
||||
<EditableFullName
|
||||
firstname={props.row.original.firstname}
|
||||
lastname={props.row.original.lastname}
|
||||
changeHandler={(firstName: string, lastName: string) => {
|
||||
const person = props.row.original;
|
||||
person.firstname = firstName;
|
||||
person.lastname = lastName;
|
||||
updatePerson(person).catch((error) => console.error(error)); // TODO: handle error
|
||||
}}
|
||||
/>
|
||||
</HorizontalyAlignedContainer>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('email', {
|
||||
|
Loading…
Reference in New Issue
Block a user