Date filter improvements (#5917) (#7196)

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:
ad-elias
2024-09-27 15:57:38 +02:00
committed by GitHub
parent c9c2f32922
commit 9d36493cf0
28 changed files with 983 additions and 131 deletions

View File

@ -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",

View File

@ -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',

View File

@ -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}
/>
);

View File

@ -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,
});
}

View File

@ -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('');
};

View File

@ -1,5 +1,4 @@
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { FilterDefinition } from './FilterDefinition';
export type Filter = {

View File

@ -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, []],

View File

@ -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 ?? '',
};
};

View File

@ -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 ': ';
}

View File

@ -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,

View File

@ -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(' ');
};

View File

@ -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,

View File

@ -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>
</>
);
};

View File

@ -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 && (

View File

@ -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>
);
};

View File

@ -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' },
];

View File

@ -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' },
];

View File

@ -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;
};

View File

@ -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,
}));

View File

@ -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);
}

View File

@ -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',
}

View File

@ -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}`;

View File

@ -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>;
};

View File

@ -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>;
}
};

View File

@ -0,0 +1,7 @@
import { ViewFilter } from '@/views/types/ViewFilter';
export const resolveNumberViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return viewFilter.value === '' ? null : +viewFilter.value;
};

View File

@ -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>,

View File

@ -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,

View File

@ -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"