Skip to content
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

Merged
merged 5 commits into from
Jan 8, 2025
Merged

Implement aggregate operations on dates #9444

merged 5 commits into from
Jan 8, 2025

Conversation

ijreilly
Copy link
Collaborator

@ijreilly ijreilly commented Jan 7, 2025

Adding aggregate operations for dates: min ("Earliest date") and max ("Latest date")

@ijreilly ijreilly marked this pull request as ready for review January 8, 2025 10:35
Copy link
Contributor

@greptile-apps greptile-apps bot left a 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

Comment on lines +12 to +13
expect(getAggregateOperationLabel('EARLIEST')).toBe('Earliest date');
expect(getAggregateOperationLabel('LATEST')).toBe('Latest date');
Copy link
Contributor

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.

Comment on lines +28 to +31
case 'EARLIEST':
return 'Earliest date';
case 'LATEST':
return 'Latest date';
Copy link
Contributor

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;
Copy link
Contributor

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

Comment on lines +90 to +96
value = formatDateString({
value,
displayAsRelativeDate,
timeZone,
dateFormat,
timeFormat,
});
Copy link
Contributor

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

Comment on lines +5 to +6
| 'EARLIEST'
| 'LATEST';
Copy link
Contributor

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

Comment on lines +54 to +58
jest.mock('@/localization/utils/formatDateISOStringToDateTime', () => ({
formatDateISOStringToDateTime: jest
.fn()
.mockReturnValue(mockFormattedDate),
}));
Copy link
Contributor

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.

Comment on lines +31 to +33
it('should format date as relative when displayAsRelativeDate is true', () => {
const mockDate = DateTime.now().minus({ months: 2 }).toISO();
const mockRelativeDate = '2 months ago';
Copy link
Contributor

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.

Comment on lines +19 to +23
const formattedDate = value
? displayAsRelativeDate
? formatDateISOStringToRelativeDate(value)
: formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
: '';
Copy link
Contributor

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

Comment on lines +13 to +17
timeZone: string;
dateFormat: DateFormat;
timeFormat: TimeFormat;
value?: string | null;
displayAsRelativeDate?: boolean;
Copy link
Contributor

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

Copy link
Member

@Weiko Weiko left a 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 = (
Copy link
Member

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?

Copy link
Member

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 🙂

Copy link
Member

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!

Copy link
Collaborator Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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]: [],
Copy link
Member

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;
Copy link
Member

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?

Copy link
Collaborator Author

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

@ijreilly ijreilly enabled auto-merge (squash) January 8, 2025 15:36
@ijreilly ijreilly merged commit 8475b55 into main Jan 8, 2025
23 checks passed
@ijreilly ijreilly deleted the aggregate-dates branch January 8, 2025 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants