Skip to content

Commit

Permalink
Date filter improvements (#5917) (#7196)
Browse files Browse the repository at this point in the history
Solves issue #5917.

This PR is now ready for the first review!

Filters do not fully work yet, there's a problem applying multiple
filters like the following:

```
{
  and: [
    {
      [correspondingField.name]: {
        gte: start.toISOString(),
      } as DateFilter,
    },
    {
      [correspondingField.name]: {
        lte: end.toISOString(),
      } as DateFilter,
    },
  ],
}
```

I'll do my best to dig into it tonight!

---------

Co-authored-by: Félix Malfait <[email protected]>
  • Loading branch information
ad-elias and FelixMalfait authored Sep 27, 2024
1 parent c9c2f32 commit 9d36493
Show file tree
Hide file tree
Showing 28 changed files with 979 additions and 127 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"@types/node": "18.19.26",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/pluralize": "^0.0.33",
"@types/react": "^18.2.39",
"@types/react-datepicker": "^6.2.0",
"@types/react-dom": "^18.2.15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,20 @@ export const MultipleFiltersDropdownContent = ({
selectedOperandInDropdownState,
);

const isEmptyOperand =
const isConfigurable =
selectedOperandInDropdown &&
[ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty].includes(
selectedOperandInDropdown,
);
[
ViewFilterOperand.Is,
ViewFilterOperand.IsNotNull,
ViewFilterOperand.IsNot,
ViewFilterOperand.LessThan,
ViewFilterOperand.GreaterThan,
ViewFilterOperand.IsBefore,
ViewFilterOperand.IsAfter,
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
ViewFilterOperand.IsRelative,
].includes(selectedOperandInDropdown);

return (
<StyledContainer>
Expand All @@ -72,7 +81,7 @@ export const MultipleFiltersDropdownContent = ({
<ObjectFilterDropdownOperandSelect />
</StyledOperandSelectContainer>
)}
{!isEmptyOperand && selectedOperandInDropdown && (
{isConfigurable && selectedOperandInDropdown && (
<>
{[
'TEXT',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';

import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { computeVariableDateViewFilterValue } from '@/views/utils/view-filter-value/computeVariableDateViewFilterValue';
import {
VariableDateViewFilterValueDirection,
VariableDateViewFilterValueUnit,
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue';
import { useState } from 'react';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';

export const ObjectFilterDropdownDateInput = () => {
const {
Expand All @@ -23,40 +32,92 @@ export const ObjectFilterDropdownDateInput = () => {
selectedOperandInDropdownState,
);

const selectedFilter = useRecoilValue(selectedFilterState);
const selectedFilter = useRecoilValue(selectedFilterState) as
| (Filter & { definition: { type: 'DATE' | 'DATE_TIME' } })
| null
| undefined;

const initialFilterValue = selectedFilter
? resolveFilterValue(selectedFilter)
: null;
const [internalDate, setInternalDate] = useState<Date | null>(
selectedFilter?.value ? new Date(selectedFilter.value) : new Date(),
initialFilterValue instanceof Date ? initialFilterValue : null,
);

const isDateTimeInput =
filterDefinitionUsedInDropdown?.type === FieldMetadataType.DateTime;

const handleChange = (date: Date | null) => {
setInternalDate(date);
const handleAbsoluteDateChange = (newDate: Date | null) => {
setInternalDate(newDate);

if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;

selectFilter?.({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
value: isDefined(date) ? date.toISOString() : '',
value: newDate?.toISOString() ?? '',
operand: selectedOperandInDropdown,
displayValue: isDefined(date)
displayValue: isDefined(newDate)
? isDateTimeInput
? date.toLocaleString()
: date.toLocaleDateString()
? newDate.toLocaleString()
: newDate.toLocaleDateString()
: '',
definition: filterDefinitionUsedInDropdown,
});

setIsObjectFilterDropdownUnfolded(false);
};

const handleRelativeDateChange = (
relativeDate: {
direction: VariableDateViewFilterValueDirection;
amount?: number;
unit: VariableDateViewFilterValueUnit;
} | null,
) => {
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;

const value = relativeDate
? computeVariableDateViewFilterValue(
relativeDate.direction,
relativeDate.amount,
relativeDate.unit,
)
: '';

selectFilter?.({
id: selectedFilter?.id ? selectedFilter.id : v4(),
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
value,
operand: selectedOperandInDropdown,
displayValue: getRelativeDateDisplayValue(relativeDate),
definition: filterDefinitionUsedInDropdown,
});

setIsObjectFilterDropdownUnfolded(false);
};

const isRelativeOperand =
selectedOperandInDropdown === ViewFilterOperand.IsRelative;

const resolvedValue = selectedFilter
? resolveFilterValue(selectedFilter)
: null;

const relativeDate =
resolvedValue && !(resolvedValue instanceof Date)
? resolvedValue
: undefined;

return (
<InternalDatePicker
relativeDate={relativeDate}
highlightedDateRange={relativeDate}
isRelative={isRelativeOperand}
date={internalDate}
onChange={handleChange}
onMouseSelect={handleChange}
onChange={handleAbsoluteDateChange}
onRelativeDateChange={handleRelativeDateChange}
onMouseSelect={handleAbsoluteDateChange}
isDateTimeInput={isDateTimeInput}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';

import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { isDefined } from '~/utils/isDefined';

import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandLabel } from '../utils/getOperandLabel';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';

Expand Down Expand Up @@ -36,22 +36,25 @@ export const ObjectFilterDropdownOperandSelect = () => {
);

const handleOperandChange = (newOperand: ViewFilterOperand) => {
const isEmptyOperand = [
const isValuelessOperand = [
ViewFilterOperand.IsEmpty,
ViewFilterOperand.IsNotEmpty,
ViewFilterOperand.IsInPast,
ViewFilterOperand.IsInFuture,
ViewFilterOperand.IsToday,
].includes(newOperand);

setSelectedOperandInDropdown(newOperand);
setIsObjectFilterDropdownOperandSelectUnfolded(false);

if (isEmptyOperand) {
if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) {
selectFilter?.({
id: v4(),
fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '',
displayValue: '',
operand: newOperand,
value: '',
definition: filterDefinitionUsedInDropdown as FilterDefinition,
definition: filterDefinitionUsedInDropdown,
});
return;
}
Expand All @@ -60,12 +63,19 @@ export const ObjectFilterDropdownOperandSelect = () => {
isDefined(filterDefinitionUsedInDropdown) &&
isDefined(selectedFilter)
) {
const { value, displayValue } = getInitialFilterValue(
filterDefinitionUsedInDropdown.type,
newOperand,
selectedFilter.value,
selectedFilter.displayValue,
);

selectFilter?.({
id: selectedFilter.id ? selectedFilter.id : v4(),
fieldMetadataId: selectedFilter.fieldMetadataId,
displayValue: selectedFilter.displayValue,
displayValue,
operand: newOperand,
value: selectedFilter.value,
value,
definition: filterDefinitionUsedInDropdown,
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { v4 } from 'uuid';

type SelectFilterParams = {
filterDefinition: FilterDefinition;
Expand All @@ -13,6 +15,7 @@ export const useSelectFilter = () => {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput,
selectFilter: filterDropdownSelectFilter,
} = useFilterDropdown();

const setHotkeyScope = useSetHotkeyScope();
Expand All @@ -31,6 +34,22 @@ export const useSelectFilter = () => {
getOperandsForFilterType(filterDefinition.type)?.[0],
);

const { value, displayValue } = getInitialFilterValue(
filterDefinition.type,
getOperandsForFilterType(filterDefinition.type)?.[0],
);

if (value !== '') {
filterDropdownSelectFilter({
id: v4(),
fieldMetadataId: filterDefinition.fieldMetadataId,
displayValue,
operand: getOperandsForFilterType(filterDefinition.type)?.[0],
value,
definition: filterDefinition,
});
}

setObjectFilterDropdownSearchInput('');
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';

import { FilterDefinition } from './FilterDefinition';

export type Filter = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ describe('getOperandsForFilterType', () => {
ViewFilterOperand.LessThan,
];

const dateOperands = [
ViewFilterOperand.Is,
ViewFilterOperand.IsRelative,
ViewFilterOperand.IsInPast,
ViewFilterOperand.IsInFuture,
ViewFilterOperand.IsToday,
ViewFilterOperand.IsBefore,
ViewFilterOperand.IsAfter,
];

const relationOperand = [ViewFilterOperand.Is, ViewFilterOperand.IsNot];

const testCases = [
Expand All @@ -31,7 +41,8 @@ describe('getOperandsForFilterType', () => {
['ACTOR', [...containsOperands, ...emptyOperands]],
['CURRENCY', [...numberOperands, ...emptyOperands]],
['NUMBER', [...numberOperands, ...emptyOperands]],
['DATE_TIME', [...numberOperands, ...emptyOperands]],
['DATE', [...dateOperands, ...emptyOperands]],
['DATE_TIME', [...dateOperands, ...emptyOperands]],
['RELATION', [...relationOperand, ...emptyOperands]],
[undefined, []],
[null, []],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { z } from 'zod';

export const getInitialFilterValue = (
newType: FilterType,
newOperand: ViewFilterOperand,
oldValue?: string,
oldDisplayValue?: string,
): Pick<Filter, 'value' | 'displayValue'> | Record<string, never> => {
switch (newType) {
case 'DATE':
case 'DATE_TIME': {
const activeDatePickerOperands = [
ViewFilterOperand.IsBefore,
ViewFilterOperand.Is,
ViewFilterOperand.IsAfter,
];

if (activeDatePickerOperands.includes(newOperand)) {
const date = z.coerce.date().safeParse(oldValue).data ?? new Date();
const value = date.toISOString();
const displayValue =
newType === 'DATE'
? date.toLocaleString()
: date.toLocaleDateString();

return { value, displayValue };
}

if (newOperand === ViewFilterOperand.IsRelative) {
return { value: '', displayValue: '' };
}
break;
}
}
return {
value: oldValue ?? '',
displayValue: oldDisplayValue ?? '',
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const getOperandLabel = (
return 'Greater than';
case ViewFilterOperand.LessThan:
return 'Less than';
case ViewFilterOperand.IsBefore:
return 'Is before';
case ViewFilterOperand.IsAfter:
return 'Is after';
case ViewFilterOperand.Is:
return 'Is';
case ViewFilterOperand.IsNot:
Expand All @@ -22,6 +26,14 @@ export const getOperandLabel = (
return 'Is empty';
case ViewFilterOperand.IsNotEmpty:
return 'Is not empty';
case ViewFilterOperand.IsRelative:
return 'Is relative';
case ViewFilterOperand.IsInPast:
return 'Is in past';
case ViewFilterOperand.IsInFuture:
return 'Is in future';
case ViewFilterOperand.IsToday:
return 'Is today';
default:
return '';
}
Expand All @@ -47,6 +59,16 @@ export const getOperandLabelShort = (
return '\u00A0> ';
case ViewFilterOperand.LessThan:
return '\u00A0< ';
case ViewFilterOperand.IsBefore:
return '\u00A0< ';
case ViewFilterOperand.IsAfter:
return '\u00A0> ';
case ViewFilterOperand.IsInPast:
return ': Past';
case ViewFilterOperand.IsInFuture:
return ': Future';
case ViewFilterOperand.IsToday:
return ': Today';
default:
return ': ';
}
Expand Down
Loading

0 comments on commit 9d36493

Please sign in to comment.