-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement aggregate operations on dates #9444
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This PR implements date aggregation operations (min/max) across the record board and table components, allowing users to find earliest and latest dates in datasets.
- Added new
ExtendedAggregateOperations
type with 'EARLIEST'/'LATEST' operations in/packages/twenty-front/src/modules/object-record/record-table/types/ExtendedAggregateOperations.ts
- Implemented date formatting with user preferences in
/packages/twenty-front/src/utils/string/formatDateString.ts
for consistent date display - Added DateTime field support for min/max operations in
/packages/twenty-front/src/modules/object-record/record-table/constants/FieldTypesAvailableForNonStandardAggregateOperation.ts
- Created conversion utilities between standard and extended operations in
/packages/twenty-front/src/modules/object-record/utils/convertAggregateOperationToExtendedAggregateOperation.ts
- Updated server-side descriptions from 'Oldest/Most recent' to 'Earliest/Latest' date in
/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts
28 file(s) reviewed, 17 comment(s)
Edit PR Review Bot Settings | Greptile
expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date'); | ||
expect(getAggregateOperationLabel('LATEST')).toBe('Latest date'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Testing new date aggregate operations 'EARLIEST' and 'LATEST' which are not defined in AGGREGATE_OPERATIONS enum. Consider adding these to the enum for consistency.
case 'EARLIEST': | ||
return 'Earliest date'; | ||
case 'LATEST': | ||
return 'Latest date'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Consider using AGGREGATE_OPERATIONS enum for EARLIEST/LATEST instead of string literals to maintain consistency with other cases
const { decimals, type } = field.settings ?? {}; | ||
value = | ||
type === 'percentage' | ||
? `${formatNumber(value * 100, decimals)}%` | ||
: formatNumber(value, decimals); | ||
break; | ||
} | ||
|
||
case FieldMetadataType.DateTime: { | ||
value = aggregateValue as string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: type assertion to string could be unsafe if aggregateValue is not actually a string
value = formatDateString({ | ||
value, | ||
displayAsRelativeDate, | ||
timeZone, | ||
dateFormat, | ||
timeFormat, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: formatDateString could throw if value is invalid - consider adding error handling
| 'EARLIEST' | ||
| 'LATEST'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: consider using an enum for these values to maintain consistency with AGGREGATE_OPERATIONS and prevent string literal typos
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({ | ||
formatDateISOStringToDateTime: jest | ||
.fn() | ||
.mockReturnValue(mockFormattedDate), | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Duplicate mock implementation across multiple tests. Consider using beforeEach() to set up mocks and afterEach() to clear them.
it('should format date as relative when displayAsRelativeDate is true', () => { | ||
const mockDate = DateTime.now().minus({ months: 2 }).toISO(); | ||
const mockRelativeDate = '2 months ago'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Using DateTime.now() in tests can make them non-deterministic. Use a fixed date instead.
const formattedDate = value | ||
? displayAsRelativeDate | ||
? formatDateISOStringToRelativeDate(value) | ||
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat) | ||
: ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: consider adding input validation for value format before passing to formatDateISOStringToDateTime/RelativeDate to prevent runtime errors with malformed dates
timeZone: string; | ||
dateFormat: DateFormat; | ||
timeFormat: TimeFormat; | ||
value?: string | null; | ||
displayAsRelativeDate?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: all parameters except value and displayAsRelativeDate are required but lack validation - consider adding runtime checks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
|
||
import { ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; | ||
|
||
export const convertExtendedAggregateOperationToAggregateOperation = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Extended" seems very broad. Looks like we are talking about "dates" here right? Why do we need to introduce this and not simply use min for dates? Is it for display?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what's the best strategy here actually, let's keep it like that until we have other things to convert, it's not clear to me 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI you can also use map for this kind of thing, might be easier to maintain?
const dateTimeOperationMap: Record<AGGREGATE_OPERATIONS, ExtendedAggregateOperations> = {
[AGGREGATE_OPERATIONS.min]: 'EARLIEST',
[AGGREGATE_OPERATIONS.max]: 'LATEST',
then in your code you can just use the map based on its field metadata type (via factories? But I think it's not a practice in the FE though)
Anyway, just FYI!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I need to be able to "convert" in both senses. I think there is no perfect solution for this, I will also need two maps, or to use the map using values as keys sometimes :/
if (acc[convertedAggregateOperation] === undefined) { | ||
acc[convertedAggregateOperation] = []; | ||
} | ||
acc[convertedAggregateOperation]?.push(field.id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
acc[convertedAggregateOperation]?.push(field.id); | |
acc[convertedAggregateOperation].push(field.id); |
looks like you don't need "?" anymore here
@@ -8,6 +9,7 @@ export const initializeAvailableFieldsForAggregateOperationMap = ( | |||
(acc, operation) => ({ | |||
...acc, | |||
[operation]: [], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we remove this line?
In practice it will try to do
{
min: [],
min: [],
max: [],
max: [],
earliest: [],
latest: [],
}
but js will actually create
{
min: [],
max: [],
earliest: [],
latest: [],
}
so it does not matter much but let's remove the line if it's not necessary!
gap: 4px; | ||
height: 32px; | ||
justify-content: flex-end; | ||
padding: 0 8px; | ||
overflow-x: auto; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: not really sure as I don't do much FE but it seems you are adding the same css to both styled components, do we use some kind of shared style components in the codebase for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not find any shared component but I extracted the style to use it for both components
Adding aggregate operations for dates: min ("Earliest date") and max ("Latest date")