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 <felix@twenty.com>
This commit is contained in:
@ -273,6 +273,7 @@
|
|||||||
"@types/node": "18.19.26",
|
"@types/node": "18.19.26",
|
||||||
"@types/passport-google-oauth20": "^2.0.11",
|
"@types/passport-google-oauth20": "^2.0.11",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "^3.0.8",
|
||||||
|
"@types/pluralize": "^0.0.33",
|
||||||
"@types/react": "^18.2.39",
|
"@types/react": "^18.2.39",
|
||||||
"@types/react-datepicker": "^6.2.0",
|
"@types/react-datepicker": "^6.2.0",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
|||||||
@ -54,11 +54,20 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
selectedOperandInDropdownState,
|
selectedOperandInDropdownState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmptyOperand =
|
const isConfigurable =
|
||||||
selectedOperandInDropdown &&
|
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 (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
@ -72,7 +81,7 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
<ObjectFilterDropdownOperandSelect />
|
<ObjectFilterDropdownOperandSelect />
|
||||||
</StyledOperandSelectContainer>
|
</StyledOperandSelectContainer>
|
||||||
)}
|
)}
|
||||||
{!isEmptyOperand && selectedOperandInDropdown && (
|
{isConfigurable && selectedOperandInDropdown && (
|
||||||
<>
|
<>
|
||||||
{[
|
{[
|
||||||
'TEXT',
|
'TEXT',
|
||||||
|
|||||||
@ -2,10 +2,19 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
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 { 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 { useState } from 'react';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const ObjectFilterDropdownDateInput = () => {
|
export const ObjectFilterDropdownDateInput = () => {
|
||||||
const {
|
const {
|
||||||
@ -23,28 +32,35 @@ export const ObjectFilterDropdownDateInput = () => {
|
|||||||
selectedOperandInDropdownState,
|
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>(
|
const [internalDate, setInternalDate] = useState<Date | null>(
|
||||||
selectedFilter?.value ? new Date(selectedFilter.value) : new Date(),
|
initialFilterValue instanceof Date ? initialFilterValue : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDateTimeInput =
|
const isDateTimeInput =
|
||||||
filterDefinitionUsedInDropdown?.type === FieldMetadataType.DateTime;
|
filterDefinitionUsedInDropdown?.type === FieldMetadataType.DateTime;
|
||||||
|
|
||||||
const handleChange = (date: Date | null) => {
|
const handleAbsoluteDateChange = (newDate: Date | null) => {
|
||||||
setInternalDate(date);
|
setInternalDate(newDate);
|
||||||
|
|
||||||
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
||||||
|
|
||||||
selectFilter?.({
|
selectFilter?.({
|
||||||
id: selectedFilter?.id ? selectedFilter.id : v4(),
|
id: selectedFilter?.id ? selectedFilter.id : v4(),
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: isDefined(date) ? date.toISOString() : '',
|
value: newDate?.toISOString() ?? '',
|
||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: isDefined(date)
|
displayValue: isDefined(newDate)
|
||||||
? isDateTimeInput
|
? isDateTimeInput
|
||||||
? date.toLocaleString()
|
? newDate.toLocaleString()
|
||||||
: date.toLocaleDateString()
|
: newDate.toLocaleDateString()
|
||||||
: '',
|
: '',
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
});
|
});
|
||||||
@ -52,11 +68,56 @@ export const ObjectFilterDropdownDateInput = () => {
|
|||||||
setIsObjectFilterDropdownUnfolded(false);
|
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 (
|
return (
|
||||||
<InternalDatePicker
|
<InternalDatePicker
|
||||||
|
relativeDate={relativeDate}
|
||||||
|
highlightedDateRange={relativeDate}
|
||||||
|
isRelative={isRelativeOperand}
|
||||||
date={internalDate}
|
date={internalDate}
|
||||||
onChange={handleChange}
|
onChange={handleAbsoluteDateChange}
|
||||||
onMouseSelect={handleChange}
|
onRelativeDateChange={handleRelativeDateChange}
|
||||||
|
onMouseSelect={handleAbsoluteDateChange}
|
||||||
isDateTimeInput={isDateTimeInput}
|
isDateTimeInput={isDateTimeInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
|
||||||
import { getOperandLabel } from '../utils/getOperandLabel';
|
import { getOperandLabel } from '../utils/getOperandLabel';
|
||||||
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
||||||
|
|
||||||
@ -36,22 +36,25 @@ export const ObjectFilterDropdownOperandSelect = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOperandChange = (newOperand: ViewFilterOperand) => {
|
const handleOperandChange = (newOperand: ViewFilterOperand) => {
|
||||||
const isEmptyOperand = [
|
const isValuelessOperand = [
|
||||||
ViewFilterOperand.IsEmpty,
|
ViewFilterOperand.IsEmpty,
|
||||||
ViewFilterOperand.IsNotEmpty,
|
ViewFilterOperand.IsNotEmpty,
|
||||||
|
ViewFilterOperand.IsInPast,
|
||||||
|
ViewFilterOperand.IsInFuture,
|
||||||
|
ViewFilterOperand.IsToday,
|
||||||
].includes(newOperand);
|
].includes(newOperand);
|
||||||
|
|
||||||
setSelectedOperandInDropdown(newOperand);
|
setSelectedOperandInDropdown(newOperand);
|
||||||
setIsObjectFilterDropdownOperandSelectUnfolded(false);
|
setIsObjectFilterDropdownOperandSelectUnfolded(false);
|
||||||
|
|
||||||
if (isEmptyOperand) {
|
if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) {
|
||||||
selectFilter?.({
|
selectFilter?.({
|
||||||
id: v4(),
|
id: v4(),
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '',
|
fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '',
|
||||||
displayValue: '',
|
displayValue: '',
|
||||||
operand: newOperand,
|
operand: newOperand,
|
||||||
value: '',
|
value: '',
|
||||||
definition: filterDefinitionUsedInDropdown as FilterDefinition,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -60,12 +63,19 @@ export const ObjectFilterDropdownOperandSelect = () => {
|
|||||||
isDefined(filterDefinitionUsedInDropdown) &&
|
isDefined(filterDefinitionUsedInDropdown) &&
|
||||||
isDefined(selectedFilter)
|
isDefined(selectedFilter)
|
||||||
) {
|
) {
|
||||||
|
const { value, displayValue } = getInitialFilterValue(
|
||||||
|
filterDefinitionUsedInDropdown.type,
|
||||||
|
newOperand,
|
||||||
|
selectedFilter.value,
|
||||||
|
selectedFilter.displayValue,
|
||||||
|
);
|
||||||
|
|
||||||
selectFilter?.({
|
selectFilter?.({
|
||||||
id: selectedFilter.id ? selectedFilter.id : v4(),
|
id: selectedFilter.id ? selectedFilter.id : v4(),
|
||||||
fieldMetadataId: selectedFilter.fieldMetadataId,
|
fieldMetadataId: selectedFilter.fieldMetadataId,
|
||||||
displayValue: selectedFilter.displayValue,
|
displayValue,
|
||||||
operand: newOperand,
|
operand: newOperand,
|
||||||
value: selectedFilter.value,
|
value,
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
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 { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
type SelectFilterParams = {
|
type SelectFilterParams = {
|
||||||
filterDefinition: FilterDefinition;
|
filterDefinition: FilterDefinition;
|
||||||
@ -13,6 +15,7 @@ export const useSelectFilter = () => {
|
|||||||
setFilterDefinitionUsedInDropdown,
|
setFilterDefinitionUsedInDropdown,
|
||||||
setSelectedOperandInDropdown,
|
setSelectedOperandInDropdown,
|
||||||
setObjectFilterDropdownSearchInput,
|
setObjectFilterDropdownSearchInput,
|
||||||
|
selectFilter: filterDropdownSelectFilter,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
@ -31,6 +34,22 @@ export const useSelectFilter = () => {
|
|||||||
getOperandsForFilterType(filterDefinition.type)?.[0],
|
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('');
|
setObjectFilterDropdownSearchInput('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
import { FilterDefinition } from './FilterDefinition';
|
import { FilterDefinition } from './FilterDefinition';
|
||||||
|
|
||||||
export type Filter = {
|
export type Filter = {
|
||||||
|
|||||||
@ -19,6 +19,16 @@ describe('getOperandsForFilterType', () => {
|
|||||||
ViewFilterOperand.LessThan,
|
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 relationOperand = [ViewFilterOperand.Is, ViewFilterOperand.IsNot];
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
@ -31,7 +41,8 @@ describe('getOperandsForFilterType', () => {
|
|||||||
['ACTOR', [...containsOperands, ...emptyOperands]],
|
['ACTOR', [...containsOperands, ...emptyOperands]],
|
||||||
['CURRENCY', [...numberOperands, ...emptyOperands]],
|
['CURRENCY', [...numberOperands, ...emptyOperands]],
|
||||||
['NUMBER', [...numberOperands, ...emptyOperands]],
|
['NUMBER', [...numberOperands, ...emptyOperands]],
|
||||||
['DATE_TIME', [...numberOperands, ...emptyOperands]],
|
['DATE', [...dateOperands, ...emptyOperands]],
|
||||||
|
['DATE_TIME', [...dateOperands, ...emptyOperands]],
|
||||||
['RELATION', [...relationOperand, ...emptyOperands]],
|
['RELATION', [...relationOperand, ...emptyOperands]],
|
||||||
[undefined, []],
|
[undefined, []],
|
||||||
[null, []],
|
[null, []],
|
||||||
|
|||||||
@ -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 ?? '',
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -12,6 +12,10 @@ export const getOperandLabel = (
|
|||||||
return 'Greater than';
|
return 'Greater than';
|
||||||
case ViewFilterOperand.LessThan:
|
case ViewFilterOperand.LessThan:
|
||||||
return 'Less than';
|
return 'Less than';
|
||||||
|
case ViewFilterOperand.IsBefore:
|
||||||
|
return 'Is before';
|
||||||
|
case ViewFilterOperand.IsAfter:
|
||||||
|
return 'Is after';
|
||||||
case ViewFilterOperand.Is:
|
case ViewFilterOperand.Is:
|
||||||
return 'Is';
|
return 'Is';
|
||||||
case ViewFilterOperand.IsNot:
|
case ViewFilterOperand.IsNot:
|
||||||
@ -22,6 +26,14 @@ export const getOperandLabel = (
|
|||||||
return 'Is empty';
|
return 'Is empty';
|
||||||
case ViewFilterOperand.IsNotEmpty:
|
case ViewFilterOperand.IsNotEmpty:
|
||||||
return 'Is not empty';
|
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:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -47,6 +59,16 @@ export const getOperandLabelShort = (
|
|||||||
return '\u00A0> ';
|
return '\u00A0> ';
|
||||||
case ViewFilterOperand.LessThan:
|
case ViewFilterOperand.LessThan:
|
||||||
return '\u00A0< ';
|
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:
|
default:
|
||||||
return ': ';
|
return ': ';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,13 +31,23 @@ export const getOperandsForFilterType = (
|
|||||||
];
|
];
|
||||||
case 'CURRENCY':
|
case 'CURRENCY':
|
||||||
case 'NUMBER':
|
case 'NUMBER':
|
||||||
case 'DATE_TIME':
|
|
||||||
case 'DATE':
|
|
||||||
return [
|
return [
|
||||||
ViewFilterOperand.GreaterThan,
|
ViewFilterOperand.GreaterThan,
|
||||||
ViewFilterOperand.LessThan,
|
ViewFilterOperand.LessThan,
|
||||||
...emptyOperands,
|
...emptyOperands,
|
||||||
];
|
];
|
||||||
|
case 'DATE_TIME':
|
||||||
|
case 'DATE':
|
||||||
|
return [
|
||||||
|
ViewFilterOperand.Is,
|
||||||
|
ViewFilterOperand.IsRelative,
|
||||||
|
ViewFilterOperand.IsInPast,
|
||||||
|
ViewFilterOperand.IsInFuture,
|
||||||
|
ViewFilterOperand.IsToday,
|
||||||
|
ViewFilterOperand.IsBefore,
|
||||||
|
ViewFilterOperand.IsAfter,
|
||||||
|
...emptyOperands,
|
||||||
|
];
|
||||||
case 'RATING':
|
case 'RATING':
|
||||||
return [
|
return [
|
||||||
ViewFilterOperand.Is,
|
ViewFilterOperand.Is,
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
VariableDateViewFilterValueDirection,
|
||||||
|
VariableDateViewFilterValueUnit,
|
||||||
|
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
|
import { plural } from 'pluralize';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
export const getRelativeDateDisplayValue = (
|
||||||
|
relativeDate: {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
} | null,
|
||||||
|
) => {
|
||||||
|
if (!relativeDate) return '';
|
||||||
|
const { direction, amount, unit } = relativeDate;
|
||||||
|
|
||||||
|
const directionStr = capitalize(direction.toLowerCase());
|
||||||
|
const amountStr = direction === 'THIS' ? '' : amount;
|
||||||
|
const unitStr = amount
|
||||||
|
? amount > 1
|
||||||
|
? plural(unit.toLowerCase())
|
||||||
|
: unit.toLowerCase()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return [directionStr, amountStr, unitStr]
|
||||||
|
.filter((item) => item !== undefined)
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
@ -25,6 +25,9 @@ import {
|
|||||||
convertLessThanRatingToArrayOfRatingValues,
|
convertLessThanRatingToArrayOfRatingValues,
|
||||||
convertRatingToRatingValue,
|
convertRatingToRatingValue,
|
||||||
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
} from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
|
||||||
|
import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue';
|
||||||
|
import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
||||||
|
import { z } from 'zod';
|
||||||
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
||||||
|
|
||||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||||
@ -289,16 +292,19 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
(field) => field.id === rawUIFilter.fieldMetadataId,
|
(field) => field.id === rawUIFilter.fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmptyOperand = [
|
const isValuelessOperand = [
|
||||||
ViewFilterOperand.IsEmpty,
|
ViewFilterOperand.IsEmpty,
|
||||||
ViewFilterOperand.IsNotEmpty,
|
ViewFilterOperand.IsNotEmpty,
|
||||||
|
ViewFilterOperand.IsInPast,
|
||||||
|
ViewFilterOperand.IsInFuture,
|
||||||
|
ViewFilterOperand.IsToday,
|
||||||
].includes(rawUIFilter.operand);
|
].includes(rawUIFilter.operand);
|
||||||
|
|
||||||
if (!correspondingField) {
|
if (!correspondingField) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEmptyOperand) {
|
if (!isValuelessOperand) {
|
||||||
if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') {
|
if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -341,24 +347,31 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'DATE':
|
case 'DATE':
|
||||||
case 'DATE_TIME':
|
case 'DATE_TIME': {
|
||||||
|
const resolvedFilterValue = resolveFilterValue(rawUIFilter);
|
||||||
|
const now = roundToNearestMinutes(new Date());
|
||||||
|
const date =
|
||||||
|
resolvedFilterValue instanceof Date ? resolvedFilterValue : now;
|
||||||
|
|
||||||
switch (rawUIFilter.operand) {
|
switch (rawUIFilter.operand) {
|
||||||
case ViewFilterOperand.GreaterThan:
|
case ViewFilterOperand.IsAfter: {
|
||||||
objectRecordFilters.push({
|
objectRecordFilters.push({
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
gte: rawUIFilter.value,
|
gt: date.toISOString(),
|
||||||
} as DateFilter,
|
} as DateFilter,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ViewFilterOperand.LessThan:
|
}
|
||||||
|
case ViewFilterOperand.IsBefore: {
|
||||||
objectRecordFilters.push({
|
objectRecordFilters.push({
|
||||||
[correspondingField.name]: {
|
[correspondingField.name]: {
|
||||||
lte: rawUIFilter.value,
|
lt: date.toISOString(),
|
||||||
} as DateFilter,
|
} as DateFilter,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ViewFilterOperand.IsEmpty:
|
case ViewFilterOperand.IsEmpty:
|
||||||
case ViewFilterOperand.IsNotEmpty:
|
case ViewFilterOperand.IsNotEmpty: {
|
||||||
applyEmptyFilters(
|
applyEmptyFilters(
|
||||||
rawUIFilter.operand,
|
rawUIFilter.operand,
|
||||||
correspondingField,
|
correspondingField,
|
||||||
@ -366,12 +379,99 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
rawUIFilter.definition.type,
|
rawUIFilter.definition.type,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
case ViewFilterOperand.IsRelative: {
|
||||||
|
const dateRange = z
|
||||||
|
.object({ start: z.date(), end: z.date() })
|
||||||
|
.safeParse(resolvedFilterValue).data;
|
||||||
|
|
||||||
|
const defaultDateRange = resolveFilterValue({
|
||||||
|
value: 'PAST_1_DAY',
|
||||||
|
definition: {
|
||||||
|
type: 'DATE',
|
||||||
|
},
|
||||||
|
operand: ViewFilterOperand.IsRelative,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!defaultDateRange)
|
||||||
|
throw new Error('Failed to resolve default date range');
|
||||||
|
|
||||||
|
const { start, end } = dateRange ?? defaultDateRange;
|
||||||
|
|
||||||
|
objectRecordFilters.push({
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
gte: start.toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
lte: end.toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ViewFilterOperand.Is: {
|
||||||
|
const isValid = resolvedFilterValue instanceof Date;
|
||||||
|
const date = isValid ? resolvedFilterValue : now;
|
||||||
|
|
||||||
|
objectRecordFilters.push({
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
lte: endOfDay(date).toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
gte: startOfDay(date).toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ViewFilterOperand.IsInPast:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
[correspondingField.name]: {
|
||||||
|
lte: now.toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.IsInFuture:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
[correspondingField.name]: {
|
||||||
|
gte: now.toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.IsToday: {
|
||||||
|
objectRecordFilters.push({
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
lte: endOfDay(now).toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
gte: startOfDay(now).toISOString(),
|
||||||
|
} as DateFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, //
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'RATING':
|
case 'RATING':
|
||||||
switch (rawUIFilter.operand) {
|
switch (rawUIFilter.operand) {
|
||||||
case ViewFilterOperand.Is:
|
case ViewFilterOperand.Is:
|
||||||
@ -446,7 +546,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'RELATION': {
|
case 'RELATION': {
|
||||||
if (!isEmptyOperand) {
|
if (!isValuelessOperand) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(rawUIFilter.value);
|
JSON.parse(rawUIFilter.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -743,7 +843,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'SELECT': {
|
case 'SELECT': {
|
||||||
if (isEmptyOperand) {
|
if (isValuelessOperand) {
|
||||||
applyEmptyFilters(
|
applyEmptyFilters(
|
||||||
rawUIFilter.operand,
|
rawUIFilter.operand,
|
||||||
correspondingField,
|
correspondingField,
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { IconChevronLeft, IconChevronRight } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||||
|
|
||||||
|
import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions';
|
||||||
|
import {
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||||
|
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
|
||||||
|
} from './InternalDatePicker';
|
||||||
|
|
||||||
|
const StyledCustomDatePickerHeader = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const years = Array.from(
|
||||||
|
{ length: 200 },
|
||||||
|
(_, i) => new Date().getFullYear() + 5 - i,
|
||||||
|
).map((year) => ({ label: year.toString(), value: year }));
|
||||||
|
|
||||||
|
type AbsoluteDatePickerHeaderProps = {
|
||||||
|
date: Date;
|
||||||
|
onChange?: (date: Date | null) => void;
|
||||||
|
onChangeMonth: (month: number) => void;
|
||||||
|
onChangeYear: (year: number) => void;
|
||||||
|
onAddMonth: () => void;
|
||||||
|
onSubtractMonth: () => void;
|
||||||
|
prevMonthButtonDisabled: boolean;
|
||||||
|
nextMonthButtonDisabled: boolean;
|
||||||
|
isDateTimeInput?: boolean;
|
||||||
|
timeZone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AbsoluteDatePickerHeader = ({
|
||||||
|
date,
|
||||||
|
onChange,
|
||||||
|
onChangeMonth,
|
||||||
|
onChangeYear,
|
||||||
|
onAddMonth,
|
||||||
|
onSubtractMonth,
|
||||||
|
prevMonthButtonDisabled,
|
||||||
|
nextMonthButtonDisabled,
|
||||||
|
isDateTimeInput,
|
||||||
|
timeZone,
|
||||||
|
}: AbsoluteDatePickerHeaderProps) => {
|
||||||
|
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
|
||||||
|
day: date.getDate(),
|
||||||
|
month: date.getMonth() + 1,
|
||||||
|
year: date.getFullYear(),
|
||||||
|
hour: 23,
|
||||||
|
minute: 59,
|
||||||
|
second: 59,
|
||||||
|
millisecond: 999,
|
||||||
|
});
|
||||||
|
|
||||||
|
const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DateTimeInput
|
||||||
|
date={date}
|
||||||
|
isDateTimeInput={isDateTimeInput}
|
||||||
|
onChange={onChange}
|
||||||
|
userTimezone={timeZone}
|
||||||
|
/>
|
||||||
|
<StyledCustomDatePickerHeader>
|
||||||
|
<Select
|
||||||
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||||
|
options={getMonthSelectOptions()}
|
||||||
|
disableBlur
|
||||||
|
onChange={onChangeMonth}
|
||||||
|
value={endOfDayInLocalTimezone.getMonth()}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||||
|
onChange={onChangeYear}
|
||||||
|
value={endOfDayInLocalTimezone.getFullYear()}
|
||||||
|
options={years}
|
||||||
|
disableBlur
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconChevronLeft}
|
||||||
|
onClick={onSubtractMonth}
|
||||||
|
size="medium"
|
||||||
|
disabled={prevMonthButtonDisabled}
|
||||||
|
/>
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconChevronRight}
|
||||||
|
onClick={onAddMonth}
|
||||||
|
size="medium"
|
||||||
|
disabled={nextMonthButtonDisabled}
|
||||||
|
/>
|
||||||
|
</StyledCustomDatePickerHeader>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,52 +2,32 @@ import styled from '@emotion/styled';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import ReactDatePicker from 'react-datepicker';
|
import ReactDatePicker from 'react-datepicker';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import {
|
import { IconCalendarX, OVERLAY_BACKGROUND } from 'twenty-ui';
|
||||||
IconCalendarX,
|
|
||||||
IconChevronLeft,
|
|
||||||
IconChevronRight,
|
|
||||||
OVERLAY_BACKGROUND,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
|
||||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
|
||||||
|
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
||||||
|
import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates';
|
||||||
import { UserContext } from '@/users/contexts/UserContext';
|
import { UserContext } from '@/users/contexts/UserContext';
|
||||||
|
import {
|
||||||
|
VariableDateViewFilterValueDirection,
|
||||||
|
VariableDateViewFilterValueUnit,
|
||||||
|
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
|
||||||
const months = [
|
|
||||||
{ label: 'January', value: 0 },
|
|
||||||
{ label: 'February', value: 1 },
|
|
||||||
{ label: 'March', value: 2 },
|
|
||||||
{ label: 'April', value: 3 },
|
|
||||||
{ label: 'May', value: 4 },
|
|
||||||
{ label: 'June', value: 5 },
|
|
||||||
{ label: 'July', value: 6 },
|
|
||||||
{ label: 'August', value: 7 },
|
|
||||||
{ label: 'September', value: 8 },
|
|
||||||
{ label: 'October', value: 9 },
|
|
||||||
{ label: 'November', value: 10 },
|
|
||||||
{ label: 'December', value: 11 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const years = Array.from(
|
|
||||||
{ length: 200 },
|
|
||||||
(_, i) => new Date().getFullYear() + 5 - i,
|
|
||||||
).map((year) => ({ label: year.toString(), value: year }));
|
|
||||||
|
|
||||||
export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
|
export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
|
||||||
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
|
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
|
||||||
'date-picker-month-and-year-dropdown-month-select';
|
'date-picker-month-and-year-dropdown-month-select';
|
||||||
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
|
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
|
||||||
'date-picker-month-and-year-dropdown-year-select';
|
'date-picker-month-and-year-dropdown-year-select';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
|
||||||
& .react-datepicker {
|
& .react-datepicker {
|
||||||
border-color: ${({ theme }) => theme.border.color.light};
|
border-color: ${({ theme }) => theme.border.color.light};
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -207,6 +187,10 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
& .react-datepicker__month {
|
& .react-datepicker__month {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
pointer-events: ${({ calendarDisabled }) =>
|
||||||
|
calendarDisabled ? 'none' : 'auto'};
|
||||||
|
opacity: ${({ calendarDisabled }) => (calendarDisabled ? '0.5' : '1')};
|
||||||
}
|
}
|
||||||
|
|
||||||
& .react-datepicker__day {
|
& .react-datepicker__day {
|
||||||
@ -288,21 +272,27 @@ const StyledButton = styled(MenuItemLeftContent)`
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCustomDatePickerHeader = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
type InternalDatePickerProps = {
|
type InternalDatePickerProps = {
|
||||||
|
isRelative?: boolean;
|
||||||
date: Date | null;
|
date: Date | null;
|
||||||
|
relativeDate?: {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
};
|
||||||
|
highlightedDateRange?: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
};
|
||||||
onMouseSelect?: (date: Date | null) => void;
|
onMouseSelect?: (date: Date | null) => void;
|
||||||
onChange?: (date: Date | null) => void;
|
onChange?: (date: Date | null) => void;
|
||||||
|
onRelativeDateChange?: (
|
||||||
|
relativeDate: {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
} | null,
|
||||||
|
) => void;
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
isDateTimeInput?: boolean;
|
isDateTimeInput?: boolean;
|
||||||
onEnter?: (date: Date | null) => void;
|
onEnter?: (date: Date | null) => void;
|
||||||
@ -321,6 +311,10 @@ export const InternalDatePicker = ({
|
|||||||
isDateTimeInput,
|
isDateTimeInput,
|
||||||
keyboardEventsDisabled,
|
keyboardEventsDisabled,
|
||||||
onClear,
|
onClear,
|
||||||
|
isRelative,
|
||||||
|
relativeDate,
|
||||||
|
onRelativeDateChange,
|
||||||
|
highlightedDateRange,
|
||||||
}: InternalDatePickerProps) => {
|
}: InternalDatePickerProps) => {
|
||||||
const internalDate = date ?? new Date();
|
const internalDate = date ?? new Date();
|
||||||
|
|
||||||
@ -469,15 +463,20 @@ export const InternalDatePicker = ({
|
|||||||
|
|
||||||
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
|
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
|
||||||
|
|
||||||
|
const highlightedDates = getHighlightedDates(highlightedDateRange);
|
||||||
|
|
||||||
|
const selectedDates = isRelative ? highlightedDates : [dateToUse];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer onKeyDown={handleKeyDown}>
|
<StyledContainer onKeyDown={handleKeyDown} calendarDisabled={isRelative}>
|
||||||
<div className={clearable ? 'clearable ' : ''}>
|
<div className={clearable ? 'clearable ' : ''}>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
open={true}
|
open={true}
|
||||||
selected={dateToUse}
|
selected={dateToUse}
|
||||||
|
selectedDates={selectedDates}
|
||||||
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
|
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
|
||||||
disabledKeyboardNavigation
|
disabledKeyboardNavigation
|
||||||
onChange={handleDateChange}
|
onChange={handleDateChange as any}
|
||||||
customInput={
|
customInput={
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
date={internalDate}
|
date={internalDate}
|
||||||
@ -489,47 +488,31 @@ export const InternalDatePicker = ({
|
|||||||
renderCustomHeader={({
|
renderCustomHeader={({
|
||||||
prevMonthButtonDisabled,
|
prevMonthButtonDisabled,
|
||||||
nextMonthButtonDisabled,
|
nextMonthButtonDisabled,
|
||||||
}) => (
|
}) =>
|
||||||
<>
|
isRelative ? (
|
||||||
<DateTimeInput
|
<RelativeDatePickerHeader
|
||||||
date={internalDate}
|
direction={relativeDate?.direction ?? 'PAST'}
|
||||||
isDateTimeInput={isDateTimeInput}
|
amount={relativeDate?.amount}
|
||||||
onChange={onChange}
|
unit={relativeDate?.unit ?? 'DAY'}
|
||||||
userTimezone={timeZone}
|
onChange={onRelativeDateChange}
|
||||||
/>
|
/>
|
||||||
<StyledCustomDatePickerHeader>
|
) : (
|
||||||
<Select
|
<AbsoluteDatePickerHeader
|
||||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
date={internalDate}
|
||||||
options={months}
|
onChange={onChange}
|
||||||
disableBlur
|
onChangeMonth={handleChangeMonth}
|
||||||
onChange={handleChangeMonth}
|
onChangeYear={handleChangeYear}
|
||||||
value={endOfDayInLocalTimezone.getMonth()}
|
onAddMonth={handleAddMonth}
|
||||||
fullWidth
|
onSubtractMonth={handleSubtractMonth}
|
||||||
/>
|
prevMonthButtonDisabled={prevMonthButtonDisabled}
|
||||||
<Select
|
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
||||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
isDateTimeInput={isDateTimeInput}
|
||||||
onChange={handleChangeYear}
|
timeZone={timeZone}
|
||||||
value={endOfDayInLocalTimezone.getFullYear()}
|
/>
|
||||||
options={years}
|
)
|
||||||
disableBlur
|
}
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<LightIconButton
|
|
||||||
Icon={IconChevronLeft}
|
|
||||||
onClick={handleSubtractMonth}
|
|
||||||
size="medium"
|
|
||||||
disabled={prevMonthButtonDisabled}
|
|
||||||
/>
|
|
||||||
<LightIconButton
|
|
||||||
Icon={IconChevronRight}
|
|
||||||
onClick={handleAddMonth}
|
|
||||||
size="medium"
|
|
||||||
disabled={nextMonthButtonDisabled}
|
|
||||||
/>
|
|
||||||
</StyledCustomDatePickerHeader>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
onSelect={handleDateSelect}
|
onSelect={handleDateSelect}
|
||||||
|
selectsMultiple={isRelative}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{clearable && (
|
{clearable && (
|
||||||
|
|||||||
@ -0,0 +1,113 @@
|
|||||||
|
import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions';
|
||||||
|
import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions';
|
||||||
|
import { Select } from '@/ui/input/components/Select';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import {
|
||||||
|
VariableDateViewFilterValueDirection,
|
||||||
|
variableDateViewFilterValuePartsSchema,
|
||||||
|
VariableDateViewFilterValueUnit,
|
||||||
|
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type RelativeDatePickerHeaderProps = {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
onChange?: (value: {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RelativeDatePickerHeader = (
|
||||||
|
props: RelativeDatePickerHeaderProps,
|
||||||
|
) => {
|
||||||
|
const [direction, setDirection] = useState(props.direction);
|
||||||
|
const [amountString, setAmountString] = useState(
|
||||||
|
props.amount ? props.amount.toString() : '',
|
||||||
|
);
|
||||||
|
const [unit, setUnit] = useState(props.unit);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAmountString(props.amount ? props.amount.toString() : '');
|
||||||
|
setUnit(props.unit);
|
||||||
|
setDirection(props.direction);
|
||||||
|
}, [props.amount, props.unit, props.direction]);
|
||||||
|
|
||||||
|
const textInputValue = direction === 'THIS' ? '' : amountString;
|
||||||
|
const textInputPlaceholder = direction === 'THIS' ? '-' : 'Number';
|
||||||
|
|
||||||
|
const isUnitPlural = props.amount && props.amount > 1 && direction !== 'THIS';
|
||||||
|
const unitSelectOptions = RELATIVE_DATE_UNITS_SELECT_OPTIONS.map((unit) => ({
|
||||||
|
...unit,
|
||||||
|
label: `${unit.label}${isUnitPlural ? 's' : ''}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<Select
|
||||||
|
disableBlur
|
||||||
|
dropdownId="direction-select"
|
||||||
|
value={direction}
|
||||||
|
onChange={(newDirection) => {
|
||||||
|
setDirection(newDirection);
|
||||||
|
if (props.amount === undefined && newDirection !== 'THIS') return;
|
||||||
|
props.onChange?.({
|
||||||
|
direction: newDirection,
|
||||||
|
amount: props.amount,
|
||||||
|
unit: unit,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={textInputValue}
|
||||||
|
onChange={(text) => {
|
||||||
|
const amountString = text.replace(/[^0-9]|^0+/g, '');
|
||||||
|
const amount = parseInt(amountString);
|
||||||
|
|
||||||
|
setAmountString(amountString);
|
||||||
|
|
||||||
|
const valueParts = {
|
||||||
|
direction,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
variableDateViewFilterValuePartsSchema.safeParse(valueParts).success
|
||||||
|
) {
|
||||||
|
props.onChange?.(valueParts);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={textInputPlaceholder}
|
||||||
|
disabled={direction === 'THIS'}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
disableBlur
|
||||||
|
dropdownId="unit-select"
|
||||||
|
value={unit}
|
||||||
|
onChange={(newUnit) => {
|
||||||
|
setUnit(newUnit);
|
||||||
|
if (direction !== 'THIS' && props.amount === undefined) return;
|
||||||
|
props.onChange?.({
|
||||||
|
direction,
|
||||||
|
amount: props.amount,
|
||||||
|
unit: newUnit,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
options={unitSelectOptions}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { VariableDateViewFilterValueDirection } from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
|
|
||||||
|
type RelativeDateDirectionOption = {
|
||||||
|
value: VariableDateViewFilterValueDirection;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RELATIVE_DATE_DIRECTION_SELECT_OPTIONS: RelativeDateDirectionOption[] =
|
||||||
|
[
|
||||||
|
{ value: 'PAST', label: 'Past' },
|
||||||
|
{ value: 'THIS', label: 'This' },
|
||||||
|
{ value: 'NEXT', label: 'Next' },
|
||||||
|
];
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { VariableDateViewFilterValueUnit } from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
|
|
||||||
|
type RelativeDateUnit = {
|
||||||
|
value: VariableDateViewFilterValueUnit;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnit[] = [
|
||||||
|
{ value: 'DAY', label: 'Day' },
|
||||||
|
{ value: 'WEEK', label: 'Week' },
|
||||||
|
{ value: 'MONTH', label: 'Month' },
|
||||||
|
{ value: 'YEAR', label: 'Year' },
|
||||||
|
];
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { addDays, addMonths, startOfDay, subMonths } from 'date-fns';
|
||||||
|
|
||||||
|
export const getHighlightedDates = (highlightedDateRange?: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
}): Date[] => {
|
||||||
|
if (!highlightedDateRange) return [];
|
||||||
|
const { start, end } = highlightedDateRange;
|
||||||
|
|
||||||
|
const highlightedDates: Date[] = [];
|
||||||
|
const currentDate = startOfDay(new Date());
|
||||||
|
const minDate = subMonths(currentDate, 2);
|
||||||
|
const maxDate = addMonths(currentDate, 2);
|
||||||
|
|
||||||
|
let dateToHighlight = start < minDate ? minDate : start;
|
||||||
|
const lastDate = end > maxDate ? maxDate : end;
|
||||||
|
|
||||||
|
while (dateToHighlight <= lastDate) {
|
||||||
|
highlightedDates.push(dateToHighlight);
|
||||||
|
dateToHighlight = addDays(dateToHighlight, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlightedDates;
|
||||||
|
};
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
const getMonthName = (index: number): string =>
|
||||||
|
new Intl.DateTimeFormat('en-US', { month: 'long' }).format(
|
||||||
|
new Date(0, index, 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMonthNames = (monthNames: string[] = []): string[] => {
|
||||||
|
if (monthNames.length === 12) return monthNames;
|
||||||
|
|
||||||
|
return getMonthNames([...monthNames, getMonthName(monthNames.length)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMonthSelectOptions = (): { label: string; value: number }[] =>
|
||||||
|
getMonthNames().map((month, index) => ({
|
||||||
|
label: month,
|
||||||
|
value: index,
|
||||||
|
}));
|
||||||
@ -9,6 +9,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
|
||||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||||
@ -74,7 +75,13 @@ export const EditableFilterDropdownButton = ({
|
|||||||
const { id: fieldId, value, operand } = viewFilter;
|
const { id: fieldId, value, operand } = viewFilter;
|
||||||
if (
|
if (
|
||||||
!value &&
|
!value &&
|
||||||
![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand)
|
![
|
||||||
|
FilterOperand.IsEmpty,
|
||||||
|
FilterOperand.IsNotEmpty,
|
||||||
|
ViewFilterOperand.IsInPast,
|
||||||
|
ViewFilterOperand.IsInFuture,
|
||||||
|
ViewFilterOperand.IsToday,
|
||||||
|
].includes(operand)
|
||||||
) {
|
) {
|
||||||
deleteCombinedViewFilter(fieldId);
|
deleteCombinedViewFilter(fieldId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,14 @@ export enum ViewFilterOperand {
|
|||||||
IsNot = 'isNot',
|
IsNot = 'isNot',
|
||||||
LessThan = 'lessThan',
|
LessThan = 'lessThan',
|
||||||
GreaterThan = 'greaterThan',
|
GreaterThan = 'greaterThan',
|
||||||
|
IsBefore = 'isBefore',
|
||||||
|
IsAfter = 'isAfter',
|
||||||
Contains = 'contains',
|
Contains = 'contains',
|
||||||
DoesNotContain = 'doesNotContain',
|
DoesNotContain = 'doesNotContain',
|
||||||
IsEmpty = 'isEmpty',
|
IsEmpty = 'isEmpty',
|
||||||
IsNotEmpty = 'isNotEmpty',
|
IsNotEmpty = 'isNotEmpty',
|
||||||
|
IsRelative = 'isRelative',
|
||||||
|
IsInPast = 'isInPast',
|
||||||
|
IsInFuture = 'isInFuture',
|
||||||
|
IsToday = 'isToday',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import {
|
||||||
|
VariableDateViewFilterValueDirection,
|
||||||
|
VariableDateViewFilterValueUnit,
|
||||||
|
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||||
|
|
||||||
|
export const computeVariableDateViewFilterValue = (
|
||||||
|
direction: VariableDateViewFilterValueDirection,
|
||||||
|
amount: number | undefined,
|
||||||
|
unit: VariableDateViewFilterValueUnit,
|
||||||
|
) => `${direction}_${amount?.toString()}_${unit}`;
|
||||||
@ -0,0 +1,190 @@
|
|||||||
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import {
|
||||||
|
addDays,
|
||||||
|
addMonths,
|
||||||
|
addWeeks,
|
||||||
|
addYears,
|
||||||
|
endOfDay,
|
||||||
|
endOfMonth,
|
||||||
|
endOfWeek,
|
||||||
|
endOfYear,
|
||||||
|
roundToNearestMinutes,
|
||||||
|
startOfDay,
|
||||||
|
startOfMonth,
|
||||||
|
startOfWeek,
|
||||||
|
startOfYear,
|
||||||
|
subDays,
|
||||||
|
subMonths,
|
||||||
|
subWeeks,
|
||||||
|
subYears,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const variableDateViewFilterValueDirectionSchema = z.enum([
|
||||||
|
'NEXT',
|
||||||
|
'THIS',
|
||||||
|
'PAST',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type VariableDateViewFilterValueDirection = z.infer<
|
||||||
|
typeof variableDateViewFilterValueDirectionSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
const variableDateViewFilterValueAmountSchema = z
|
||||||
|
.union([z.coerce.number().int().positive(), z.literal('undefined')])
|
||||||
|
.transform((val) => (val === 'undefined' ? undefined : val));
|
||||||
|
|
||||||
|
export const variableDateViewFilterValueUnitSchema = z.enum([
|
||||||
|
'DAY',
|
||||||
|
'WEEK',
|
||||||
|
'MONTH',
|
||||||
|
'YEAR',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type VariableDateViewFilterValueUnit = z.infer<
|
||||||
|
typeof variableDateViewFilterValueUnitSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const variableDateViewFilterValuePartsSchema = z
|
||||||
|
.object({
|
||||||
|
direction: variableDateViewFilterValueDirectionSchema,
|
||||||
|
amount: variableDateViewFilterValueAmountSchema,
|
||||||
|
unit: variableDateViewFilterValueUnitSchema,
|
||||||
|
})
|
||||||
|
.refine((data) => !(data.amount === undefined && data.direction !== 'THIS'), {
|
||||||
|
message: "Amount cannot be 'undefined' unless direction is 'THIS'",
|
||||||
|
});
|
||||||
|
|
||||||
|
const variableDateViewFilterValueSchema = z.string().transform((value) => {
|
||||||
|
const [direction, amount, unit] = value.split('_');
|
||||||
|
|
||||||
|
return variableDateViewFilterValuePartsSchema.parse({
|
||||||
|
direction,
|
||||||
|
amount,
|
||||||
|
unit,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const addUnit = (
|
||||||
|
date: Date,
|
||||||
|
amount: number,
|
||||||
|
unit: VariableDateViewFilterValueUnit,
|
||||||
|
) => {
|
||||||
|
switch (unit) {
|
||||||
|
case 'DAY':
|
||||||
|
return addDays(date, amount);
|
||||||
|
case 'WEEK':
|
||||||
|
return addWeeks(date, amount);
|
||||||
|
case 'MONTH':
|
||||||
|
return addMonths(date, amount);
|
||||||
|
case 'YEAR':
|
||||||
|
return addYears(date, amount);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subUnit = (
|
||||||
|
date: Date,
|
||||||
|
amount: number,
|
||||||
|
unit: VariableDateViewFilterValueUnit,
|
||||||
|
) => {
|
||||||
|
switch (unit) {
|
||||||
|
case 'DAY':
|
||||||
|
return subDays(date, amount);
|
||||||
|
case 'WEEK':
|
||||||
|
return subWeeks(date, amount);
|
||||||
|
case 'MONTH':
|
||||||
|
return subMonths(date, amount);
|
||||||
|
case 'YEAR':
|
||||||
|
return subYears(date, amount);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => {
|
||||||
|
switch (unit) {
|
||||||
|
case 'DAY':
|
||||||
|
return startOfDay(date);
|
||||||
|
case 'WEEK':
|
||||||
|
return startOfWeek(date);
|
||||||
|
case 'MONTH':
|
||||||
|
return startOfMonth(date);
|
||||||
|
case 'YEAR':
|
||||||
|
return startOfYear(date);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const endOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => {
|
||||||
|
switch (unit) {
|
||||||
|
case 'DAY':
|
||||||
|
return endOfDay(date);
|
||||||
|
case 'WEEK':
|
||||||
|
return endOfWeek(date);
|
||||||
|
case 'MONTH':
|
||||||
|
return endOfMonth(date);
|
||||||
|
case 'YEAR':
|
||||||
|
return endOfYear(date);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveVariableDateViewFilterValueFromRelativeDate = (relativeDate: {
|
||||||
|
direction: VariableDateViewFilterValueDirection;
|
||||||
|
amount?: number;
|
||||||
|
unit: VariableDateViewFilterValueUnit;
|
||||||
|
}) => {
|
||||||
|
const { direction, amount, unit } = relativeDate;
|
||||||
|
const now = roundToNearestMinutes(new Date());
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'NEXT':
|
||||||
|
if (amount === undefined) throw new Error('Amount is required');
|
||||||
|
return {
|
||||||
|
start: now,
|
||||||
|
end: addUnit(now, amount, unit),
|
||||||
|
...relativeDate,
|
||||||
|
};
|
||||||
|
case 'PAST':
|
||||||
|
if (amount === undefined) throw new Error('Amount is required');
|
||||||
|
return {
|
||||||
|
start: subUnit(now, amount, unit),
|
||||||
|
end: now,
|
||||||
|
...relativeDate,
|
||||||
|
};
|
||||||
|
case 'THIS':
|
||||||
|
return {
|
||||||
|
start: startOfUnit(now, unit),
|
||||||
|
end: endOfUnit(now, unit),
|
||||||
|
...relativeDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveVariableDateViewFilterValue = (value?: string | null) => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
const relativeDate = variableDateViewFilterValueSchema.parse(value);
|
||||||
|
return resolveVariableDateViewFilterValueFromRelativeDate(relativeDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolvedDateViewFilterValue<O extends ViewFilterOperand> =
|
||||||
|
O extends ViewFilterOperand.IsRelative
|
||||||
|
? ReturnType<typeof resolveVariableDateViewFilterValue>
|
||||||
|
: Date | null;
|
||||||
|
|
||||||
|
type PartialViewFilter<O extends ViewFilterOperand> = Pick<
|
||||||
|
ViewFilter,
|
||||||
|
'value'
|
||||||
|
> & { operand: O };
|
||||||
|
|
||||||
|
export const resolveDateViewFilterValue = <O extends ViewFilterOperand>(
|
||||||
|
viewFilter: PartialViewFilter<O>,
|
||||||
|
): ResolvedDateViewFilterValue<O> => {
|
||||||
|
if (!viewFilter.value) return null;
|
||||||
|
|
||||||
|
if (viewFilter.operand === ViewFilterOperand.IsRelative) {
|
||||||
|
return resolveVariableDateViewFilterValue(
|
||||||
|
viewFilter.value,
|
||||||
|
) as ResolvedDateViewFilterValue<O>;
|
||||||
|
}
|
||||||
|
return new Date(viewFilter.value) as ResolvedDateViewFilterValue<O>;
|
||||||
|
};
|
||||||
@ -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 { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue';
|
||||||
|
import {
|
||||||
|
resolveDateViewFilterValue,
|
||||||
|
ResolvedDateViewFilterValue,
|
||||||
|
} from './resolveDateViewFilterValue';
|
||||||
|
|
||||||
|
type ResolvedFilterValue<
|
||||||
|
T extends FilterType,
|
||||||
|
O extends ViewFilterOperand,
|
||||||
|
> = T extends 'DATE' | 'DATE_TIME'
|
||||||
|
? ResolvedDateViewFilterValue<O>
|
||||||
|
: T extends 'NUMBER'
|
||||||
|
? ReturnType<typeof resolveNumberViewFilterValue>
|
||||||
|
: string;
|
||||||
|
|
||||||
|
type PartialFilter<T extends FilterType, O extends ViewFilterOperand> = Pick<
|
||||||
|
Filter,
|
||||||
|
'value'
|
||||||
|
> & {
|
||||||
|
definition: { type: T };
|
||||||
|
operand: O;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveFilterValue = <
|
||||||
|
T extends FilterType,
|
||||||
|
O extends ViewFilterOperand,
|
||||||
|
>(
|
||||||
|
filter: PartialFilter<T, O>,
|
||||||
|
) => {
|
||||||
|
switch (filter.definition.type) {
|
||||||
|
case 'DATE':
|
||||||
|
case 'DATE_TIME':
|
||||||
|
return resolveDateViewFilterValue(filter) as ResolvedFilterValue<T, O>;
|
||||||
|
case 'NUMBER':
|
||||||
|
return resolveNumberViewFilterValue(filter) as ResolvedFilterValue<T, O>;
|
||||||
|
default:
|
||||||
|
return filter.value as ResolvedFilterValue<T, O>;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { ViewFilter } from '@/views/types/ViewFilter';
|
||||||
|
|
||||||
|
export const resolveNumberViewFilterValue = (
|
||||||
|
viewFilter: Pick<ViewFilter, 'value'>,
|
||||||
|
) => {
|
||||||
|
return viewFilter.value === '' ? null : +viewFilter.value;
|
||||||
|
};
|
||||||
@ -62,9 +62,9 @@ export class GraphqlQueryParser {
|
|||||||
return queryBuilder;
|
return queryBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkForDeletedAtFilter(
|
private checkForDeletedAtFilter = (
|
||||||
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
||||||
): boolean {
|
): boolean => {
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
return filter.some((subFilter) =>
|
return filter.some((subFilter) =>
|
||||||
this.checkForDeletedAtFilter(subFilter),
|
this.checkForDeletedAtFilter(subFilter),
|
||||||
@ -86,7 +86,7 @@ export class GraphqlQueryParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
applyOrderToBuilder(
|
applyOrderToBuilder(
|
||||||
queryBuilder: SelectQueryBuilder<any>,
|
queryBuilder: SelectQueryBuilder<any>,
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||||
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
||||||
|
|||||||
@ -17310,6 +17310,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/pluralize@npm:^0.0.33":
|
||||||
|
version: 0.0.33
|
||||||
|
resolution: "@types/pluralize@npm:0.0.33"
|
||||||
|
checksum: 10c0/24899caf85b79dd291a6b6e9b9f3b67b452b18d578d0ac0d531a705bf5ee0361d9386ea1f8532c64de9e22c6e9606c5497787bb5e31bd299c487980436c59785
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/pretty-hrtime@npm:^1.0.0":
|
"@types/pretty-hrtime@npm:^1.0.0":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@types/pretty-hrtime@npm:1.0.3"
|
resolution: "@types/pretty-hrtime@npm:1.0.3"
|
||||||
@ -47693,6 +47700,7 @@ __metadata:
|
|||||||
"@types/passport-google-oauth20": "npm:^2.0.11"
|
"@types/passport-google-oauth20": "npm:^2.0.11"
|
||||||
"@types/passport-jwt": "npm:^3.0.8"
|
"@types/passport-jwt": "npm:^3.0.8"
|
||||||
"@types/passport-microsoft": "npm:^1.0.3"
|
"@types/passport-microsoft": "npm:^1.0.3"
|
||||||
|
"@types/pluralize": "npm:^0.0.33"
|
||||||
"@types/react": "npm:^18.2.39"
|
"@types/react": "npm:^18.2.39"
|
||||||
"@types/react-datepicker": "npm:^6.2.0"
|
"@types/react-datepicker": "npm:^6.2.0"
|
||||||
"@types/react-dom": "npm:^18.2.15"
|
"@types/react-dom": "npm:^18.2.15"
|
||||||
|
|||||||
Reference in New Issue
Block a user