Merge pull request #47 from twentyhq/cbo-sort-filter-2

Add sorting bar on table views
This commit is contained in:
Charles Bochet
2023-04-19 23:17:16 +02:00
committed by GitHub
13 changed files with 278 additions and 31 deletions

View File

@ -18,10 +18,12 @@ type OwnProps = {
}; };
const StyledTable = styled.table` const StyledTable = styled.table`
min-width: 100%; min-width: calc(100% - ${(props) => props.theme.spacing(4)});
border-radius: 4px; border-radius: 4px;
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
margin-left: ${(props) => props.theme.spacing(2)};
margin-right: ${(props) => props.theme.spacing(2)};
th { th {
border-collapse: collapse; border-collapse: collapse;

View File

@ -1,13 +1,14 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter'; import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
import { modalBackground } from '../../../layout/styles/themes'; import { modalBackground } from '../../../layout/styles/themes';
import { SortType } from './SortAndFilterBar';
type OwnProps = { type OwnProps = {
label: string; label: string;
options: Array<{ label: string; icon: IconProp }>; options: Array<SortType>;
onSortSelect?: (id: string) => void;
}; };
const StyledDropdownButtonContainer = styled.div` const StyledDropdownButtonContainer = styled.div`
@ -68,7 +69,7 @@ const StyledIcon = styled.div`
margin-right: ${(props) => props.theme.spacing(1)}; margin-right: ${(props) => props.theme.spacing(1)};
`; `;
function DropdownButton({ label, options }: OwnProps) { function DropdownButton({ label, options, onSortSelect }: OwnProps) {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const onButtonClick = () => { const onButtonClick = () => {
@ -90,9 +91,17 @@ function DropdownButton({ label, options }: OwnProps) {
{isUnfolded && options.length > 0 && ( {isUnfolded && options.length > 0 && (
<StyledDropdown ref={dropdownRef}> <StyledDropdown ref={dropdownRef}>
{options.map((option, index) => ( {options.map((option, index) => (
<StyledDropdownItem key={index}> <StyledDropdownItem
key={index}
onClick={() => {
setIsUnfolded(false);
if (onSortSelect) {
onSortSelect(option.id);
}
}}
>
<StyledIcon> <StyledIcon>
<FontAwesomeIcon icon={option.icon} /> {option.icon && <FontAwesomeIcon icon={option.icon} />}
</StyledIcon> </StyledIcon>
{option.label} {option.label}
</StyledDropdownItem> </StyledDropdownItem>

View File

@ -0,0 +1,45 @@
import styled from '@emotion/styled';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import SortOrFilterChip from './SortOrFilterChip';
import { faArrowDown, faArrowUp } from '@fortawesome/pro-regular-svg-icons';
type OwnProps = {
sorts: Array<SortType>;
onRemoveSort: (sortId: string) => void;
};
export type SortType = {
label: string;
order: string;
id: string;
icon?: IconProp;
};
const StyledBar = styled.div`
display: flex;
flex-direction: row;
border-top: 1px solid ${(props) => props.theme.primaryBorder};
align-items: center;
justify-content: space-between;
height: 40px;
`;
function SortAndFilterBar({ sorts, onRemoveSort }: OwnProps) {
return (
<StyledBar>
{sorts.map((sort) => {
return (
<SortOrFilterChip
key={sort.id}
label={sort.label}
id={sort.id}
icon={sort.order === 'asc' ? faArrowDown : faArrowUp}
onRemove={() => onRemoveSort(sort.id)}
/>
);
})}
</StyledBar>
);
}
export default SortAndFilterBar;

View File

@ -0,0 +1,48 @@
import styled from '@emotion/styled';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faTimes } from '@fortawesome/pro-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type OwnProps = {
id: string;
label: string;
icon: IconProp;
onRemove: () => void;
};
const StyledChip = styled.div`
border-radius: 50px;
display: flex;
flex-direction: row;
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)};
margin-left: ${(props) => props.theme.spacing(2)};
fontsize: ${(props) => props.theme.fontSizeSmall};
`;
const StyledIcon = styled.div`
margin-right: ${(props) => props.theme.spacing(1)};
`;
const StyledDelete = styled.div`
margin-left: ${(props) => props.theme.spacing(2)};
cursor: pointer;
`;
function SortOrFilterChip({ id, label, icon, onRemove }: OwnProps) {
return (
<StyledChip>
<StyledIcon>
<FontAwesomeIcon icon={icon} />
</StyledIcon>
{label}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + id}>
<FontAwesomeIcon icon={faTimes} />
</StyledDelete>
</StyledChip>
);
}
export default SortOrFilterChip;

View File

@ -3,13 +3,20 @@ 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 { faCalendar } from '@fortawesome/pro-regular-svg-icons'; import { faCalendar } from '@fortawesome/pro-regular-svg-icons';
import SortAndFilterBar, { SortType } from './SortAndFilterBar';
import { useState } from 'react';
type OwnProps = { type OwnProps = {
viewName: string; viewName: string;
viewIcon?: IconProp; viewIcon?: IconProp;
}; };
const StyledTitle = styled.div` const StyledContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledTableHeader = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -17,7 +24,8 @@ const StyledTitle = styled.div`
height: 40px; height: 40px;
color: ${(props) => props.theme.text60}; color: ${(props) => props.theme.text60};
font-weight: 500; font-weight: 500;
padding-left: ${(props) => props.theme.spacing(2)}; padding-left: ${(props) => props.theme.spacing(3)};
padding-right: ${(props) => props.theme.spacing(1)};
`; `;
const StyledIcon = styled.div` const StyledIcon = styled.div`
@ -36,23 +44,53 @@ const StyledFilters = styled.div`
`; `;
function TableHeader({ viewName, viewIcon }: OwnProps) { function TableHeader({ viewName, viewIcon }: OwnProps) {
const [sorts, setSorts] = useState([] as Array<SortType>);
const onSortItemSelect = (sortId: string) => {
setSorts([
{
label: 'Created at',
order: 'asc',
id: sortId,
},
]);
};
const onSortItemUnSelect = (sortId: string) => {
setSorts([]);
};
const sortsAvailable: Array<SortType> = [
{
id: 'created_at',
label: 'Created at',
order: 'asc',
icon: faCalendar,
},
];
return ( return (
<StyledTitle> <StyledContainer>
<StyledViewSection> <StyledTableHeader>
<StyledIcon> <StyledViewSection>
{viewIcon && <FontAwesomeIcon icon={viewIcon} size="lg" />} <StyledIcon>
</StyledIcon> {viewIcon && <FontAwesomeIcon icon={viewIcon} size="lg" />}
{viewName} </StyledIcon>
</StyledViewSection> {viewName}
<StyledFilters> </StyledViewSection>
<DropdownButton label="Filter" options={[]} /> <StyledFilters>
<DropdownButton <DropdownButton label="Filter" options={[]} />
label="Sort" <DropdownButton
options={[{ label: 'Created at', icon: faCalendar }]} label="Sort"
/> options={sortsAvailable}
<DropdownButton label="Settings" options={[]} /> onSortSelect={onSortItemSelect}
</StyledFilters> />
</StyledTitle> <DropdownButton label="Settings" options={[]} />
</StyledFilters>
</StyledTableHeader>
{sorts.length > 0 && (
<SortAndFilterBar sorts={sorts} onRemoveSort={onSortItemUnSelect} />
)}
</StyledContainer>
); );
} }

View File

@ -0,0 +1,37 @@
import SortAndFilterBar from '../SortAndFilterBar';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { faArrowDown } from '@fortawesome/pro-regular-svg-icons';
export default {
title: 'SortAndFilterBar',
component: SortAndFilterBar,
};
type OwnProps = {
removeFunction: () => void;
};
export const RegularSortAndFilterBar = ({ removeFunction }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<SortAndFilterBar
sorts={[
{
label: 'Test sort',
order: 'asc',
id: 'test_sort',
icon: faArrowDown,
},
{
label: 'Test sort 2',
order: 'desc',
id: 'test_sort_2',
icon: faArrowDown,
},
]}
onRemoveSort={removeFunction}
/>
</ThemeProvider>
);
};

View File

@ -0,0 +1,26 @@
import SortOrFilterChip from '../SortOrFilterChip';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { faArrowDown } from '@fortawesome/pro-regular-svg-icons';
export default {
title: 'SortOrFilterChip',
component: SortOrFilterChip,
};
type OwnProps = {
removeFunction: () => void;
};
export const RegularSortOrFilterChip = ({ removeFunction }: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<SortOrFilterChip
id="test_sort"
icon={faArrowDown}
label="Test sort"
onRemove={removeFunction}
/>
</ThemeProvider>
);
};

View File

@ -0,0 +1,17 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularSortAndFilterBar } from '../__stories__/SortAndFilterBar.stories';
const removeFunction = jest.fn();
it('Checks the SortAndFilterBar renders', async () => {
const { getByText, getByTestId } = render(
<RegularSortAndFilterBar removeFunction={removeFunction} />,
);
expect(getByText('Test sort')).toBeDefined();
const removeIcon = getByTestId('remove-icon-test_sort');
fireEvent.click(removeIcon);
expect(removeFunction).toHaveBeenCalled();
});

View File

@ -0,0 +1,17 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularSortOrFilterChip } from '../__stories__/SortOrFilterChip.stories';
const removeFunction = jest.fn();
it('Checks the RegularSortOrFilterChip renders', async () => {
const { getByText, getByTestId } = render(
<RegularSortOrFilterChip removeFunction={removeFunction} />,
);
expect(getByText('Test sort')).toBeDefined();
const removeIcon = getByTestId('remove-icon-test_sort');
fireEvent.click(removeIcon);
expect(removeFunction).toHaveBeenCalled();
});

View File

@ -1,9 +1,15 @@
import { render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { RegularTableHeader } from '../__stories__/TableHeader.stories'; import { RegularTableHeader } from '../__stories__/TableHeader.stories';
it('Checks the TableHeader renders', () => { it('Checks the TableHeader renders', async () => {
const { getByText } = render(<RegularTableHeader />); const { getByText } = render(<RegularTableHeader />);
expect(getByText('Test')).toBeDefined(); const sortDropdownButton = getByText('Sort');
fireEvent.click(sortDropdownButton);
const sortByCreatedAt = getByText('Created at');
fireEvent.click(sortByCreatedAt);
expect(getByText('Created at')).toBeDefined();
}); });

View File

@ -8,8 +8,6 @@ export function useOutsideAlerter(
) { ) {
useEffect(() => { useEffect(() => {
function handleClickOutside(event: Event) { function handleClickOutside(event: Event) {
console.log('test3');
const target = event.target as HTMLButtonElement; const target = event.target as HTMLButtonElement;
if (ref.current && !ref.current.contains(target)) { if (ref.current && !ref.current.contains(target)) {
callback(); callback();

View File

@ -40,6 +40,9 @@ const lightThemeSpecific = {
green: '#1e7e50', green: '#1e7e50',
purple: '#1111b7', purple: '#1111b7',
yellow: '#cc660a', yellow: '#cc660a',
blueHighTransparency: 'rgba(25, 97, 237, 0.03)',
blueLowTransparency: 'rgba(25, 97, 237, 0.32)',
}; };
const darkThemeSpecific: typeof lightThemeSpecific = { const darkThemeSpecific: typeof lightThemeSpecific = {
@ -70,6 +73,9 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
green: '#e6fff2', green: '#e6fff2',
purple: '#e0e0ff', purple: '#e0e0ff',
yellow: '#fff2e7', yellow: '#fff2e7',
blueHighTransparency: 'rgba(104, 149, 236, 0.03)',
blueLowTransparency: 'rgba(104, 149, 236, 0.32)',
}; };
export const modalBackground = (props: any) => export const modalBackground = (props: any) =>

View File

@ -36,8 +36,6 @@ type Person = {
const StyledPeopleContainer = styled.div` const StyledPeopleContainer = styled.div`
display: flex; display: flex;
padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)};
width: 100%; width: 100%;
a { a {