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) => {
|
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>) => {
|
||||||
|
@ -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: '',
|
__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: '',
|
||||||
|
@ -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',
|
||||||
});
|
});
|
||||||
|
@ -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', {
|
||||||
|
Loading…
Reference in New Issue
Block a user