mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 17:12:53 +03:00
Enable add person on People Table (#111)
Add possibility to add Person on People table
This commit is contained in:
parent
50a4a97145
commit
48a75358b4
@ -50,7 +50,7 @@ describe('mapCompany', () => {
|
||||
expect(company.name).toBe('ACME');
|
||||
expect(company.domain_name).toBe('exmaple.com');
|
||||
expect(company.creationDate).toEqual(now);
|
||||
expect(company.accountOwner).toBeUndefined();
|
||||
expect(company.accountOwner).toBeNull();
|
||||
expect(company.employees).toBe(10);
|
||||
expect(company.address).toBe(
|
||||
'1 Infinite Loop, 95014 Cupertino, California, USA',
|
||||
|
@ -54,7 +54,7 @@ export const mapCompany = (company: GraphqlQueryCompany): Company => ({
|
||||
email: company.account_owner.email,
|
||||
displayName: company.account_owner.displayName,
|
||||
}
|
||||
: undefined,
|
||||
: null,
|
||||
creationDate: new Date(company.created_at),
|
||||
opportunities: [],
|
||||
});
|
||||
|
@ -44,7 +44,6 @@ describe('mapPerson', () => {
|
||||
name: '',
|
||||
icon: '',
|
||||
},
|
||||
countryCode: '',
|
||||
});
|
||||
expect(person.firstname).toBe('John');
|
||||
});
|
||||
|
@ -7,12 +7,11 @@ export type Person = {
|
||||
lastname: string;
|
||||
picture?: string;
|
||||
email: string;
|
||||
company: PartialCompany;
|
||||
company: PartialCompany | null;
|
||||
phone: string;
|
||||
creationDate: Date;
|
||||
pipe: Pipe;
|
||||
pipe: Pipe | null;
|
||||
city: string;
|
||||
countryCode: string;
|
||||
};
|
||||
|
||||
export type GraphqlQueryPerson = {
|
||||
@ -45,7 +44,10 @@ export type GraphqlMutationPerson = {
|
||||
};
|
||||
|
||||
export const mapPerson = (person: GraphqlQueryPerson): Person => ({
|
||||
...person,
|
||||
id: person.id,
|
||||
email: person.email,
|
||||
phone: person.phone,
|
||||
city: person.city,
|
||||
firstname: person.firstname,
|
||||
lastname: person.lastname,
|
||||
creationDate: new Date(person.created_at),
|
||||
@ -54,12 +56,13 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
icon: '💰',
|
||||
},
|
||||
company: {
|
||||
id: person.company.id,
|
||||
name: person.company.name,
|
||||
domain_name: person.company.domain_name,
|
||||
},
|
||||
countryCode: 'FR',
|
||||
company: person.company
|
||||
? {
|
||||
id: person.company.id,
|
||||
name: person.company.name,
|
||||
domain_name: person.company.domain_name,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({
|
||||
@ -67,6 +70,6 @@ export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({
|
||||
firstname: person.firstname,
|
||||
lastname: person.lastname,
|
||||
created_at: person.creationDate.toUTCString(),
|
||||
company_id: person.company.id,
|
||||
company_id: person.company?.id,
|
||||
__typename: 'People',
|
||||
});
|
||||
|
@ -28,6 +28,20 @@ const mocks = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_COMPANIES,
|
||||
variables: {
|
||||
orderBy: [{ created_at: 'desc' }],
|
||||
where: {},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
companies: mockData,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const CompaniesDefault = () => (
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { FaRegUser, FaList } from 'react-icons/fa';
|
||||
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
|
||||
import Table from '../../components/table/Table';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
availableFilters,
|
||||
peopleColumns,
|
||||
availableSorts,
|
||||
usePeopleColumns,
|
||||
} from './people-table';
|
||||
import { mapPerson } from '../../interfaces/person.interface';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Person, mapPerson } from '../../interfaces/person.interface';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
PeopleSelectedSortType,
|
||||
defaultOrderBy,
|
||||
insertPerson,
|
||||
usePeopleQuery,
|
||||
} from '../../services/people';
|
||||
import { useSearch } from '../../services/search/search';
|
||||
@ -32,6 +34,7 @@ function People() {
|
||||
const [orderBy, setOrderBy] = useState(defaultOrderBy);
|
||||
const [where, setWhere] = useState<People_Bool_Exp>({});
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
|
||||
const [internalData, setInternalData] = useState<Array<Person>>([]);
|
||||
|
||||
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
|
||||
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
|
||||
@ -44,11 +47,34 @@ function People() {
|
||||
[],
|
||||
);
|
||||
|
||||
const addEmptyRow = useCallback(() => {
|
||||
console.log('add row');
|
||||
}, []);
|
||||
const { data, loading, refetch } = usePeopleQuery(orderBy, where);
|
||||
|
||||
const { data } = usePeopleQuery(orderBy, where);
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (data) {
|
||||
setInternalData(data.people.map(mapPerson));
|
||||
}
|
||||
}
|
||||
}, [loading, setInternalData, data]);
|
||||
|
||||
const addEmptyRow = useCallback(() => {
|
||||
const newCompany: Person = {
|
||||
id: uuidv4(),
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
company: null,
|
||||
pipe: null,
|
||||
creationDate: new Date(),
|
||||
city: '',
|
||||
};
|
||||
insertPerson(newCompany);
|
||||
setInternalData([newCompany, ...internalData]);
|
||||
refetch();
|
||||
}, [internalData, setInternalData, refetch]);
|
||||
|
||||
const peopleColumns = usePeopleColumns();
|
||||
|
||||
return (
|
||||
<WithTopBarContainer
|
||||
@ -59,7 +85,7 @@ function People() {
|
||||
<StyledPeopleContainer>
|
||||
{
|
||||
<Table
|
||||
data={data ? data.people.map(mapPerson) : []}
|
||||
data={internalData}
|
||||
columns={peopleColumns}
|
||||
viewName="All People"
|
||||
viewIcon={<FaList />}
|
||||
|
@ -29,6 +29,20 @@ const mocks = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_PEOPLE,
|
||||
variables: {
|
||||
orderBy: [{ created_at: 'desc' }],
|
||||
where: {},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
people: mockData,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
|
||||
|
@ -83,3 +83,24 @@ it('Checks people email edit is updating data', async () => {
|
||||
expect(getByText('john@linkedin.c')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Checks insert data is appending a new line', async () => {
|
||||
const { getByText, getByTestId, container } = render(<PeopleDefault />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('John Doe')).toBeDefined();
|
||||
});
|
||||
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
|
||||
|
||||
expect(tableRows.length).toBe(4);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(getByTestId('add-button'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const tableRows = container.querySelectorAll<HTMLElement>('table tbody tr');
|
||||
|
||||
expect(tableRows.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
@ -38,6 +38,7 @@ import EditableFullName from '../../components/table/editable-cell/EditableFullN
|
||||
import EditableDate from '../../components/table/editable-cell/EditableDate';
|
||||
import EditableRelation from '../../components/table/editable-cell/EditableRelation';
|
||||
import { updatePerson } from '../../services/people';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const availableSorts = [
|
||||
{
|
||||
@ -242,135 +243,150 @@ export const availableFilters = [
|
||||
] satisfies FilterType<People_Bool_Exp>[];
|
||||
|
||||
const columnHelper = createColumnHelper<Person>();
|
||||
export const peopleColumns = [
|
||||
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 />} />,
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('email', {
|
||||
header: () => <ColumnHead viewName="Email" viewIcon={<FaEnvelope />} />,
|
||||
cell: (props) => (
|
||||
<EditableText
|
||||
placeholder="Email"
|
||||
content={props.row.original.email}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.email = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('company', {
|
||||
header: () => (
|
||||
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<EditableRelation<PartialCompany, CompanyChipPropsType>
|
||||
relation={props.row.original.company}
|
||||
searchPlaceholder="Company"
|
||||
ChipComponent={CompanyChip}
|
||||
chipComponentPropsMapper={(
|
||||
company: PartialCompany,
|
||||
): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name,
|
||||
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||
};
|
||||
}}
|
||||
changeHandler={(relation: PartialCompany) => {
|
||||
const person = props.row.original;
|
||||
person.company.id = relation.id;
|
||||
updatePerson(person);
|
||||
}}
|
||||
searchFilter={
|
||||
{
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: () => {
|
||||
return {};
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: {
|
||||
id: company.id,
|
||||
|
||||
export const usePeopleColumns = () => {
|
||||
return useMemo(() => {
|
||||
return [
|
||||
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 />} />,
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('email', {
|
||||
header: () => <ColumnHead viewName="Email" viewIcon={<FaEnvelope />} />,
|
||||
cell: (props) => (
|
||||
<EditableText
|
||||
placeholder="Email"
|
||||
content={props.row.original.email}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.email = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('company', {
|
||||
header: () => (
|
||||
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<EditableRelation<PartialCompany, CompanyChipPropsType>
|
||||
relation={props.row.original.company}
|
||||
searchPlaceholder="Company"
|
||||
ChipComponent={CompanyChip}
|
||||
chipComponentPropsMapper={(
|
||||
company: PartialCompany,
|
||||
): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name,
|
||||
domain_name: company.domain_name,
|
||||
},
|
||||
}),
|
||||
operands: [],
|
||||
} satisfies FilterType<People_Bool_Exp>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('phone', {
|
||||
header: () => <ColumnHead viewName="Phone" viewIcon={<FaPhone />} />,
|
||||
cell: (props) => (
|
||||
<EditablePhone
|
||||
placeholder="Phone"
|
||||
value={props.row.original.phone}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.phone = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('creationDate', {
|
||||
header: () => <ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />,
|
||||
cell: (props) => (
|
||||
<EditableDate
|
||||
value={props.row.original.creationDate}
|
||||
changeHandler={(value: Date) => {
|
||||
const person = props.row.original;
|
||||
person.creationDate = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('city', {
|
||||
header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
|
||||
cell: (props) => (
|
||||
<EditableText
|
||||
editModeHorizontalAlign="right"
|
||||
placeholder="City"
|
||||
content={props.row.original.city}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.city = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
];
|
||||
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||
};
|
||||
}}
|
||||
changeHandler={(relation: PartialCompany) => {
|
||||
const person = props.row.original;
|
||||
if (person.company) {
|
||||
person.company.id = relation.id;
|
||||
} else {
|
||||
person.company = {
|
||||
id: relation.id,
|
||||
name: relation.name,
|
||||
domain_name: relation.domain_name,
|
||||
};
|
||||
}
|
||||
updatePerson(person);
|
||||
}}
|
||||
searchFilter={
|
||||
{
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: () => {
|
||||
return {};
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: {
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domain_name: company.domain_name,
|
||||
},
|
||||
}),
|
||||
operands: [],
|
||||
} satisfies FilterType<People_Bool_Exp>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('phone', {
|
||||
header: () => <ColumnHead viewName="Phone" viewIcon={<FaPhone />} />,
|
||||
cell: (props) => (
|
||||
<EditablePhone
|
||||
placeholder="Phone"
|
||||
value={props.row.original.phone}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.phone = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('creationDate', {
|
||||
header: () => (
|
||||
<ColumnHead viewName="Creation" viewIcon={<FaCalendar />} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<EditableDate
|
||||
value={props.row.original.creationDate}
|
||||
changeHandler={(value: Date) => {
|
||||
const person = props.row.original;
|
||||
person.creationDate = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor('city', {
|
||||
header: () => <ColumnHead viewName="City" viewIcon={<FaMapPin />} />,
|
||||
cell: (props) => (
|
||||
<EditableText
|
||||
editModeHorizontalAlign="right"
|
||||
placeholder="City"
|
||||
content={props.row.original.city}
|
||||
changeHandler={(value: string) => {
|
||||
const person = props.row.original;
|
||||
person.city = value;
|
||||
updatePerson(person);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
@ -44,6 +44,48 @@ export const UPDATE_PERSON = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_PERSON = gql`
|
||||
mutation InsertPerson(
|
||||
$id: uuid
|
||||
$firstname: String
|
||||
$lastname: String
|
||||
$phone: String
|
||||
$city: String
|
||||
$company_id: uuid
|
||||
$email: String
|
||||
$created_at: timestamptz
|
||||
) {
|
||||
insert_people(
|
||||
objects: {
|
||||
id: $id
|
||||
firstname: $firstname
|
||||
lastname: $lastname
|
||||
phone: $phone
|
||||
city: $city
|
||||
company_id: $company_id
|
||||
email: $email
|
||||
created_at: $created_at
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
city
|
||||
company {
|
||||
domain_name
|
||||
name
|
||||
id
|
||||
}
|
||||
email
|
||||
firstname
|
||||
id
|
||||
lastname
|
||||
phone
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export async function updatePerson(
|
||||
person: Person,
|
||||
): Promise<FetchResult<Person>> {
|
||||
@ -53,3 +95,14 @@ export async function updatePerson(
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function insertPerson(
|
||||
person: Person,
|
||||
): Promise<FetchResult<Person>> {
|
||||
const result = await apiClient.mutate({
|
||||
mutation: INSERT_PERSON,
|
||||
variables: mapGqlPerson(person),
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ insert_permissions:
|
||||
check:
|
||||
workspace_id:
|
||||
_eq: x-hasura-workspace-id
|
||||
set:
|
||||
workspace_id: x-hasura-Workspace-Id
|
||||
columns:
|
||||
- city
|
||||
- email
|
||||
|
Loading…
Reference in New Issue
Block a user