Skip to content

Commit a31d7d3

Browse files
lucasbordeauprastoin
authored andcommitted
Made the code more resilient to stale value prop in date filter (#17038)
Fixes : #17035 There was a bad pattern which risked introduce this bug in `turnRecordFilterIntoRecordGqlOperationFilter` after recent refactor of date logic and date filters, which didn't create any bug during dev time because it wasn't tested with value combinations that existed before the refactor. Code has been re-organized to make it resilient to any wrong value combination. Also fixed a small bug that appeared during QA with `DATE` filter type, which was blocking the save button from disappearing after a save.
1 parent 28e79de commit a31d7d3

File tree

3 files changed

+150
-127
lines changed

3 files changed

+150
-127
lines changed

packages/twenty-front/src/modules/views/utils/__tests__/areViewFiltersEqual.test.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ViewFilter } from '@/views/types/ViewFilter';
2-
import { ViewFilterOperand } from 'twenty-shared/types';
32
import { areViewFiltersEqual } from '@/views/utils/areViewFiltersEqual';
3+
import { ViewFilterOperand } from 'twenty-shared/types';
44

55
describe('areViewFiltersEqual', () => {
66
const baseFilter: ViewFilter = {
@@ -21,13 +21,6 @@ describe('areViewFiltersEqual', () => {
2121
expect(areViewFiltersEqual(filterA, filterB)).toBe(true);
2222
});
2323

24-
it('should return false when displayValue is different', () => {
25-
const filterA = { ...baseFilter };
26-
const filterB = { ...baseFilter, displayValue: 'different' };
27-
28-
expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
29-
});
30-
3124
it('should return false when fieldMetadataId is different', () => {
3225
const filterA = { ...baseFilter };
3326
const filterB = { ...baseFilter, fieldMetadataId: 'field-2' };

packages/twenty-front/src/modules/views/utils/areViewFiltersEqual.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export const areViewFiltersEqual = (
1010
'viewFilterGroupId',
1111
'positionInViewFilterGroup',
1212
'value',
13-
'displayValue',
1413
'operand',
1514
'subFieldName',
1615
];

packages/twenty-shared/src/utils/filter/turnRecordFilterIntoGqlOperationFilter.ts

Lines changed: 149 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
168168
);
169169
}
170170
case 'DATE': {
171-
if (recordFilter.operand === RecordFilterOperand.IS_RELATIVE) {
171+
const itsARelativeDateFilter =
172+
recordFilter.operand === RecordFilterOperand.IS_RELATIVE;
173+
174+
if (itsARelativeDateFilter) {
172175
const relativeDateFilterValue = resolveRelativeDateFilterStringified(
173176
recordFilter.value,
174177
);
@@ -204,62 +207,75 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
204207
};
205208
}
206209

207-
const nowAsPlainDate = Temporal.Now.plainDateISO(
208-
filterValueDependencies.timeZone,
209-
).toString();
210+
const operandIsTakingNowAsReference =
211+
recordFilter.operand === RecordFilterOperand.IS_TODAY ||
212+
recordFilter.operand === RecordFilterOperand.IS_IN_PAST ||
213+
recordFilter.operand === RecordFilterOperand.IS_IN_FUTURE;
210214

211-
const plainDateFilter = recordFilter.value;
215+
if (operandIsTakingNowAsReference) {
216+
const nowAsPlainDate = Temporal.Now.plainDateISO(
217+
filterValueDependencies.timeZone,
218+
).toString();
212219

213-
switch (recordFilter.operand) {
214-
case RecordFilterOperand.IS_AFTER: {
215-
return {
216-
[correspondingFieldMetadataItem.name]: {
217-
gte: plainDateFilter,
218-
} as DateFilter,
219-
};
220-
}
221-
case RecordFilterOperand.IS_BEFORE: {
222-
return {
223-
[correspondingFieldMetadataItem.name]: {
224-
lt: plainDateFilter,
225-
} as DateFilter,
226-
};
220+
switch (recordFilter.operand) {
221+
case RecordFilterOperand.IS_IN_PAST:
222+
return {
223+
[correspondingFieldMetadataItem.name]: {
224+
lt: nowAsPlainDate,
225+
} as DateFilter,
226+
};
227+
case RecordFilterOperand.IS_IN_FUTURE:
228+
return {
229+
[correspondingFieldMetadataItem.name]: {
230+
gte: nowAsPlainDate,
231+
} as DateFilter,
232+
};
233+
case RecordFilterOperand.IS_TODAY: {
234+
return {
235+
[correspondingFieldMetadataItem.name]: {
236+
eq: nowAsPlainDate,
237+
} as DateFilter,
238+
};
239+
}
227240
}
241+
} else {
242+
const plainDateFilter = recordFilter.value;
228243

229-
case RecordFilterOperand.IS: {
230-
return {
231-
[correspondingFieldMetadataItem.name]: {
232-
eq: plainDateFilter,
233-
} as DateFilter,
234-
};
235-
}
236-
case RecordFilterOperand.IS_IN_PAST:
237-
return {
238-
[correspondingFieldMetadataItem.name]: {
239-
lt: nowAsPlainDate,
240-
} as DateFilter,
241-
};
242-
case RecordFilterOperand.IS_IN_FUTURE:
243-
return {
244-
[correspondingFieldMetadataItem.name]: {
245-
gte: nowAsPlainDate,
246-
} as DateFilter,
247-
};
248-
case RecordFilterOperand.IS_TODAY: {
249-
return {
250-
[correspondingFieldMetadataItem.name]: {
251-
eq: nowAsPlainDate,
252-
} as DateFilter,
253-
};
244+
switch (recordFilter.operand) {
245+
case RecordFilterOperand.IS_AFTER: {
246+
return {
247+
[correspondingFieldMetadataItem.name]: {
248+
gte: plainDateFilter,
249+
} as DateFilter,
250+
};
251+
}
252+
case RecordFilterOperand.IS_BEFORE: {
253+
return {
254+
[correspondingFieldMetadataItem.name]: {
255+
lt: plainDateFilter,
256+
} as DateFilter,
257+
};
258+
}
259+
260+
case RecordFilterOperand.IS: {
261+
return {
262+
[correspondingFieldMetadataItem.name]: {
263+
eq: plainDateFilter,
264+
} as DateFilter,
265+
};
266+
}
254267
}
255-
default:
256-
throw new Error(
257-
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
258-
);
259268
}
269+
270+
throw new Error(
271+
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
272+
);
260273
}
261274
case 'DATE_TIME': {
262-
if (recordFilter.operand === RecordFilterOperand.IS_RELATIVE) {
275+
const itsARelativeDateTimeFilter =
276+
recordFilter.operand === RecordFilterOperand.IS_RELATIVE;
277+
278+
if (itsARelativeDateTimeFilter) {
263279
const resolvedFilterValue = resolveDateTimeFilter(recordFilter);
264280

265281
const parsedRelativeDateFilterValue =
@@ -307,81 +323,96 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
307323
};
308324
}
309325

310-
if (!isNonEmptyString(recordFilter.value)) {
311-
throw new Error(`Date filter is empty`);
312-
}
313-
314-
const resolvedDateTime = Temporal.Instant.from(recordFilter.value);
326+
const operandIsTakingNowAsReference =
327+
recordFilter.operand === RecordFilterOperand.IS_TODAY ||
328+
recordFilter.operand === RecordFilterOperand.IS_IN_PAST ||
329+
recordFilter.operand === RecordFilterOperand.IS_IN_FUTURE;
315330

316-
const now = Temporal.Now.zonedDateTimeISO(
317-
filterValueDependencies.timeZone,
318-
);
331+
if (operandIsTakingNowAsReference) {
332+
const now = Temporal.Now.zonedDateTimeISO(
333+
filterValueDependencies.timeZone,
334+
);
319335

320-
switch (recordFilter.operand) {
321-
case RecordFilterOperand.IS_AFTER: {
322-
return {
323-
[correspondingFieldMetadataItem.name]: {
324-
gte: resolvedDateTime.toString(),
325-
} as DateTimeFilter,
326-
};
336+
switch (recordFilter.operand) {
337+
case RecordFilterOperand.IS_IN_PAST:
338+
return {
339+
[correspondingFieldMetadataItem.name]: {
340+
lt: now.toInstant().round('minute').toString(),
341+
} as DateTimeFilter,
342+
};
343+
case RecordFilterOperand.IS_IN_FUTURE:
344+
return {
345+
[correspondingFieldMetadataItem.name]: {
346+
gt: now.toInstant().round('minute').toString(),
347+
} as DateTimeFilter,
348+
};
349+
case RecordFilterOperand.IS_TODAY: {
350+
return {
351+
and: [
352+
{
353+
[correspondingFieldMetadataItem.name]: {
354+
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
355+
} as DateTimeFilter,
356+
},
357+
{
358+
[correspondingFieldMetadataItem.name]: {
359+
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
360+
} as DateTimeFilter,
361+
},
362+
],
363+
};
364+
}
327365
}
328-
case RecordFilterOperand.IS_BEFORE: {
329-
return {
330-
[correspondingFieldMetadataItem.name]: {
331-
lt: resolvedDateTime.toString(),
332-
} as DateTimeFilter,
333-
};
366+
} else {
367+
if (!isNonEmptyString(recordFilter.value)) {
368+
throw new Error(`Date filter is empty`);
334369
}
335-
case RecordFilterOperand.IS: {
336-
const start = resolvedDateTime
337-
.toZonedDateTimeISO('UTC')
338-
.with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
339-
.toInstant();
340370

341-
const end = start.add({ minutes: 1 });
371+
const resolvedDateTime = Temporal.Instant.from(recordFilter.value);
342372

343-
return {
344-
and: [
345-
{
346-
[correspondingFieldMetadataItem.name]: {
347-
lt: end.toString(),
348-
} as DateTimeFilter,
349-
},
350-
{
351-
[correspondingFieldMetadataItem.name]: {
352-
gte: start.toString(),
353-
} as DateTimeFilter,
354-
},
355-
],
356-
};
357-
}
358-
case RecordFilterOperand.IS_IN_PAST:
359-
return {
360-
[correspondingFieldMetadataItem.name]: {
361-
lt: now.toInstant().round('minute').toString(),
362-
} as DateTimeFilter,
363-
};
364-
case RecordFilterOperand.IS_IN_FUTURE:
365-
return {
366-
[correspondingFieldMetadataItem.name]: {
367-
gt: now.toInstant().round('minute').toString(),
368-
} as DateTimeFilter,
369-
};
370-
case RecordFilterOperand.IS_TODAY: {
371-
return {
372-
and: [
373-
{
374-
[correspondingFieldMetadataItem.name]: {
375-
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
376-
} as DateTimeFilter,
377-
},
378-
{
379-
[correspondingFieldMetadataItem.name]: {
380-
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
381-
} as DateTimeFilter,
382-
},
383-
],
384-
};
373+
switch (recordFilter.operand) {
374+
case RecordFilterOperand.IS_AFTER: {
375+
return {
376+
[correspondingFieldMetadataItem.name]: {
377+
gte: resolvedDateTime.toString(),
378+
} as DateTimeFilter,
379+
};
380+
}
381+
case RecordFilterOperand.IS_BEFORE: {
382+
return {
383+
[correspondingFieldMetadataItem.name]: {
384+
lt: resolvedDateTime.toString(),
385+
} as DateTimeFilter,
386+
};
387+
}
388+
case RecordFilterOperand.IS: {
389+
const start = resolvedDateTime
390+
.toZonedDateTimeISO('UTC')
391+
.with({
392+
second: 0,
393+
millisecond: 0,
394+
microsecond: 0,
395+
nanosecond: 0,
396+
})
397+
.toInstant();
398+
399+
const end = start.add({ minutes: 1 });
400+
401+
return {
402+
and: [
403+
{
404+
[correspondingFieldMetadataItem.name]: {
405+
lt: end.toString(),
406+
} as DateTimeFilter,
407+
},
408+
{
409+
[correspondingFieldMetadataItem.name]: {
410+
gte: start.toString(),
411+
} as DateTimeFilter,
412+
},
413+
],
414+
};
415+
}
385416
}
386417
}
387418

0 commit comments

Comments
 (0)