Enable filtering by creation date with datepicker (#131)

Enable to filter by date with datepicker
This commit is contained in:
Charles Bochet
2023-05-19 13:17:32 +02:00
committed by GitHub
parent 192b89a7b7
commit 5adc5b833c
7 changed files with 149 additions and 71 deletions

View File

@ -2,7 +2,6 @@ import styled from '@emotion/styled';
import { forwardRef, useState } from 'react'; import { forwardRef, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper'; import EditableCellWrapper from './EditableCellWrapper';
import DatePicker from '../form/DatePicker'; import DatePicker from '../form/DatePicker';
import { CalendarContainer } from 'react-datepicker';
import { modalBackground } from '../../layout/styles/themes'; import { modalBackground } from '../../layout/styles/themes';
export type EditableDateProps = { export type EditableDateProps = {
@ -24,10 +23,10 @@ const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
position: absolute; position: absolute;
border: 1px solid ${(props) => props.theme.primaryBorder}; border: 1px solid ${(props) => props.theme.primaryBorder};
border-radius: 8px; border-radius: 8px;
width: 280px;
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09); box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
z-index: 1; z-index: 1;
left: -10px; left: -10px;
top: 10px;
${modalBackground}; ${modalBackground};
`; `;
function EditableDate({ function EditableDate({
@ -54,21 +53,11 @@ function EditableDate({
); );
interface DatePickerContainerProps { interface DatePickerContainerProps {
className?: string;
children: React.ReactNode; children: React.ReactNode;
} }
const DatePickerContainer = ({ const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
className, return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
children,
}: DatePickerContainerProps) => {
return (
<StyledCalendarContainer>
<CalendarContainer className={className}>
<div style={{ position: 'relative' }}>{children}</div>
</CalendarContainer>
</StyledCalendarContainer>
);
}; };
return ( return (
@ -86,7 +75,7 @@ function EditableDate({
setInputValue(date); setInputValue(date);
}} }}
customInput={<DateDisplay />} customInput={<DateDisplay />}
customContainer={DatePickerContainer} customCalendarContainer={DatePickerContainer}
/> />
</StyledContainer> </StyledContainer>
} }

View File

@ -9,7 +9,7 @@ export type DatePickerProps = {
date: Date; date: Date;
onChangeHandler: (date: Date) => void; onChangeHandler: (date: Date) => void;
customInput?: ReactElement; customInput?: ReactElement;
customContainer?(props: CalendarContainerProps): React.ReactNode; customCalendarContainer?(props: CalendarContainerProps): React.ReactNode;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -22,6 +22,13 @@ const StyledContainer = styled.div`
display: block; display: block;
} }
& .react-datepicker-popper {
position: relative !important;
inset: auto !important;
transform: none !important;
padding: 0 !important;
}
& .react-datepicker__triangle::after { & .react-datepicker__triangle::after {
display: none; display: none;
} }
@ -125,6 +132,10 @@ const StyledContainer = styled.div`
line-height: 40px; line-height: 40px;
} }
& .react-datepicker__month-container {
float: none;
}
// Days // Days
& .react-datepicker__month { & .react-datepicker__month {
@ -172,7 +183,7 @@ function DatePicker({
date, date,
onChangeHandler, onChangeHandler,
customInput, customInput,
customContainer, customCalendarContainer,
}: DatePickerProps) { }: DatePickerProps) {
const [startDate, setStartDate] = useState(date); const [startDate, setStartDate] = useState(date);
@ -203,7 +214,9 @@ function DatePicker({
onChangeHandler(date); onChangeHandler(date);
}} }}
customInput={customInput ? customInput : <DefaultDateDisplay />} customInput={customInput ? customInput : <DefaultDateDisplay />}
calendarContainer={customContainer ? customContainer : undefined} calendarContainer={
customCalendarContainer ? customCalendarContainer : undefined
}
/> />
</StyledContainer> </StyledContainer>
); );

View File

@ -76,7 +76,6 @@ const StyledDropdownItem = styled.li`
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;
background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.text60}; color: ${(props) => props.theme.text60};
@ -91,7 +90,6 @@ const StyledDropdownTopOption = styled.li`
justify-content: space-between; justify-content: space-between;
padding: calc(${(props) => props.theme.spacing(2)} + 2px) padding: calc(${(props) => props.theme.spacing(2)} + 2px)
calc(${(props) => props.theme.spacing(2)}); calc(${(props) => props.theme.spacing(2)});
background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.text60}; color: ${(props) => props.theme.text60};
font-weight: ${(props) => props.theme.fontWeightBold}; font-weight: ${(props) => props.theme.fontWeightBold};
@ -115,7 +113,6 @@ const StyledSearchField = styled.li`
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: rgba(0, 0, 0, 0.04);
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.text60}; color: ${(props) => props.theme.text60};
font-weight: ${(props) => props.theme.fontWeightBold}; font-weight: ${(props) => props.theme.fontWeightBold};

View File

@ -10,6 +10,8 @@ import {
SearchResultsType, SearchResultsType,
useSearch, useSearch,
} from '../../../services/api/search/search'; } from '../../../services/api/search/search';
import DatePicker from '../../form/DatePicker';
import styled from '@emotion/styled';
type OwnProps<TData extends FilterableFieldsType> = { type OwnProps<TData extends FilterableFieldsType> = {
isFilterSelected: boolean; isFilterSelected: boolean;
@ -26,7 +28,8 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
}: OwnProps<TData>) => { }: OwnProps<TData>) => {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
useState(false);
const [selectedFilter, setSelectedFilter] = useState< const [selectedFilter, setSelectedFilter] = useState<
FilterConfigType<TData> | undefined FilterConfigType<TData> | undefined
@ -39,19 +42,19 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch(); const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
const resetState = useCallback(() => { const resetState = useCallback(() => {
setIsOptionUnfolded(false); setIsOperandSelectionUnfolded(false);
setSelectedFilter(undefined); setSelectedFilter(undefined);
setSelectedFilterOperand(undefined); setSelectedFilterOperand(undefined);
setFilterSearch(null); setFilterSearch(null);
}, [setFilterSearch]); }, [setFilterSearch]);
const renderSelectOptionItems = selectedFilter?.operands.map( const renderOperandSelection = selectedFilter?.operands.map(
(filterOperand, index) => ( (filterOperand, index) => (
<DropdownButton.StyledDropdownItem <DropdownButton.StyledDropdownItem
key={`select-filter-operand-${index}`} key={`select-filter-operand-${index}`}
onClick={() => { onClick={() => {
setSelectedFilterOperand(filterOperand); setSelectedFilterOperand(filterOperand);
setIsOptionUnfolded(false); setIsOperandSelectionUnfolded(false);
}} }}
> >
{filterOperand.label} {filterOperand.label}
@ -59,6 +62,21 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
), ),
); );
const renderFilterSelection = availableFilters.map((filter, index) => (
<DropdownButton.StyledDropdownItem
key={`select-filter-${index}`}
onClick={() => {
setSelectedFilter(filter);
setSelectedFilterOperand(filter.operands[0]);
filter.searchConfig && setFilterSearch(filter.searchConfig);
setSearchInput('');
}}
>
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
{filter.label}
</DropdownButton.StyledDropdownItem>
));
const renderSearchResults = ( const renderSearchResults = (
filterSearchResults: SearchResultsType, filterSearchResults: SearchResultsType,
selectedFilter: FilterConfigType<TData>, selectedFilter: FilterConfigType<TData>,
@ -93,22 +111,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
)); ));
}; };
const renderSelectFilterITems = availableFilters.map((filter, index) => ( function renderValueSelection(
<DropdownButton.StyledDropdownItem
key={`select-filter-${index}`}
onClick={() => {
setSelectedFilter(filter);
setSelectedFilterOperand(filter.operands[0]);
filter.searchConfig && setFilterSearch(filter.searchConfig);
setSearchInput('');
}}
>
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
{filter.label}
</DropdownButton.StyledDropdownItem>
));
function renderFilterDropdown(
selectedFilter: FilterConfigType<TData>, selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType<TData>, selectedFilterOperand: FilterOperandType<TData>,
) { ) {
@ -116,38 +119,65 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
<> <>
<DropdownButton.StyledDropdownTopOption <DropdownButton.StyledDropdownTopOption
key={'selected-filter-operand'} key={'selected-filter-operand'}
onClick={() => setIsOptionUnfolded(true)} onClick={() => setIsOperandSelectionUnfolded(true)}
> >
{selectedFilterOperand.label} {selectedFilterOperand.label}
<DropdownButton.StyledDropdownTopOptionAngleDown /> <DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption> </DropdownButton.StyledDropdownTopOption>
<DropdownButton.StyledSearchField key={'search-filter'}> <DropdownButton.StyledSearchField key={'search-filter'}>
<input {['text', 'relation'].includes(selectedFilter.type) && (
type="text" <input
placeholder={selectedFilter.label} type="text"
onChange={(event: ChangeEvent<HTMLInputElement>) => { placeholder={selectedFilter.label}
if (selectedFilter.searchConfig) { onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(selectedFilter.searchConfig); if (
setSearchInput(event.target.value); selectedFilter.type === 'relation' &&
} else { selectedFilter.searchConfig
if (event.target.value === '') { ) {
onFilterRemove(selectedFilter.key); setFilterSearch(selectedFilter.searchConfig);
} else { setSearchInput(event.target.value);
onFilterSelect({
key: selectedFilter.key,
label: selectedFilter.label,
value: event.target.value,
displayValue: event.target.value,
icon: selectedFilter.icon,
operand: selectedFilterOperand,
});
} }
}
}} if (selectedFilter.type === 'text') {
/> if (event.target.value === '') {
onFilterRemove(selectedFilter.key);
} else {
onFilterSelect({
key: selectedFilter.key,
label: selectedFilter.label,
value: event.target.value,
displayValue: event.target.value,
icon: selectedFilter.icon,
operand: selectedFilterOperand,
});
}
}
}}
/>
)}
{selectedFilter.type === 'date' && (
<DatePicker
date={new Date()}
onChangeHandler={(date) => {
onFilterSelect({
key: selectedFilter.key,
label: selectedFilter.label,
value: date.toISOString(),
displayValue: date.toLocaleDateString(),
icon: selectedFilter.icon,
operand: selectedFilterOperand,
});
}}
customInput={<></>}
customCalendarContainer={styled.div`
top: -10px;
`}
/>
)}
</DropdownButton.StyledSearchField> </DropdownButton.StyledSearchField>
{filterSearchResults && {selectedFilter.type === 'relation' &&
filterSearchResults &&
renderSearchResults( renderSearchResults(
filterSearchResults, filterSearchResults,
selectedFilter, selectedFilter,
@ -165,11 +195,11 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
setIsUnfolded={setIsUnfolded} setIsUnfolded={setIsUnfolded}
resetState={resetState} resetState={resetState}
> >
{selectedFilter && selectedFilterOperand {selectedFilter
? isOptionUnfolded ? isOperandSelectionUnfolded
? renderSelectOptionItems ? renderOperandSelection
: renderFilterDropdown(selectedFilter, selectedFilterOperand) : renderValueSelection(selectedFilter, selectedFilterOperand)
: renderSelectFilterITems} : renderFilterSelection}
</DropdownButton> </DropdownButton>
); );
}; };

View File

@ -14,6 +14,13 @@ export type FilterConfigType<
key: string; key: string;
label: string; label: string;
icon: ReactNode; icon: ReactNode;
type: WhereType extends UnknownType
? 'relation' | 'text' | 'date'
: WhereType extends AnyEntity
? 'relation'
: WhereType extends string
? 'text' | 'date'
: never;
operands: FilterOperandType<FilteredType, WhereType>[]; operands: FilterOperandType<FilteredType, WhereType>[];
} & (WhereType extends UnknownType } & (WhereType extends UnknownType
? { searchConfig?: SearchConfigType<UnknownType> } ? { searchConfig?: SearchConfigType<UnknownType> }

View File

@ -1,11 +1,18 @@
import { Company } from '../../interfaces/entities/company.interface'; import { Company } from '../../interfaces/entities/company.interface';
import { FaLink, FaBuilding, FaMapPin, FaUsers } from 'react-icons/fa'; import {
FaLink,
FaBuilding,
FaMapPin,
FaUsers,
FaCalendar,
} from 'react-icons/fa';
import { FilterConfigType } from '../../interfaces/filters/interface'; import { FilterConfigType } from '../../interfaces/filters/interface';
export const nameFilter = { export const nameFilter = {
key: 'company_name', key: 'company_name',
label: 'Company', label: 'Company',
icon: <FaBuilding />, icon: <FaBuilding />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
@ -28,6 +35,7 @@ export const urlFilter = {
key: 'company_domain_name', key: 'company_domain_name',
label: 'Url', label: 'Url',
icon: <FaLink />, icon: <FaLink />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
@ -50,6 +58,7 @@ export const addressFilter = {
key: 'company_address', key: 'company_address',
label: 'Address', label: 'Address',
icon: <FaMapPin />, icon: <FaMapPin />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
@ -72,6 +81,7 @@ export const employeesFilter = {
key: 'company_employees', key: 'company_employees',
label: 'Employees', label: 'Employees',
icon: <FaUsers />, icon: <FaUsers />,
type: 'text',
operands: [ operands: [
{ {
label: 'Greater than', label: 'Greater than',
@ -94,9 +104,37 @@ export const employeesFilter = {
], ],
} satisfies FilterConfigType<Company, string>; } satisfies FilterConfigType<Company, string>;
export const creationDateFilter = {
key: 'company_created_at',
label: 'Created At',
icon: <FaCalendar />,
type: 'date',
operands: [
{
label: 'Greater than',
id: 'greater_than',
whereTemplate: (searchString) => ({
created_at: {
_gte: searchString,
},
}),
},
{
label: 'Less than',
id: 'less_than',
whereTemplate: (searchString) => ({
created_at: {
_lte: searchString,
},
}),
},
],
} satisfies FilterConfigType<Company, string>;
export const availableFilters = [ export const availableFilters = [
nameFilter, nameFilter,
urlFilter, urlFilter,
addressFilter, addressFilter,
employeesFilter, employeesFilter,
creationDateFilter,
]; ];

View File

@ -11,6 +11,7 @@ export const fullnameFilter = {
key: 'fullname', key: 'fullname',
label: 'People', label: 'People',
icon: <FaUser />, icon: <FaUser />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
@ -41,6 +42,7 @@ export const companyFilter = {
key: 'company_name', key: 'company_name',
label: 'Company', label: 'Company',
icon: <FaBuilding />, icon: <FaBuilding />,
type: 'relation',
searchConfig: { searchConfig: {
query: SEARCH_COMPANY_QUERY, query: SEARCH_COMPANY_QUERY,
template: (searchString: string) => ({ template: (searchString: string) => ({
@ -74,6 +76,7 @@ export const emailFilter = {
key: 'email', key: 'email',
label: 'Email', label: 'Email',
icon: <FaEnvelope />, icon: <FaEnvelope />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
@ -96,6 +99,7 @@ export const cityFilter = {
key: 'city', key: 'city',
label: 'City', label: 'City',
icon: <FaMapPin />, icon: <FaMapPin />,
type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',