Sammy/t 194 aau when i set sort back and forth the (#103)

* bugfix: use original row id in cells to make sure it rerenders

* feature: implement multiple sorts

* bugfix: recreate new array to make sure component rerenders

* feature: orderBy is an array to keep orders

* test: snapshot the searchTemplate methods

* feature: remove the console log and return undefined

* feature: use orderByTemplate instead of hardcoded orderBy

* refactor: move sort and where filters helpers out of service

* refactor: rename file helper

* refactor: move assert function in test
This commit is contained in:
Sammy Teillet
2023-05-05 16:22:47 +02:00
committed by GitHub
parent f022bf8335
commit b8cd842633
21 changed files with 253 additions and 156 deletions

View File

@ -87,7 +87,7 @@ const StyledTableScrollableContainer = styled.div`
flex: 1;
`;
function Table<TData, SortField extends string, FilterProperies>({
function Table<TData extends { id: string }, SortField, FilterProperies>({
data,
columns,
viewName,
@ -140,7 +140,7 @@ function Table<TData, SortField extends string, FilterProperies>({
<tr key={row.id} data-testid={`row-id-${row.index}`}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
<td key={cell.id + row.original.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),

View File

@ -84,10 +84,11 @@ export function FilterDropdownButton<FilterProperties>({
label: selectedFilter.label,
value: value.displayValue,
icon: selectedFilter.icon,
where: selectedFilter.whereTemplate(
selectedFilterOperand,
value.value,
),
where:
selectedFilter.whereTemplate(
selectedFilterOperand,
value.value,
) || ({} as FilterProperties),
searchResultMapper: selectedFilter.searchResultMapper,
});
setIsUnfolded(false);

View File

@ -41,7 +41,7 @@ const StyledCancelButton = styled.button`
}
`;
function SortAndFilterBar<SortField extends string, FilterProperties>({
function SortAndFilterBar<SortField, FilterProperties>({
sorts,
onRemoveSort,
filters,

View File

@ -8,9 +8,9 @@ type OwnProps<SortField> = {
availableSorts: SortType<SortField>[];
};
const options: Array<SelectedSortType<string>['order']> = ['asc', 'desc'];
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField extends string>({
export function SortDropdownButton<SortField>({
isSortSelected,
availableSorts,
onSortSelect,

View File

@ -66,7 +66,7 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)};
`;
function TableHeader<SortField extends string, FilterProperties>({
function TableHeader<SortField, FilterProperties>({
viewName,
viewIcon,
availableSorts,
@ -84,37 +84,40 @@ function TableHeader<SortField extends string, FilterProperties>({
>([]);
const sortSelect = useCallback(
(sort: SelectedSortType<SortField>) => {
innerSetSorts([sort]);
onSortsUpdate && onSortsUpdate([sort]);
},
[onSortsUpdate],
);
const sortUnselect = useCallback(
(sortId: string) => {
const newSorts = [] as SelectedSortType<SortField>[];
(newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort);
innerSetSorts(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
},
[onSortsUpdate],
[onSortsUpdate, sorts],
);
const sortUnselect = useCallback(
(sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
innerSetSorts(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
},
[onSortsUpdate, sorts],
);
const filterSelect = useCallback(
(filter: SelectedFilterType<FilterProperties>) => {
innerSetFilters([filter]);
onFiltersUpdate && onFiltersUpdate([filter]);
const newFilters = updateSortOrFilterByKey(filters, filter);
innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters);
},
[onFiltersUpdate],
[onFiltersUpdate, filters],
);
const filterUnselect = useCallback(
(filterId: SelectedFilterType<FilterProperties>['key']) => {
const newFilters = [] as SelectedFilterType<FilterProperties>[];
const newFilters = filters.filter((filter) => filter.key !== filterId);
innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters);
},
[onFiltersUpdate],
[onFiltersUpdate, filters],
);
const filterSearch = useCallback(
@ -161,3 +164,19 @@ function TableHeader<SortField extends string, FilterProperties>({
}
export default TableHeader;
function updateSortOrFilterByKey<SortOrFilter extends { key: string }>(
sorts: Readonly<SortOrFilter[]>,
newSort: SortOrFilter,
): SortOrFilter[] {
const newSorts = [...sorts];
const existingSortIndex = sorts.findIndex((sort) => sort.key === newSort.key);
if (existingSortIndex !== -1) {
newSorts[existingSortIndex] = newSort;
} else {
newSorts.push(newSort);
}
return newSorts;
}

View File

@ -25,12 +25,14 @@ export const RegularSortAndFilterBar = ({ removeFunction }: OwnProps) => {
order: 'asc',
key: 'test_sort',
icon: <FaArrowDown />,
_type: 'default_sort',
},
{
label: 'Test sort 2',
order: 'desc',
key: 'test_sort_2',
icon: <FaArrowDown />,
_type: 'default_sort',
},
]}
onRemoveSort={removeFunction}

View File

@ -11,6 +11,7 @@ import {
} from 'react-icons/fa';
import { SortDropdownButton } from '../SortDropdownButton';
import styled from '@emotion/styled';
import { Order_By, People_Order_By } from '../../../../generated/graphql';
const component = {
title: 'SortDropdownButton',
@ -28,25 +29,31 @@ const availableSorts = [
key: 'fullname',
label: 'People',
icon: <FaRegUser />,
_type: 'custom_sort',
orderByTemplate: () => ({ email: Order_By.Asc }),
},
{
key: 'company_name',
label: 'Company',
icon: <FaRegBuilding />,
_type: 'custom_sort',
orderByTemplate: () => ({ email: Order_By.Asc }),
},
{
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
_type: 'default_sort',
},
{ key: 'phone', label: 'Phone', icon: <FaPhone /> },
{ key: 'phone', label: 'Phone', icon: <FaPhone />, _type: 'default_sort' },
{
key: 'created_at',
label: 'Created at',
icon: <FaCalendar />,
_type: 'default_sort',
},
{ key: 'city', label: 'City', icon: <FaMapPin /> },
] satisfies SortType[];
{ key: 'city', label: 'City', icon: <FaMapPin />, _type: 'default_sort' },
] satisfies SortType<People_Order_By>[];
const StyleDiv = styled.div`
height: 200px;
@ -57,7 +64,7 @@ export const RegularSortDropdownButton = ({ setSorts }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<StyleDiv>
<SortDropdownButton
<SortDropdownButton<People_Order_By>
isSortSelected={true}
availableSorts={availableSorts}
onSortSelect={setSorts}

View File

@ -12,11 +12,12 @@ const component = {
export default component;
export const RegularTableHeader = () => {
const availableSorts: Array<SortType> = [
const availableSorts: Array<SortType<Record<'created_at', 'asc'>>> = [
{
key: 'created_at',
label: 'Created at',
icon: <FaCalendar />,
_type: 'default_sort',
},
];
return (

View File

@ -19,6 +19,7 @@ it('Checks the default top option is Ascending', async () => {
key: 'email',
icon: <FaEnvelope />,
order: 'asc',
_type: 'default_sort',
});
});
@ -45,5 +46,6 @@ it('Checks the selection of Descending', async () => {
key: 'email',
icon: <FaEnvelope />,
order: 'desc',
_type: 'default_sort',
});
});

View File

@ -0,0 +1,33 @@
import { Order_By } from '../../../generated/graphql';
import { SelectedFilterType, SelectedSortType } from './interface';
export const reduceFiltersToWhere = <T>(
filters: Array<SelectedFilterType<T>>,
): T => {
const where = filters.reduce((acc, filter) => {
const { where } = filter;
return { ...acc, ...where };
}, {} as T);
return where;
};
const mapOrderToOrder_By = (order: string) => {
if (order === 'asc') return Order_By.Asc;
return Order_By.Desc;
};
export const defaultOrderByTemplateFactory =
(key: string) => (order: string) => ({
[key]: order,
});
export const reduceSortsToOrderBy = <OrderByTemplate>(
sorts: Array<SelectedSortType<OrderByTemplate>>,
): OrderByTemplate[] => {
const mappedSorts = sorts.map((sort) => {
if (sort._type === 'custom_sort')
return sort.orderByTemplate(mapOrderToOrder_By(sort.order));
return defaultOrderByTemplateFactory(sort.key as string)(sort.order);
});
return mappedSorts as OrderByTemplate[];
};

View File

@ -2,23 +2,27 @@ import { DocumentNode } from 'graphql';
import { ReactNode } from 'react';
import {
Companies_Bool_Exp,
Order_By,
People_Bool_Exp,
Users_Bool_Exp,
} from '../../../generated/graphql';
import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../../services/search/search';
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
export type SortType<SortKey = string> = {
label: string;
key: SortKey;
icon?: ReactNode;
};
export type SortType<OrderByTemplate> =
| {
_type: 'default_sort';
label: string;
key: keyof OrderByTemplate & string;
icon?: ReactNode;
}
| {
_type: 'custom_sort';
label: string;
key: string;
icon?: ReactNode;
orderByTemplate: (order: Order_By) => OrderByTemplate;
};
export type SelectedSortType<SortField = string> = SortType<SortField> & {
export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
order: 'asc' | 'desc';
};
@ -30,7 +34,7 @@ export type FilterType<WhereTemplate, FilterValue = Record<string, any>> = {
whereTemplate: (
operand: FilterOperandType,
value: FilterValue,
) => WhereTemplate;
) => WhereTemplate | undefined;
searchQuery: DocumentNode;
searchTemplate: (
searchInput: string,
@ -52,25 +56,3 @@ export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & {
operand: FilterOperandType;
where: WhereTemplate;
};
export function assertFilterUseCompanySearch<FilterValue>(
filter: FilterType<People_Bool_Exp>,
): filter is FilterType<People_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryCompany) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_COMPANY_QUERY;
}
export function assertFilterUsePeopleSearch<FilterValue>(
filter: FilterType<People_Bool_Exp>,
): filter is FilterType<People_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryPerson) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_PEOPLE_QUERY;
}