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:
@@ -13,12 +13,9 @@ import {
|
||||
SelectedFilterType,
|
||||
} from '../../interfaces/filters/interface';
|
||||
import { SortType, SelectedSortType } from '../../interfaces/sorts/interface';
|
||||
|
||||
declare module 'react' {
|
||||
function forwardRef<T, P = object>(
|
||||
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
|
||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
||||
}
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { currentRowSelectionState } from '../../modules/ui/tables/states/rowSelectionState';
|
||||
import { useResetTableRowSelection } from '../../modules/ui/tables/hooks/useResetTableRowSelection';
|
||||
|
||||
type OwnProps<
|
||||
TData extends { id: string; __typename: 'companies' | 'people' },
|
||||
@@ -87,52 +84,41 @@ const StyledTableScrollableContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Table = <
|
||||
export function EntityTable<
|
||||
TData extends { id: string; __typename: 'companies' | 'people' },
|
||||
SortField,
|
||||
>(
|
||||
{
|
||||
data,
|
||||
columns,
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
availableFilters,
|
||||
onSortsUpdate,
|
||||
onFiltersUpdate,
|
||||
onRowSelectionChange,
|
||||
}: OwnProps<TData, SortField>,
|
||||
ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>,
|
||||
) => {
|
||||
const [internalRowSelection, setInternalRowSelection] = React.useState({});
|
||||
>({
|
||||
data,
|
||||
columns,
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
availableFilters,
|
||||
onSortsUpdate,
|
||||
onFiltersUpdate,
|
||||
}: OwnProps<TData, SortField>) {
|
||||
const [currentRowSelection, setCurrentRowSelection] = useRecoilState(
|
||||
currentRowSelectionState,
|
||||
);
|
||||
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
|
||||
React.useEffect(() => {
|
||||
resetTableRowSelection();
|
||||
}, [resetTableRowSelection]);
|
||||
|
||||
const table = useReactTable<TData>({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
rowSelection: internalRowSelection,
|
||||
rowSelection: currentRowSelection,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setInternalRowSelection,
|
||||
onRowSelectionChange: setCurrentRowSelection,
|
||||
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 (
|
||||
<StyledTableWithHeader>
|
||||
<TableHeader
|
||||
@@ -186,6 +172,4 @@ const Table = <
|
||||
</StyledTableScrollableContainer>
|
||||
</StyledTableWithHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(Table);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
+2
-3
@@ -16,6 +16,7 @@ const StyledButton = styled.div`
|
||||
|
||||
padding: ${(props) => props.theme.spacing(2)};
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.tertiaryBackground};
|
||||
@@ -27,7 +28,7 @@ const StyledButtonLabel = styled.div`
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
function ActionBarButton({ label, icon, onClick }: OwnProps) {
|
||||
export function EntityTableActionBarButton({ label, icon, onClick }: OwnProps) {
|
||||
return (
|
||||
<StyledButton onClick={onClick}>
|
||||
{icon}
|
||||
@@ -35,5 +36,3 @@ function ActionBarButton({ label, icon, onClick }: OwnProps) {
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionBarButton;
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user