Lucas/refactored table state with recoil (#149)

* Fixed ActionBar paddings and added transition on button hover

* Added recoil library for state management

* Refactor table state with recoil :

- Removed table internal states
- Added refetchQueries to plug apollo store directly into tables
- Added an action bar component that manages itself
- Use recoil state and selector for row selection
- Refactored Companies and People tables

* Moved hook

* Cleaned some files

* Fix bug infinite re-compute table row selection

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-05-27 08:41:26 +02:00
committed by GitHub
parent 9a3aa1d3d2
commit 8f88605f32
20 changed files with 238 additions and 212 deletions

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback } from 'react';
import { FaList } from 'react-icons/fa';
import styled from '@emotion/styled';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
@ -6,11 +6,10 @@ import { v4 as uuidv4 } from 'uuid';
import {
CompaniesSelectedSortType,
defaultOrderBy,
deleteCompanies,
insertCompany,
useCompaniesQuery,
} from '../../services/api/companies';
import Table from '../../components/table/Table';
import { EntityTable } from '../../components/table/EntityTable';
import {
Company,
mapToCompany,
@ -21,13 +20,14 @@ import {
reduceSortsToOrderBy,
} from '../../components/table/table-header/helpers';
import { CompanyOrderByWithRelationInput as Companies_Order_By } from '../../generated/graphql';
import ActionBar from '../../components/table/action-bar/ActionBar';
import { SelectedFilterType } from '../../interfaces/filters/interface';
import { BoolExpType } from '../../interfaces/entities/generic.interface';
import { useCompaniesColumns } from './companies-columns';
import { availableSorts } from './companies-sorts';
import { availableFilters } from './companies-filters';
import { TbBuilding } from 'react-icons/tb';
import { EntityTableActionBar } from '../../components/table/action-bar/EntityTableActionBar';
import { TableActionBarButtonDeleteCompanies } from './table/TableActionBarButtonDeleteCompanies';
const StyledCompaniesContainer = styled.div`
display: flex;
@ -37,8 +37,6 @@ const StyledCompaniesContainer = styled.div`
function Companies() {
const [orderBy, setOrderBy] = useState<Companies_Order_By[]>(defaultOrderBy);
const [where, setWhere] = useState<BoolExpType<Company>>({});
const [internalData, setInternalData] = useState<Array<Company>>([]);
const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]);
const updateSorts = useCallback((sorts: Array<CompaniesSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
@ -51,17 +49,11 @@ function Companies() {
[],
);
const { data, loading, refetch } = useCompaniesQuery(orderBy, where);
const { data } = useCompaniesQuery(orderBy, where);
useEffect(() => {
if (!loading) {
if (data) {
setInternalData(data.companies.map(mapToCompany));
}
}
}, [loading, setInternalData, data]);
const companies = data?.companies.map(mapToCompany) ?? [];
const addEmptyRow = useCallback(async () => {
async function handleAddButtonClick() {
const newCompany: Company = {
id: uuidv4(),
name: '',
@ -73,36 +65,22 @@ function Companies() {
accountOwner: null,
__typename: 'companies',
};
await insertCompany(newCompany);
setInternalData([newCompany, ...internalData]);
refetch();
}, [internalData, setInternalData, refetch]);
const deleteRows = useCallback(async () => {
await deleteCompanies(selectedRowIds);
setInternalData([
...internalData.filter((row) => !selectedRowIds.includes(row.id)),
]);
refetch();
if (tableRef.current) {
tableRef.current.resetRowSelection();
}
}, [internalData, selectedRowIds, refetch]);
await insertCompany(newCompany);
}
const companiesColumns = useCompaniesColumns();
const tableRef = useRef<{ resetRowSelection: () => void }>();
return (
<WithTopBarContainer
title="Companies"
icon={<TbBuilding size={16} />}
onAddButtonClick={addEmptyRow}
onAddButtonClick={handleAddButtonClick}
>
<>
<StyledCompaniesContainer>
<Table
ref={tableRef}
data={internalData}
<EntityTable
data={companies}
columns={companiesColumns}
viewName="All Companies"
viewIcon={<FaList />}
@ -110,10 +88,11 @@ function Companies() {
availableFilters={availableFilters}
onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters}
onRowSelectionChange={setSelectedRowIds}
/>
</StyledCompaniesContainer>
{selectedRowIds.length > 0 && <ActionBar onDeleteClick={deleteRows} />}
<EntityTableActionBar>
<TableActionBarButtonDeleteCompanies />
</EntityTableActionBar>
</>
</WithTopBarContainer>
);

View File

@ -0,0 +1,34 @@
import { TbTrash } from 'react-icons/tb';
import { EntityTableActionBarButton } from '../../../components/table/action-bar/EntityTableActionBarButton';
import { useDeleteCompaniesMutation } from '../../../generated/graphql';
import { selectedRowIdsState } from '../../../modules/ui/tables/states/selectedRowIdsState';
import { useRecoilValue } from 'recoil';
import { useResetTableRowSelection } from '../../../modules/ui/tables/hooks/useResetTableRowSelection';
export function TableActionBarButtonDeleteCompanies() {
const selectedRowIds = useRecoilValue(selectedRowIdsState);
const resetRowSelection = useResetTableRowSelection();
const [deleteCompanies] = useDeleteCompaniesMutation({
refetchQueries: ['GetCompanies'],
});
async function handleDeleteClick() {
await deleteCompanies({
variables: {
ids: selectedRowIds,
},
});
resetRowSelection();
}
return (
<EntityTableActionBarButton
label="Delete"
icon={<TbTrash size={16} />}
onClick={handleDeleteClick}
/>
);
}

View File

@ -1,10 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { FaList } from 'react-icons/fa';
import { v4 as uuidv4 } from 'uuid';
import styled from '@emotion/styled';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import Table from '../../components/table/Table';
import { EntityTable } from '../../components/table/EntityTable';
import {
Person,
@ -13,7 +13,6 @@ import {
import {
PeopleSelectedSortType,
defaultOrderBy,
deletePeople,
insertPerson,
usePeopleQuery,
} from '../../services/api/people';
@ -21,13 +20,14 @@ import {
reduceFiltersToWhere,
reduceSortsToOrderBy,
} from '../../components/table/table-header/helpers';
import ActionBar from '../../components/table/action-bar/ActionBar';
import { SelectedFilterType } from '../../interfaces/filters/interface';
import { BoolExpType } from '../../interfaces/entities/generic.interface';
import { usePeopleColumns } from './people-columns';
import { availableSorts } from './people-sorts';
import { availableFilters } from './people-filters';
import { TbUser } from 'react-icons/tb';
import { EntityTableActionBar } from '../../components/table/action-bar/EntityTableActionBar';
import { TableActionBarButtonDeletePeople } from './table/TableActionBarButtonDeletePeople';
const StyledPeopleContainer = styled.div`
display: flex;
@ -38,8 +38,6 @@ const StyledPeopleContainer = styled.div`
function People() {
const [orderBy, setOrderBy] = useState(defaultOrderBy);
const [where, setWhere] = useState<BoolExpType<Person>>({});
const [internalData, setInternalData] = useState<Array<Person>>([]);
const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]);
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
@ -52,17 +50,11 @@ function People() {
[],
);
const { data, loading, refetch } = usePeopleQuery(orderBy, where);
const { data } = usePeopleQuery(orderBy, where);
useEffect(() => {
if (!loading) {
if (data) {
setInternalData(data.people.map(mapToPerson));
}
}
}, [loading, setInternalData, data]);
const people = data?.people.map(mapToPerson) ?? [];
const addEmptyRow = useCallback(async () => {
async function handleAddButtonClick() {
const newPerson: Person = {
__typename: 'people',
id: uuidv4(),
@ -75,36 +67,22 @@ function People() {
creationDate: new Date(),
city: '',
};
await insertPerson(newPerson);
setInternalData([newPerson, ...internalData]);
refetch();
}, [internalData, setInternalData, refetch]);
}
const deleteRows = useCallback(async () => {
await deletePeople(selectedRowIds);
setInternalData([
...internalData.filter((row) => !selectedRowIds.includes(row.id)),
]);
refetch();
if (tableRef.current) {
tableRef.current.resetRowSelection();
}
}, [internalData, selectedRowIds, refetch]);
const tableRef = useRef<{ resetRowSelection: () => void }>();
const peopleColumns = usePeopleColumns();
return (
<WithTopBarContainer
title="People"
icon={<TbUser size={16} />}
onAddButtonClick={addEmptyRow}
onAddButtonClick={handleAddButtonClick}
>
<>
<StyledPeopleContainer>
<Table
ref={tableRef}
data={internalData}
<EntityTable
data={people}
columns={peopleColumns}
viewName="All People"
viewIcon={<FaList />}
@ -112,10 +90,11 @@ function People() {
availableFilters={availableFilters}
onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters}
onRowSelectionChange={setSelectedRowIds}
/>
</StyledPeopleContainer>
{selectedRowIds.length > 0 && <ActionBar onDeleteClick={deleteRows} />}
<EntityTableActionBar>
<TableActionBarButtonDeletePeople />
</EntityTableActionBar>
</>
</WithTopBarContainer>
);

View File

@ -0,0 +1,34 @@
import { TbTrash } from 'react-icons/tb';
import { EntityTableActionBarButton } from '../../../components/table/action-bar/EntityTableActionBarButton';
import { useDeletePeopleMutation } from '../../../generated/graphql';
import { selectedRowIdsState } from '../../../modules/ui/tables/states/selectedRowIdsState';
import { useRecoilValue } from 'recoil';
import { useResetTableRowSelection } from '../../../modules/ui/tables/hooks/useResetTableRowSelection';
export function TableActionBarButtonDeletePeople() {
const selectedRowIds = useRecoilValue(selectedRowIdsState);
const resetRowSelection = useResetTableRowSelection();
const [deletePeople] = useDeletePeopleMutation({
refetchQueries: ['GetPeople'],
});
async function handleDeleteClick() {
await deletePeople({
variables: {
ids: selectedRowIds,
},
});
resetRowSelection();
}
return (
<EntityTableActionBarButton
label="Delete"
icon={<TbTrash size={16} />}
onClick={handleDeleteClick}
/>
);
}