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:
25
front/package-lock.json
generated
25
front/package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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);
|
|
||||||
@ -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>;
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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' {
|
||||||
|
|||||||
@ -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>>,
|
||||||
@ -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],
|
||||||
|
);
|
||||||
|
}
|
||||||
7
front/src/modules/ui/tables/states/rowSelectionState.ts
Normal file
7
front/src/modules/ui/tables/states/rowSelectionState.ts
Normal 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: {},
|
||||||
|
});
|
||||||
13
front/src/modules/ui/tables/states/selectedRowIdsState.ts
Normal file
13
front/src/modules/ui/tables/states/selectedRowIdsState.ts
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user