From a93c92c65cc5b7da8f33f362b334d0b2379abe7e Mon Sep 17 00:00:00 2001 From: Sammy Teillet Date: Tue, 25 Apr 2023 16:29:08 +0200 Subject: [PATCH] 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 --- front/src/components/table/Table.tsx | 12 +-- .../table/table-header/DropdownButton.tsx | 26 ++++++- .../table/table-header/SortAndFilterBar.tsx | 14 +++- .../table/table-header/SortDropdownButton.tsx | 78 +++++++++++++------ .../table/table-header/SortOrFilterChip.tsx | 5 +- .../table/table-header/TableHeader.tsx | 23 +++--- .../SortDropdownButton.stories.tsx | 38 +++++++++ .../__stories__/TableHeader.stories.tsx | 1 - .../__tests__/SortDropdownButton.test.tsx | 53 +++++++++++++ front/src/layout/styles/themes.ts | 2 + front/src/pages/people/People.tsx | 20 ++--- front/src/pages/people/people-table.tsx | 22 ++++-- front/src/services/people/select.test.ts | 12 +++ front/src/services/people/select.ts | 33 +++++++- 14 files changed, 270 insertions(+), 69 deletions(-) create mode 100644 front/src/components/table/table-header/__stories__/SortDropdownButton.stories.tsx create mode 100644 front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx create mode 100644 front/src/services/people/select.test.ts diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index 905cb71a0..d0c4af908 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -9,15 +9,15 @@ import { import TableHeader from './table-header/TableHeader'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import styled from '@emotion/styled'; -import { SortType } from './table-header/SortAndFilterBar'; +import { SelectedSortType, SortType } from './table-header/SortAndFilterBar'; -type OwnProps = { +type OwnProps = { data: Array; columns: Array>; viewName: string; viewIcon?: IconProp; - onSortsUpdate?: (sorts: Array) => void; - sortsAvailable?: Array; + onSortsUpdate?: (sorts: Array>) => void; + sortsAvailable?: Array>; }; const StyledTable = styled.table` @@ -71,14 +71,14 @@ const StyledTableScrollableContainer = styled.div` flex: 1; `; -function Table({ +function Table({ data, columns, viewName, viewIcon, onSortsUpdate, sortsAvailable, -}: OwnProps) { +}: OwnProps) { const table = useReactTable({ data, columns, diff --git a/front/src/components/table/table-header/DropdownButton.tsx b/front/src/components/table/table-header/DropdownButton.tsx index 75469b77e..cdb901834 100644 --- a/front/src/components/table/table-header/DropdownButton.tsx +++ b/front/src/components/table/table-header/DropdownButton.tsx @@ -57,11 +57,11 @@ const StyledDropdown = styled.ul` li { border-radius: 2px; - &:first-child { + &:first-of-type { border-top-left-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-right-radius: var(--outer-border-radius); } @@ -70,6 +70,7 @@ const StyledDropdown = styled.ul` const StyledDropdownItem = styled.li` display: flex; + align-items: center; padding: ${(props) => props.theme.spacing(2)} calc(${(props) => props.theme.spacing(2)} - 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` display: flex; margin-right: ${(props) => props.theme.spacing(1)}; + min-width: ${(props) => props.theme.spacing(4)}; + justify-content: center; `; function DropdownButton({ @@ -122,6 +143,7 @@ function DropdownButton({ } DropdownButton.StyledDropdownItem = StyledDropdownItem; +DropdownButton.StyledDropdownTopOption = StyledDropdownTopOption; DropdownButton.StyledIcon = StyledIcon; export default DropdownButton; diff --git a/front/src/components/table/table-header/SortAndFilterBar.tsx b/front/src/components/table/table-header/SortAndFilterBar.tsx index 1e028fc3d..e21f4ca6f 100644 --- a/front/src/components/table/table-header/SortAndFilterBar.tsx +++ b/front/src/components/table/table-header/SortAndFilterBar.tsx @@ -3,18 +3,21 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import SortOrFilterChip from './SortOrFilterChip'; import { faArrowDown, faArrowUp } from '@fortawesome/pro-regular-svg-icons'; -type OwnProps = { - sorts: Array; +type OwnProps = { + sorts: Array>; onRemoveSort: (sortId: string) => void; }; export type SortType = { label: string; - order: 'asc' | 'desc'; id: SortIds; icon?: IconProp; }; +export type SelectedSortType = SortType & { + order: 'asc' | 'desc'; +}; + const StyledBar = styled.div` display: flex; flex-direction: row; @@ -44,7 +47,10 @@ const StyledCancelButton = styled.button` } `; -function SortAndFilterBar({ sorts, onRemoveSort }: OwnProps) { +function SortAndFilterBar({ + sorts, + onRemoveSort, +}: OwnProps) { return ( {sorts.map((sort) => { diff --git a/front/src/components/table/table-header/SortDropdownButton.tsx b/front/src/components/table/table-header/SortDropdownButton.tsx index 35d1dcb2d..18e74aa34 100644 --- a/front/src/components/table/table-header/SortDropdownButton.tsx +++ b/front/src/components/table/table-header/SortDropdownButton.tsx @@ -1,27 +1,35 @@ import { useCallback, useState } from 'react'; import DropdownButton from './DropdownButton'; -import { SortType } from './SortAndFilterBar'; +import { SelectedSortType, SortType } from './SortAndFilterBar'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faAngleDown } from '@fortawesome/pro-regular-svg-icons'; -type OwnProps = { - sorts: SortType[]; - setSorts: (sorts: SortType[]) => void; - sortsAvailable: SortType[]; +type OwnProps = { + sorts: SelectedSortType[]; + setSorts: (sorts: SelectedSortType[]) => void; + sortsAvailable: SortType[]; }; -export function SortDropdownButton({ +const options: Array['order']> = ['asc', 'desc']; + +export function SortDropdownButton({ sortsAvailable, setSorts, sorts, -}: OwnProps) { +}: OwnProps) { const [isUnfolded, setIsUnfolded] = useState(false); + const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); + + const [selectedSortDirection, setSelectedSortDirection] = + useState['order']>('asc'); + const onSortItemSelect = useCallback( - (sort: SortType) => { - const newSorts = [sort]; + (sort: SortType) => { + const newSorts = [{ ...sort, order: selectedSortDirection }]; setSorts(newSorts); }, - [setSorts], + [setSorts, selectedSortDirection], ); return ( @@ -31,20 +39,42 @@ export function SortDropdownButton({ isUnfolded={isUnfolded} setIsUnfolded={setIsUnfolded} > - {sortsAvailable.map((option, index) => ( - { - setIsUnfolded(false); - onSortItemSelect(option); - }} - > - - {option.icon && } - - {option.label} - - ))} + {isOptionUnfolded + ? options.map((option, index) => ( + { + setSelectedSortDirection(option); + setIsOptionUnfolded(false); + }} + > + {option === 'asc' ? 'Ascending' : 'Descending'} + + )) + : [ + setIsOptionUnfolded(true)} + > + {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} + + + , + ...sortsAvailable.map((sort, index) => ( + { + setIsUnfolded(false); + onSortItemSelect(sort); + }} + > + + {sort.icon && } + + {sort.label} + + )), + ]} ); } diff --git a/front/src/components/table/table-header/SortOrFilterChip.tsx b/front/src/components/table/table-header/SortOrFilterChip.tsx index 8bdef75c3..0a09ad6de 100644 --- a/front/src/components/table/table-header/SortOrFilterChip.tsx +++ b/front/src/components/table/table-header/SortOrFilterChip.tsx @@ -17,10 +17,9 @@ const StyledChip = styled.div` background-color: ${(props) => props.theme.blueHighTransparency}; border: 1px solid ${(props) => props.theme.blueLowTransparency}; color: ${(props) => props.theme.blue}; - padding: ${(props) => props.theme.spacing(1)} - ${(props) => props.theme.spacing(2)}; + padding: ${(props) => props.theme.spacing(1) + ' ' + 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` margin-right: ${(props) => props.theme.spacing(1)}; diff --git a/front/src/components/table/table-header/TableHeader.tsx b/front/src/components/table/table-header/TableHeader.tsx index 1b92c9230..5455bfcc4 100644 --- a/front/src/components/table/table-header/TableHeader.tsx +++ b/front/src/components/table/table-header/TableHeader.tsx @@ -2,15 +2,18 @@ import styled from '@emotion/styled'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import DropdownButton from './DropdownButton'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import SortAndFilterBar, { SortType } from './SortAndFilterBar'; +import SortAndFilterBar, { + SelectedSortType, + SortType, +} from './SortAndFilterBar'; import { useCallback, useState } from 'react'; import { SortDropdownButton } from './SortDropdownButton'; -type OwnProps = { +type OwnProps = { viewName: string; viewIcon?: IconProp; - onSortsUpdate?: (sorts: Array) => void; - sortsAvailable: Array; + onSortsUpdate?: (sorts: Array>) => void; + sortsAvailable: Array>; }; const StyledContainer = styled.div` @@ -49,16 +52,18 @@ const StyledFilters = styled.div` margin-right: ${(props) => props.theme.spacing(2)}; `; -function TableHeader({ +function TableHeader({ viewName, viewIcon, onSortsUpdate, sortsAvailable, -}: OwnProps) { - const [sorts, innerSetSorts] = useState([] as Array); +}: OwnProps) { + const [sorts, innerSetSorts] = useState>>( + [], + ); const setSorts = useCallback( - (sorts: SortType[]) => { + (sorts: SelectedSortType[]) => { innerSetSorts(sorts); onSortsUpdate && onSortsUpdate(sorts); }, @@ -67,7 +72,7 @@ function TableHeader({ const onSortItemUnSelect = useCallback( (sortId: string) => { - const newSorts = [] as SortType[]; + const newSorts = [] as SelectedSortType[]; innerSetSorts(newSorts); onSortsUpdate && onSortsUpdate(newSorts); }, diff --git a/front/src/components/table/table-header/__stories__/SortDropdownButton.stories.tsx b/front/src/components/table/table-header/__stories__/SortDropdownButton.stories.tsx new file mode 100644 index 000000000..76184affe --- /dev/null +++ b/front/src/components/table/table-header/__stories__/SortDropdownButton.stories.tsx @@ -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 ( + + + + ); +}; diff --git a/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx index 2862c0734..d5d9376a5 100644 --- a/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx +++ b/front/src/components/table/table-header/__stories__/TableHeader.stories.tsx @@ -16,7 +16,6 @@ export const RegularTableHeader = () => { { id: 'created_at', label: 'Created at', - order: 'asc', icon: faCalendar, }, ]; diff --git a/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx b/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx new file mode 100644 index 000000000..0ae5efdad --- /dev/null +++ b/front/src/components/table/table-header/__tests__/SortDropdownButton.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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', + }, + ]); +}); diff --git a/front/src/layout/styles/themes.ts b/front/src/layout/styles/themes.ts index 057c5c37b..c87ce2661 100644 --- a/front/src/layout/styles/themes.ts +++ b/front/src/layout/styles/themes.ts @@ -12,6 +12,8 @@ const commonTheme = { iconSizeMedium: '1.08rem', iconSizeLarge: '1.23rem', + fontWeightBold: 500, + spacing: (multiplicator: number) => `${multiplicator * 4}px`, }; diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index f689bfc64..95259144d 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -5,8 +5,12 @@ import styled from '@emotion/styled'; import { peopleColumns, sortsAvailable } from './people-table'; import { mapPerson } from '../../interfaces/person.interface'; import { useCallback, useState } from 'react'; -import { SortType } from '../../components/table/table-header/SortAndFilterBar'; -import { OrderBy, usePeopleQuery } from '../../services/people'; +import { + OrderBy, + PeopleSelectedSortType, + reduceSortsToOrderBy, + usePeopleQuery, +} from '../../services/people'; const StyledPeopleContainer = styled.div` display: flex; @@ -19,19 +23,11 @@ const defaultOrderBy: OrderBy[] = [ }, ]; -const reduceSortsToOrderBy = (sorts: Array): OrderBy[] => { - const mappedSorts = sorts.reduce((acc, sort) => { - acc[sort.id] = sort.order; - return acc; - }, {} as OrderBy); - return [mappedSorts]; -}; - function People() { - const [, setSorts] = useState([] as Array); + const [, setSorts] = useState([] as Array); const [orderBy, setOrderBy] = useState(defaultOrderBy); - const updateSorts = useCallback((sorts: Array) => { + const updateSorts = useCallback((sorts: Array) => { setSorts(sorts); setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy); }, []); diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index 81b00743e..077a55a94 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -15,28 +15,36 @@ import Checkbox from '../../components/form/Checkbox'; import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer'; import CompanyChip from '../../components/chips/CompanyChip'; 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 { SortType } from '../../components/table/table-header/SortAndFilterBar'; import EditableCell from '../../components/table/EditableCell'; -import { updatePerson } from '../../services/people'; +import { OrderByFields, updatePerson } from '../../services/people'; export const sortsAvailable = [ + { + id: 'fullname', + label: 'People', + icon: faUser, + }, + { + id: 'company_name', + label: 'Company', + icon: faBuildings, + }, { id: 'email', label: 'Email', - order: 'asc', icon: faEnvelope, }, - { id: 'phone', label: 'Phone', order: 'asc', icon: faPhone }, + { id: 'phone', label: 'Phone', icon: faPhone }, { id: 'created_at', label: 'Created at', - order: 'asc', icon: faCalendar, }, - { id: 'city', label: 'City', order: 'asc', icon: faMapPin }, -] satisfies Array>; + { id: 'city', label: 'City', icon: faMapPin }, +] satisfies Array>; const columnHelper = createColumnHelper(); export const peopleColumns = [ diff --git a/front/src/services/people/select.test.ts b/front/src/services/people/select.test.ts new file mode 100644 index 000000000..9121c08ae --- /dev/null +++ b/front/src/services/people/select.test.ts @@ -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' }]); + }); +}); diff --git a/front/src/services/people/select.ts b/front/src/services/people/select.ts index 6cffc3fdd..a665dd2ae 100644 --- a/front/src/services/people/select.ts +++ b/front/src/services/people/select.ts @@ -1,7 +1,38 @@ import { QueryResult, gql, useQuery } from '@apollo/client'; import { GraphqlQueryPerson } from '../../interfaces/person.interface'; +import { SelectedSortType } from '../../components/table/table-header/SortAndFilterBar'; -export type OrderBy = Record; +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; + +export const reduceSortsToOrderBy = ( + sorts: Array, +): 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` query GetPeople($orderBy: [people_order_by!]) {