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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,18 @@
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';
export const nameFilter = {
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
type: 'text',
operands: [
{
label: 'Contains',
@ -28,6 +35,7 @@ export const urlFilter = {
key: 'company_domain_name',
label: 'Url',
icon: <FaLink />,
type: 'text',
operands: [
{
label: 'Contains',
@ -50,6 +58,7 @@ export const addressFilter = {
key: 'company_address',
label: 'Address',
icon: <FaMapPin />,
type: 'text',
operands: [
{
label: 'Contains',
@ -72,6 +81,7 @@ export const employeesFilter = {
key: 'company_employees',
label: 'Employees',
icon: <FaUsers />,
type: 'text',
operands: [
{
label: 'Greater than',
@ -94,9 +104,37 @@ export const employeesFilter = {
],
} 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 = [
nameFilter,
urlFilter,
addressFilter,
employeesFilter,
creationDateFilter,
];

View File

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