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

@ -25,6 +25,7 @@
"react-hotkeys-hook": "^4.4.0", "react-hotkeys-hook": "^4.4.0",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"recoil": "^0.7.7",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@ -15893,6 +15894,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
},
"node_modules/handle-thing": { "node_modules/handle-thing": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -25407,6 +25413,25 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/recoil": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
"integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
"dependencies": {
"hamt_plus": "1.0.2"
},
"peerDependencies": {
"react": ">=16.13.1"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/recursive-readdir": { "node_modules/recursive-readdir": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",

View File

@ -20,6 +20,7 @@
"react-hotkeys-hook": "^4.4.0", "react-hotkeys-hook": "^4.4.0",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-router-dom": "^6.4.4", "react-router-dom": "^6.4.4",
"recoil": "^0.7.7",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@ -1,6 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { DoubleTextInput } from '../inputs/DoubleTextInput'; import { DoubleTextInput } from '../inputs/DoubleTextInput';
import { useListenClickOutsideArrayOfRef } from '../../modules/ui/hooks/useListenClickOutsideArrayOfRef'; import { useListenClickOutsideArrayOfRef } from '../../modules/ui/common/hooks/useListenClickOutsideArrayOfRef';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { CellBaseContainer } from '../editable-cell/CellBaseContainer'; import { CellBaseContainer } from '../editable-cell/CellBaseContainer';
import { CellEditModeContainer } from '../editable-cell/CellEditModeContainer'; import { CellEditModeContainer } from '../editable-cell/CellEditModeContainer';

View File

@ -13,12 +13,9 @@ import {
SelectedFilterType, SelectedFilterType,
} from '../../interfaces/filters/interface'; } from '../../interfaces/filters/interface';
import { SortType, SelectedSortType } from '../../interfaces/sorts/interface'; import { SortType, SelectedSortType } from '../../interfaces/sorts/interface';
import { useRecoilState } from 'recoil';
declare module 'react' { import { currentRowSelectionState } from '../../modules/ui/tables/states/rowSelectionState';
function forwardRef<T, P = object>( import { useResetTableRowSelection } from '../../modules/ui/tables/hooks/useResetTableRowSelection';
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}
type OwnProps< type OwnProps<
TData extends { id: string; __typename: 'companies' | 'people' }, TData extends { id: string; __typename: 'companies' | 'people' },
@ -87,52 +84,41 @@ const StyledTableScrollableContainer = styled.div`
flex: 1; flex: 1;
`; `;
const Table = < export function EntityTable<
TData extends { id: string; __typename: 'companies' | 'people' }, TData extends { id: string; __typename: 'companies' | 'people' },
SortField, SortField,
>( >({
{ data,
data, columns,
columns, viewName,
viewName, viewIcon,
viewIcon, availableSorts,
availableSorts, availableFilters,
availableFilters, onSortsUpdate,
onSortsUpdate, onFiltersUpdate,
onFiltersUpdate, }: OwnProps<TData, SortField>) {
onRowSelectionChange, const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
}: OwnProps<TData, SortField>, currentRowSelectionState,
ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>, );
) => {
const [internalRowSelection, setInternalRowSelection] = React.useState({}); const resetTableRowSelection = useResetTableRowSelection();
React.useEffect(() => {
resetTableRowSelection();
}, [resetTableRowSelection]);
const table = useReactTable<TData>({ const table = useReactTable<TData>({
data, data,
columns, columns,
state: { state: {
rowSelection: internalRowSelection, rowSelection: currentRowSelection,
}, },
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
enableRowSelection: true, enableRowSelection: true,
onRowSelectionChange: setInternalRowSelection, onRowSelectionChange: setCurrentRowSelection,
getRowId: (row) => row.id, getRowId: (row) => row.id,
}); });
const selectedRows = table.getSelectedRowModel().rows;
React.useEffect(() => {
const selectedRowIds = selectedRows.map((row) => row.original.id);
onRowSelectionChange && onRowSelectionChange(selectedRowIds);
}, [onRowSelectionChange, selectedRows]);
React.useImperativeHandle(ref, () => {
return {
resetRowSelection: () => {
table.resetRowSelection();
},
};
});
return ( return (
<StyledTableWithHeader> <StyledTableWithHeader>
<TableHeader <TableHeader
@ -186,6 +172,4 @@ const Table = <
</StyledTableScrollableContainer> </StyledTableScrollableContainer>
</StyledTableWithHeader> </StyledTableWithHeader>
); );
}; }
export default React.forwardRef(Table);

View File

@ -1,39 +0,0 @@
import styled from '@emotion/styled';
import ActionBarButton from './ActionBarButton';
import { TbTrash } from 'react-icons/tb';
type OwnProps = {
onDeleteClick: () => void;
};
const StyledContainer = styled.div`
display: flex;
position: absolute;
z-index: 1;
height: 48px;
bottom: 38px;
background: ${(props) => props.theme.secondaryBackground};
align-items: center;
padding-left: ${(props) => props.theme.spacing(4)};
padding-right: ${(props) => props.theme.spacing(4)};
color: ${(props) => props.theme.red};
left: 50%;
transform: translateX(-50%);
border-radius: 8px;
border: 1px solid ${(props) => props.theme.primaryBorder};
`;
function ActionBar({ onDeleteClick }: OwnProps) {
return (
<StyledContainer>
<ActionBarButton
label="Delete"
icon={<TbTrash size={16} />}
onClick={onDeleteClick}
/>
</StyledContainer>
);
}
export default ActionBar;

View File

@ -0,0 +1,36 @@
import styled from '@emotion/styled';
import React from 'react';
import { useRecoilValue } from 'recoil';
import { selectedRowIdsState } from '../../../modules/ui/tables/states/selectedRowIdsState';
type OwnProps = {
children: React.ReactNode | React.ReactNode[];
};
const StyledContainer = styled.div`
display: flex;
position: absolute;
z-index: 1;
height: 48px;
bottom: 38px;
background: ${(props) => props.theme.secondaryBackground};
align-items: center;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
color: ${(props) => props.theme.red};
left: 50%;
transform: translateX(-50%);
border-radius: 8px;
border: 1px solid ${(props) => props.theme.primaryBorder};
`;
export function EntityTableActionBar({ children }: OwnProps) {
const selectedRowIds = useRecoilValue(selectedRowIdsState);
if (selectedRowIds.length === 0) {
return <></>;
}
return <StyledContainer>{children}</StyledContainer>;
}

View File

@ -16,6 +16,7 @@ const StyledButton = styled.div`
padding: ${(props) => props.theme.spacing(2)}; padding: ${(props) => props.theme.spacing(2)};
border-radius: 4px; border-radius: 4px;
transition: background 0.1s ease;
&:hover { &:hover {
background: ${(props) => props.theme.tertiaryBackground}; background: ${(props) => props.theme.tertiaryBackground};
@ -27,7 +28,7 @@ const StyledButtonLabel = styled.div`
font-weight: 500; font-weight: 500;
`; `;
function ActionBarButton({ label, icon, onClick }: OwnProps) { export function EntityTableActionBarButton({ label, icon, onClick }: OwnProps) {
return ( return (
<StyledButton onClick={onClick}> <StyledButton onClick={onClick}>
{icon} {icon}
@ -35,5 +36,3 @@ function ActionBarButton({ label, icon, onClick }: OwnProps) {
</StyledButton> </StyledButton>
); );
} }
export default ActionBarButton;

View File

@ -1,30 +0,0 @@
import ActionBar from '../ActionBar';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'ActionBar',
component: ActionBar,
};
type OwnProps = {
onDeleteClick: () => void;
};
export default component;
const Template: StoryFn<typeof ActionBar> = (args: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<ActionBar {...args} />
</ThemeProvider>
);
};
export const ActionBarStory = Template.bind({});
ActionBarStory.args = {
onDeleteClick: () => {
console.log('deleted');
},
};

View File

@ -1,17 +0,0 @@
import { fireEvent, render } from '@testing-library/react';
import { ActionBarStory } from '../__stories__/ActionBar.stories';
import { act } from 'react-dom/test-utils';
it('Checks the ActionBar editing event bubbles up', async () => {
const deleteFunc = jest.fn(() => null);
const { getByText } = render(<ActionBarStory onDeleteClick={deleteFunc} />);
expect(getByText('Delete')).toBeInTheDocument();
act(() => {
fireEvent.click(getByText('Delete'));
});
expect(deleteFunc).toHaveBeenCalled();
});

View File

@ -7,18 +7,21 @@ import { ApolloProvider } from '@apollo/client';
import '@emotion/react'; import '@emotion/react';
import { ThemeType } from './layout/styles/themes'; import { ThemeType } from './layout/styles/themes';
import { apiClient } from './apollo'; import { apiClient } from './apollo';
import { RecoilRoot } from 'recoil';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
root.render( root.render(
<ApolloProvider client={apiClient}> <RecoilRoot>
<BrowserRouter> <ApolloProvider client={apiClient}>
<StrictMode> <BrowserRouter>
<App /> <StrictMode>
</StrictMode> <App />
</BrowserRouter> </StrictMode>
</ApolloProvider>, </BrowserRouter>
</ApolloProvider>
</RecoilRoot>,
); );
declare module '@emotion/react' { declare module '@emotion/react' {

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { isDefined } from '../../utils/type-guards/isDefined'; import { isDefined } from '../../../utils/type-guards/isDefined';
export function useListenClickOutsideArrayOfRef<T extends HTMLElement>( export function useListenClickOutsideArrayOfRef<T extends HTMLElement>(
arrayOfRef: Array<React.RefObject<T>>, arrayOfRef: Array<React.RefObject<T>>,

View File

@ -0,0 +1,16 @@
import { useSetRecoilState } from 'recoil';
import { currentRowSelectionState } from '../states/rowSelectionState';
import { useCallback, useMemo } from 'react';
export function useResetTableRowSelection() {
const setCurrentRowSelectionState = useSetRecoilState(
currentRowSelectionState,
);
return useCallback(
function resetCurrentRowSelection() {
setCurrentRowSelectionState({});
},
[setCurrentRowSelectionState],
);
}

View File

@ -0,0 +1,7 @@
import { RowSelectionState } from '@tanstack/react-table';
import { atom } from 'recoil';
export const currentRowSelectionState = atom<RowSelectionState>({
key: 'ui/table-row-selection-state',
default: {},
});

View File

@ -0,0 +1,13 @@
import { selector } from 'recoil';
import { currentRowSelectionState } from './rowSelectionState';
export const selectedRowIdsState = selector<string[]>({
key: 'ui/table-selected-row-ids',
get: ({ get }) => {
const currentRowSelection = get(currentRowSelectionState);
return Object.keys(currentRowSelection).filter(
(key) => currentRowSelection[key] === true,
);
},
});

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

View File

@ -94,6 +94,7 @@ export async function insertCompany(
const result = await apiClient.mutate({ const result = await apiClient.mutate({
mutation: INSERT_COMPANY, mutation: INSERT_COMPANY,
variables: mapToGqlCompany(company), variables: mapToGqlCompany(company),
refetchQueries: ['GetCompanies'],
}); });
return result; return result;

View File

@ -106,6 +106,7 @@ export async function insertPerson(
const result = await apiClient.mutate({ const result = await apiClient.mutate({
mutation: INSERT_PERSON, mutation: INSERT_PERSON,
variables: mapToGqlPerson(person), variables: mapToGqlPerson(person),
refetchQueries: ['GetPeople'],
}); });
return result; return result;