Sammy/t 131 i see a ascending descending option in (#73)

* refactor: use safe css selector

* feature: onclick update the order option

* feature: set option in the dropdown

* feature: display icon for sort options

* refactor: indent key so react does not complain of conflicting keys

* feature: align icon and text

* feature: fix size of icon to align text

* feature: create design for TopOption in dropdown

* refactor: set font weight in style

* feature: finalise design of TopOption

* refactor: rename option to sort

* refactor: remove order from the sortType

* refactor: move sort mapper in service

* test: selection of Descending in SortDropdownButton

* refactor: fix styme-component warning

* feature: add sorting by people

* refactor: set SortFields types for tables

* feature: add sort by company

* refactor: rename sortFields to singular

* refactor: rename option to SortDirection
This commit is contained in:
Sammy Teillet
2023-04-25 16:29:08 +02:00
committed by GitHub
parent 463b5f4ec9
commit a93c92c65c
14 changed files with 270 additions and 69 deletions

View File

@ -9,15 +9,15 @@ import {
import TableHeader from './table-header/TableHeader'; import TableHeader from './table-header/TableHeader';
import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { IconProp } from '@fortawesome/fontawesome-svg-core';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { SortType } from './table-header/SortAndFilterBar'; import { SelectedSortType, SortType } from './table-header/SortAndFilterBar';
type OwnProps<TData> = { type OwnProps<TData, SortField> = {
data: Array<TData>; data: Array<TData>;
columns: Array<ColumnDef<TData, any>>; columns: Array<ColumnDef<TData, any>>;
viewName: string; viewName: string;
viewIcon?: IconProp; viewIcon?: IconProp;
onSortsUpdate?: (sorts: Array<SortType>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
sortsAvailable?: Array<SortType>; sortsAvailable?: Array<SortType<SortField>>;
}; };
const StyledTable = styled.table` const StyledTable = styled.table`
@ -71,14 +71,14 @@ const StyledTableScrollableContainer = styled.div`
flex: 1; flex: 1;
`; `;
function Table<TData>({ function Table<TData, SortField extends string>({
data, data,
columns, columns,
viewName, viewName,
viewIcon, viewIcon,
onSortsUpdate, onSortsUpdate,
sortsAvailable, sortsAvailable,
}: OwnProps<TData>) { }: OwnProps<TData, SortField>) {
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,

View File

@ -57,11 +57,11 @@ const StyledDropdown = styled.ul`
li { li {
border-radius: 2px; border-radius: 2px;
&:first-child { &:first-of-type {
border-top-left-radius: var(--outer-border-radius); border-top-left-radius: var(--outer-border-radius);
border-top-right-radius: var(--outer-border-radius); border-top-right-radius: var(--outer-border-radius);
} }
&:last-child { &:last-of-type {
border-bottom-left-radius: var(--outer-border-radius); border-bottom-left-radius: var(--outer-border-radius);
border-bottom-right-radius: var(--outer-border-radius); border-bottom-right-radius: var(--outer-border-radius);
} }
@ -70,6 +70,7 @@ const StyledDropdown = styled.ul`
const StyledDropdownItem = styled.li` const StyledDropdownItem = styled.li`
display: flex; display: flex;
align-items: center;
padding: ${(props) => props.theme.spacing(2)} padding: ${(props) => props.theme.spacing(2)}
calc(${(props) => props.theme.spacing(2)} - 2px); calc(${(props) => props.theme.spacing(2)} - 2px);
margin: 2px; margin: 2px;
@ -82,9 +83,29 @@ const StyledDropdownItem = styled.li`
} }
`; `;
const StyledDropdownTopOption = styled.li`
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(${(props) => props.theme.spacing(2)} + 2px)
calc(${(props) => props.theme.spacing(2)});
background: rgba(0, 0, 0, 0);
cursor: pointer;
color: ${(props) => props.theme.text60};
font-weight: ${(props) => props.theme.fontWeightBold};
&:hover {
background: rgba(0, 0, 0, 0.04);
}
border-radius: 0%;
border-bottom: 1px solid ${(props) => props.theme.primaryBorder};
`;
const StyledIcon = styled.div` const StyledIcon = styled.div`
display: flex; display: flex;
margin-right: ${(props) => props.theme.spacing(1)}; margin-right: ${(props) => props.theme.spacing(1)};
min-width: ${(props) => props.theme.spacing(4)};
justify-content: center;
`; `;
function DropdownButton({ function DropdownButton({
@ -122,6 +143,7 @@ function DropdownButton({
} }
DropdownButton.StyledDropdownItem = StyledDropdownItem; DropdownButton.StyledDropdownItem = StyledDropdownItem;
DropdownButton.StyledDropdownTopOption = StyledDropdownTopOption;
DropdownButton.StyledIcon = StyledIcon; DropdownButton.StyledIcon = StyledIcon;
export default DropdownButton; export default DropdownButton;

View File

@ -3,18 +3,21 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import SortOrFilterChip from './SortOrFilterChip'; import SortOrFilterChip from './SortOrFilterChip';
import { faArrowDown, faArrowUp } from '@fortawesome/pro-regular-svg-icons'; import { faArrowDown, faArrowUp } from '@fortawesome/pro-regular-svg-icons';
type OwnProps = { type OwnProps<SortField> = {
sorts: Array<SortType>; sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: string) => void; onRemoveSort: (sortId: string) => void;
}; };
export type SortType<SortIds = string> = { export type SortType<SortIds = string> = {
label: string; label: string;
order: 'asc' | 'desc';
id: SortIds; id: SortIds;
icon?: IconProp; icon?: IconProp;
}; };
export type SelectedSortType<SortField = string> = SortType<SortField> & {
order: 'asc' | 'desc';
};
const StyledBar = styled.div` const StyledBar = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -44,7 +47,10 @@ const StyledCancelButton = styled.button`
} }
`; `;
function SortAndFilterBar({ sorts, onRemoveSort }: OwnProps) { function SortAndFilterBar<SortField extends string>({
sorts,
onRemoveSort,
}: OwnProps<SortField>) {
return ( return (
<StyledBar> <StyledBar>
{sorts.map((sort) => { {sorts.map((sort) => {

View File

@ -1,27 +1,35 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import DropdownButton from './DropdownButton'; import DropdownButton from './DropdownButton';
import { SortType } from './SortAndFilterBar'; import { SelectedSortType, SortType } from './SortAndFilterBar';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDown } from '@fortawesome/pro-regular-svg-icons';
type OwnProps = { type OwnProps<SortField> = {
sorts: SortType[]; sorts: SelectedSortType<SortField>[];
setSorts: (sorts: SortType[]) => void; setSorts: (sorts: SelectedSortType<SortField>[]) => void;
sortsAvailable: SortType[]; sortsAvailable: SortType<SortField>[];
}; };
export function SortDropdownButton({ const options: Array<SelectedSortType<string>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField extends string>({
sortsAvailable, sortsAvailable,
setSorts, setSorts,
sorts, sorts,
}: OwnProps) { }: OwnProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc');
const onSortItemSelect = useCallback( const onSortItemSelect = useCallback(
(sort: SortType) => { (sort: SortType<SortField>) => {
const newSorts = [sort]; const newSorts = [{ ...sort, order: selectedSortDirection }];
setSorts(newSorts); setSorts(newSorts);
}, },
[setSorts], [setSorts, selectedSortDirection],
); );
return ( return (
@ -31,20 +39,42 @@ export function SortDropdownButton({
isUnfolded={isUnfolded} isUnfolded={isUnfolded}
setIsUnfolded={setIsUnfolded} setIsUnfolded={setIsUnfolded}
> >
{sortsAvailable.map((option, index) => ( {isOptionUnfolded
<DropdownButton.StyledDropdownItem ? options.map((option, index) => (
key={index} <DropdownButton.StyledDropdownItem
onClick={() => { key={index}
setIsUnfolded(false); onClick={() => {
onSortItemSelect(option); setSelectedSortDirection(option);
}} setIsOptionUnfolded(false);
> }}
<DropdownButton.StyledIcon> >
{option.icon && <FontAwesomeIcon icon={option.icon} />} {option === 'asc' ? 'Ascending' : 'Descending'}
</DropdownButton.StyledIcon> </DropdownButton.StyledDropdownItem>
{option.label} ))
</DropdownButton.StyledDropdownItem> : [
))} <DropdownButton.StyledDropdownTopOption
key={0}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
<FontAwesomeIcon icon={faAngleDown} />
</DropdownButton.StyledDropdownTopOption>,
...sortsAvailable.map((sort, index) => (
<DropdownButton.StyledDropdownItem
key={index + 1}
onClick={() => {
setIsUnfolded(false);
onSortItemSelect(sort);
}}
>
<DropdownButton.StyledIcon>
{sort.icon && <FontAwesomeIcon icon={sort.icon} />}
</DropdownButton.StyledIcon>
{sort.label}
</DropdownButton.StyledDropdownItem>
)),
]}
</DropdownButton> </DropdownButton>
); );
} }

View File

@ -17,10 +17,9 @@ const StyledChip = styled.div`
background-color: ${(props) => props.theme.blueHighTransparency}; background-color: ${(props) => props.theme.blueHighTransparency};
border: 1px solid ${(props) => props.theme.blueLowTransparency}; border: 1px solid ${(props) => props.theme.blueLowTransparency};
color: ${(props) => props.theme.blue}; color: ${(props) => props.theme.blue};
padding: ${(props) => props.theme.spacing(1)} padding: ${(props) => props.theme.spacing(1) + ' ' + props.theme.spacing(2)};
${(props) => props.theme.spacing(2)};
margin-left: ${(props) => props.theme.spacing(2)}; margin-left: ${(props) => props.theme.spacing(2)};
fontsize: ${(props) => props.theme.fontSizeSmall}; font-size: ${(props) => props.theme.fontSizeSmall};
`; `;
const StyledIcon = styled.div` const StyledIcon = styled.div`
margin-right: ${(props) => props.theme.spacing(1)}; margin-right: ${(props) => props.theme.spacing(1)};

View File

@ -2,15 +2,18 @@ import styled from '@emotion/styled';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import DropdownButton from './DropdownButton'; import DropdownButton from './DropdownButton';
import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { IconProp } from '@fortawesome/fontawesome-svg-core';
import SortAndFilterBar, { SortType } from './SortAndFilterBar'; import SortAndFilterBar, {
SelectedSortType,
SortType,
} from './SortAndFilterBar';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { SortDropdownButton } from './SortDropdownButton'; import { SortDropdownButton } from './SortDropdownButton';
type OwnProps = { type OwnProps<SortField> = {
viewName: string; viewName: string;
viewIcon?: IconProp; viewIcon?: IconProp;
onSortsUpdate?: (sorts: Array<SortType>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
sortsAvailable: Array<SortType>; sortsAvailable: Array<SortType<SortField>>;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -49,16 +52,18 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)}; margin-right: ${(props) => props.theme.spacing(2)};
`; `;
function TableHeader({ function TableHeader<SortField extends string>({
viewName, viewName,
viewIcon, viewIcon,
onSortsUpdate, onSortsUpdate,
sortsAvailable, sortsAvailable,
}: OwnProps) { }: OwnProps<SortField>) {
const [sorts, innerSetSorts] = useState([] as Array<SortType>); const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[],
);
const setSorts = useCallback( const setSorts = useCallback(
(sorts: SortType[]) => { (sorts: SelectedSortType<SortField>[]) => {
innerSetSorts(sorts); innerSetSorts(sorts);
onSortsUpdate && onSortsUpdate(sorts); onSortsUpdate && onSortsUpdate(sorts);
}, },
@ -67,7 +72,7 @@ function TableHeader({
const onSortItemUnSelect = useCallback( const onSortItemUnSelect = useCallback(
(sortId: string) => { (sortId: string) => {
const newSorts = [] as SortType[]; const newSorts = [] as SelectedSortType<SortField>[];
innerSetSorts(newSorts); innerSetSorts(newSorts);
onSortsUpdate && onSortsUpdate(newSorts); onSortsUpdate && onSortsUpdate(newSorts);
}, },

View File

@ -0,0 +1,38 @@
import { SelectedSortType, SortType } from '../SortAndFilterBar';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { faArrowDown } from '@fortawesome/pro-regular-svg-icons';
import { SortDropdownButton } from '../SortDropdownButton';
const component = {
title: 'SortDropdownButton',
component: SortDropdownButton,
};
export default component;
type OwnProps = {
setSorts: () => void;
};
const sorts = [] satisfies SelectedSortType[];
const availableSorts = [
{
label: 'Email',
id: 'email',
icon: faArrowDown,
},
] satisfies SortType[];
export const RegularSortDropdownButton = ({ setSorts }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<SortDropdownButton
sorts={sorts}
sortsAvailable={availableSorts}
setSorts={setSorts}
/>
</ThemeProvider>
);
};

View File

@ -16,7 +16,6 @@ export const RegularTableHeader = () => {
{ {
id: 'created_at', id: 'created_at',
label: 'Created at', label: 'Created at',
order: 'asc',
icon: faCalendar, icon: faCalendar,
}, },
]; ];

View File

@ -0,0 +1,53 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularSortDropdownButton } from '../__stories__/SortDropdownButton.stories';
import { faArrowDown } from '@fortawesome/pro-regular-svg-icons';
it('Checks the default top option is Ascending', async () => {
const setSorts = jest.fn();
const { getByText } = render(
<RegularSortDropdownButton setSorts={setSorts} />,
);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith([
{
label: 'Email',
id: 'email',
icon: faArrowDown,
order: 'asc',
},
]);
});
it('Checks the selection of Descending', async () => {
const setSorts = jest.fn();
const { getByText } = render(
<RegularSortDropdownButton setSorts={setSorts} />,
);
const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const openTopOption = getByText('Ascending');
fireEvent.click(openTopOption);
const selectDescending = getByText('Descending');
fireEvent.click(selectDescending);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith([
{
label: 'Email',
id: 'email',
icon: faArrowDown,
order: 'desc',
},
]);
});

View File

@ -12,6 +12,8 @@ const commonTheme = {
iconSizeMedium: '1.08rem', iconSizeMedium: '1.08rem',
iconSizeLarge: '1.23rem', iconSizeLarge: '1.23rem',
fontWeightBold: 500,
spacing: (multiplicator: number) => `${multiplicator * 4}px`, spacing: (multiplicator: number) => `${multiplicator * 4}px`,
}; };

View File

@ -5,8 +5,12 @@ import styled from '@emotion/styled';
import { peopleColumns, sortsAvailable } from './people-table'; import { peopleColumns, sortsAvailable } from './people-table';
import { mapPerson } from '../../interfaces/person.interface'; import { mapPerson } from '../../interfaces/person.interface';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { SortType } from '../../components/table/table-header/SortAndFilterBar'; import {
import { OrderBy, usePeopleQuery } from '../../services/people'; OrderBy,
PeopleSelectedSortType,
reduceSortsToOrderBy,
usePeopleQuery,
} from '../../services/people';
const StyledPeopleContainer = styled.div` const StyledPeopleContainer = styled.div`
display: flex; display: flex;
@ -19,19 +23,11 @@ const defaultOrderBy: OrderBy[] = [
}, },
]; ];
const reduceSortsToOrderBy = (sorts: Array<SortType>): OrderBy[] => {
const mappedSorts = sorts.reduce((acc, sort) => {
acc[sort.id] = sort.order;
return acc;
}, {} as OrderBy);
return [mappedSorts];
};
function People() { function People() {
const [, setSorts] = useState([] as Array<SortType>); const [, setSorts] = useState([] as Array<PeopleSelectedSortType>);
const [orderBy, setOrderBy] = useState(defaultOrderBy); const [orderBy, setOrderBy] = useState(defaultOrderBy);
const updateSorts = useCallback((sorts: Array<SortType>) => { const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
setSorts(sorts); setSorts(sorts);
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy); setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []); }, []);

View File

@ -15,28 +15,36 @@ import Checkbox from '../../components/form/Checkbox';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer'; import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import CompanyChip from '../../components/chips/CompanyChip'; import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip'; import PersonChip from '../../components/chips/PersonChip';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface'; import { Person } from '../../interfaces/person.interface';
import PipeChip from '../../components/chips/PipeChip'; import PipeChip from '../../components/chips/PipeChip';
import { SortType } from '../../components/table/table-header/SortAndFilterBar'; import { SortType } from '../../components/table/table-header/SortAndFilterBar';
import EditableCell from '../../components/table/EditableCell'; import EditableCell from '../../components/table/EditableCell';
import { updatePerson } from '../../services/people'; import { OrderByFields, updatePerson } from '../../services/people';
export const sortsAvailable = [ export const sortsAvailable = [
{
id: 'fullname',
label: 'People',
icon: faUser,
},
{
id: 'company_name',
label: 'Company',
icon: faBuildings,
},
{ {
id: 'email', id: 'email',
label: 'Email', label: 'Email',
order: 'asc',
icon: faEnvelope, icon: faEnvelope,
}, },
{ id: 'phone', label: 'Phone', order: 'asc', icon: faPhone }, { id: 'phone', label: 'Phone', icon: faPhone },
{ {
id: 'created_at', id: 'created_at',
label: 'Created at', label: 'Created at',
order: 'asc',
icon: faCalendar, icon: faCalendar,
}, },
{ id: 'city', label: 'City', order: 'asc', icon: faMapPin }, { id: 'city', label: 'City', icon: faMapPin },
] satisfies Array<SortType<keyof GraphqlQueryPerson>>; ] satisfies Array<SortType<OrderByFields>>;
const columnHelper = createColumnHelper<Person>(); const columnHelper = createColumnHelper<Person>();
export const peopleColumns = [ export const peopleColumns = [

View File

@ -0,0 +1,12 @@
import { PeopleSelectedSortType, reduceSortsToOrderBy } from './select';
describe('reduceSortsToOrderBy', () => {
it('should return an array of objects with the id as key and the order as value', () => {
const sorts = [
{ id: 'firstname', label: 'firstname', order: 'asc' },
{ id: 'lastname', label: 'lastname', order: 'desc' },
] satisfies PeopleSelectedSortType[];
const result = reduceSortsToOrderBy(sorts);
expect(result).toEqual([{ firstname: 'asc', lastname: 'desc' }]);
});
});

View File

@ -1,7 +1,38 @@
import { QueryResult, gql, useQuery } from '@apollo/client'; import { QueryResult, gql, useQuery } from '@apollo/client';
import { GraphqlQueryPerson } from '../../interfaces/person.interface'; import { GraphqlQueryPerson } from '../../interfaces/person.interface';
import { SelectedSortType } from '../../components/table/table-header/SortAndFilterBar';
export type OrderBy = Record<string, 'asc' | 'desc'>; export type OrderByFields =
| keyof GraphqlQueryPerson
| 'fullname'
| 'company_name';
export type OrderBy = Partial<{
[key in keyof GraphqlQueryPerson]:
| 'asc'
| 'desc'
| { [key in string]: 'asc' | 'desc' };
}>;
export type PeopleSelectedSortType = SelectedSortType<OrderByFields>;
export const reduceSortsToOrderBy = (
sorts: Array<PeopleSelectedSortType>,
): OrderBy[] => {
const mappedSorts = sorts.reduce((acc, sort) => {
const id = sort.id;
if (id === 'fullname') {
acc['firstname'] = sort.order;
acc['lastname'] = sort.order;
} else if (id === 'company_name') {
acc['company'] = { company_name: sort.order };
} else {
acc[id] = sort.order;
}
return acc;
}, {} as OrderBy);
return [mappedSorts];
};
export const GET_PEOPLE = gql` export const GET_PEOPLE = gql`
query GetPeople($orderBy: [people_order_by!]) { query GetPeople($orderBy: [people_order_by!]) {