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/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",
|
||||
|
||||
@ -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>
|
||||
@ -72,7 +81,7 @@ export const MultipleFiltersDropdownContent = ({
|
||||
<ObjectFilterDropdownOperandSelect />
|
||||
</StyledOperandSelectContainer>
|
||||
)}
|
||||
{!isEmptyOperand && selectedOperandInDropdown && (
|
||||
{isConfigurable && selectedOperandInDropdown && (
|
||||
<>
|
||||
{[
|
||||
'TEXT',
|
||||
|
||||
@ -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 {
|
||||
@ -23,28 +32,35 @@ 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,
|
||||
});
|
||||
@ -52,11 +68,56 @@ export const ObjectFilterDropdownDateInput = () => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -13,6 +15,7 @@ export const useSelectFilter = () => {
|
||||
setFilterDefinitionUsedInDropdown,
|
||||
setSelectedOperandInDropdown,
|
||||
setObjectFilterDropdownSearchInput,
|
||||
selectFilter: filterDropdownSelectFilter,
|
||||
} = useFilterDropdown();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
@ -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('');
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
import { FilterDefinition } from './FilterDefinition';
|
||||
|
||||
export type Filter = {
|
||||
|
||||
@ -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 = [
|
||||
@ -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, []],
|
||||
|
||||
@ -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';
|
||||
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:
|
||||
@ -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 '';
|
||||
}
|
||||
@ -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 ': ';
|
||||
}
|
||||
|
||||
@ -31,13 +31,23 @@ export const getOperandsForFilterType = (
|
||||
];
|
||||
case 'CURRENCY':
|
||||
case 'NUMBER':
|
||||
case 'DATE_TIME':
|
||||
case 'DATE':
|
||||
return [
|
||||
ViewFilterOperand.GreaterThan,
|
||||
ViewFilterOperand.LessThan,
|
||||
...emptyOperands,
|
||||
];
|
||||
case 'DATE_TIME':
|
||||
case 'DATE':
|
||||
return [
|
||||
ViewFilterOperand.Is,
|
||||
ViewFilterOperand.IsRelative,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
ViewFilterOperand.IsBefore,
|
||||
ViewFilterOperand.IsAfter,
|
||||
...emptyOperands,
|
||||
];
|
||||
case 'RATING':
|
||||
return [
|
||||
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,
|
||||
convertRatingToRatingValue,
|
||||
} 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';
|
||||
|
||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||
@ -289,16 +292,19 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
(field) => field.id === rawUIFilter.fieldMetadataId,
|
||||
);
|
||||
|
||||
const isEmptyOperand = [
|
||||
const isValuelessOperand = [
|
||||
ViewFilterOperand.IsEmpty,
|
||||
ViewFilterOperand.IsNotEmpty,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
].includes(rawUIFilter.operand);
|
||||
|
||||
if (!correspondingField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isEmptyOperand) {
|
||||
if (!isValuelessOperand) {
|
||||
if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') {
|
||||
continue;
|
||||
}
|
||||
@ -341,24 +347,31 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
}
|
||||
break;
|
||||
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) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
case ViewFilterOperand.IsAfter: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
gte: rawUIFilter.value,
|
||||
gt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.LessThan:
|
||||
}
|
||||
case ViewFilterOperand.IsBefore: {
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name]: {
|
||||
lte: rawUIFilter.value,
|
||||
lt: date.toISOString(),
|
||||
} as DateFilter,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewFilterOperand.IsEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty:
|
||||
case ViewFilterOperand.IsNotEmpty: {
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
correspondingField,
|
||||
@ -366,12 +379,99 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
rawUIFilter.definition.type,
|
||||
);
|
||||
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:
|
||||
throw new Error(
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, //
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'RATING':
|
||||
switch (rawUIFilter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
@ -446,7 +546,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
}
|
||||
break;
|
||||
case 'RELATION': {
|
||||
if (!isEmptyOperand) {
|
||||
if (!isValuelessOperand) {
|
||||
try {
|
||||
JSON.parse(rawUIFilter.value);
|
||||
} catch (e) {
|
||||
@ -743,7 +843,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
}
|
||||
break;
|
||||
case 'SELECT': {
|
||||
if (isEmptyOperand) {
|
||||
if (isValuelessOperand) {
|
||||
applyEmptyFilters(
|
||||
rawUIFilter.operand,
|
||||
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 ReactDatePicker from 'react-datepicker';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
IconCalendarX,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
OVERLAY_BACKGROUND,
|
||||
} from 'twenty-ui';
|
||||
import { IconCalendarX, OVERLAY_BACKGROUND } from 'twenty-ui';
|
||||
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
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 { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
|
||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||
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 {
|
||||
VariableDateViewFilterValueDirection,
|
||||
VariableDateViewFilterValueUnit,
|
||||
} from '@/views/utils/view-filter-value/resolveDateViewFilterValue';
|
||||
import { useContext } from 'react';
|
||||
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_MONTH_SELECT_ID =
|
||||
'date-picker-month-and-year-dropdown-month-select';
|
||||
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
|
||||
'date-picker-month-and-year-dropdown-year-select';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const StyledContainer = styled.div<{ calendarDisabled?: boolean }>`
|
||||
& .react-datepicker {
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
background: transparent;
|
||||
@ -207,6 +187,10 @@ const StyledContainer = styled.div`
|
||||
|
||||
& .react-datepicker__month {
|
||||
margin-top: 0;
|
||||
|
||||
pointer-events: ${({ calendarDisabled }) =>
|
||||
calendarDisabled ? 'none' : 'auto'};
|
||||
opacity: ${({ calendarDisabled }) => (calendarDisabled ? '0.5' : '1')};
|
||||
}
|
||||
|
||||
& .react-datepicker__day {
|
||||
@ -288,21 +272,27 @@ const StyledButton = styled(MenuItemLeftContent)`
|
||||
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 = {
|
||||
isRelative?: boolean;
|
||||
date: Date | null;
|
||||
relativeDate?: {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
};
|
||||
highlightedDateRange?: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
onMouseSelect?: (date: Date | null) => void;
|
||||
onChange?: (date: Date | null) => void;
|
||||
onRelativeDateChange?: (
|
||||
relativeDate: {
|
||||
direction: VariableDateViewFilterValueDirection;
|
||||
amount?: number;
|
||||
unit: VariableDateViewFilterValueUnit;
|
||||
} | null,
|
||||
) => void;
|
||||
clearable?: boolean;
|
||||
isDateTimeInput?: boolean;
|
||||
onEnter?: (date: Date | null) => void;
|
||||
@ -321,6 +311,10 @@ export const InternalDatePicker = ({
|
||||
isDateTimeInput,
|
||||
keyboardEventsDisabled,
|
||||
onClear,
|
||||
isRelative,
|
||||
relativeDate,
|
||||
onRelativeDateChange,
|
||||
highlightedDateRange,
|
||||
}: InternalDatePickerProps) => {
|
||||
const internalDate = date ?? new Date();
|
||||
|
||||
@ -469,15 +463,20 @@ export const InternalDatePicker = ({
|
||||
|
||||
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
|
||||
|
||||
const highlightedDates = getHighlightedDates(highlightedDateRange);
|
||||
|
||||
const selectedDates = isRelative ? highlightedDates : [dateToUse];
|
||||
|
||||
return (
|
||||
<StyledContainer onKeyDown={handleKeyDown}>
|
||||
<StyledContainer onKeyDown={handleKeyDown} calendarDisabled={isRelative}>
|
||||
<div className={clearable ? 'clearable ' : ''}>
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={dateToUse}
|
||||
selectedDates={selectedDates}
|
||||
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
|
||||
disabledKeyboardNavigation
|
||||
onChange={handleDateChange}
|
||||
onChange={handleDateChange as any}
|
||||
customInput={
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
@ -489,47 +488,31 @@ export const InternalDatePicker = ({
|
||||
renderCustomHeader={({
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
<>
|
||||
<DateTimeInput
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
userTimezone={timeZone}
|
||||
}) =>
|
||||
isRelative ? (
|
||||
<RelativeDatePickerHeader
|
||||
direction={relativeDate?.direction ?? 'PAST'}
|
||||
amount={relativeDate?.amount}
|
||||
unit={relativeDate?.unit ?? 'DAY'}
|
||||
onChange={onRelativeDateChange}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
|
||||
options={months}
|
||||
disableBlur
|
||||
onChange={handleChangeMonth}
|
||||
value={endOfDayInLocalTimezone.getMonth()}
|
||||
fullWidth
|
||||
/>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||
onChange={handleChangeYear}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<AbsoluteDatePickerHeader
|
||||
date={internalDate}
|
||||
onChange={onChange}
|
||||
onChangeMonth={handleChangeMonth}
|
||||
onChangeYear={handleChangeYear}
|
||||
onAddMonth={handleAddMonth}
|
||||
onSubtractMonth={handleSubtractMonth}
|
||||
prevMonthButtonDisabled={prevMonthButtonDisabled}
|
||||
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onSelect={handleDateSelect}
|
||||
selectsMultiple={isRelative}
|
||||
/>
|
||||
</div>
|
||||
{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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
||||
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
|
||||
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
|
||||
@ -74,7 +75,13 @@ export const EditableFilterDropdownButton = ({
|
||||
const { id: fieldId, value, operand } = viewFilter;
|
||||
if (
|
||||
!value &&
|
||||
![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand)
|
||||
![
|
||||
FilterOperand.IsEmpty,
|
||||
FilterOperand.IsNotEmpty,
|
||||
ViewFilterOperand.IsInPast,
|
||||
ViewFilterOperand.IsInFuture,
|
||||
ViewFilterOperand.IsToday,
|
||||
].includes(operand)
|
||||
) {
|
||||
deleteCombinedViewFilter(fieldId);
|
||||
}
|
||||
|
||||
@ -4,8 +4,14 @@ export enum ViewFilterOperand {
|
||||
IsNot = 'isNot',
|
||||
LessThan = 'lessThan',
|
||||
GreaterThan = 'greaterThan',
|
||||
IsBefore = 'isBefore',
|
||||
IsAfter = 'isAfter',
|
||||
Contains = 'contains',
|
||||
DoesNotContain = 'doesNotContain',
|
||||
IsEmpty = 'isEmpty',
|
||||
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;
|
||||
}
|
||||
|
||||
private checkForDeletedAtFilter(
|
||||
private checkForDeletedAtFilter = (
|
||||
filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[],
|
||||
): boolean {
|
||||
): boolean => {
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.some((subFilter) =>
|
||||
this.checkForDeletedAtFilter(subFilter),
|
||||
@ -86,7 +86,7 @@ export class GraphqlQueryParser {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
applyOrderToBuilder(
|
||||
queryBuilder: SelectQueryBuilder<any>,
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
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 { 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 { 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 { 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({
|
||||
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
||||
|
||||
@ -17310,6 +17310,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 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-jwt": "npm:^3.0.8"
|
||||
"@types/passport-microsoft": "npm:^1.0.3"
|
||||
"@types/pluralize": "npm:^0.0.33"
|
||||
"@types/react": "npm:^18.2.39"
|
||||
"@types/react-datepicker": "npm:^6.2.0"
|
||||
"@types/react-dom": "npm:^18.2.15"
|
||||
|
||||
Reference in New Issue
Block a user