Add UTC timezone label to CRON trigger form (#14674)

- Added 'Cron will be triggered at UTC time' notice below trigger
interval dropdown
- Positioned correctly between dropdown and expression field to match
design
- Only shows when Custom CRON option is selected

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Omar Eltomy
2025-09-24 20:45:56 +03:00
committed by GitHub
parent 37ce5c48bb
commit 2af20a4cf0
36 changed files with 2093 additions and 121 deletions
@@ -0,0 +1,24 @@
import { formatTime } from '../formatTime';
describe('formatTime', () => {
it('should format 24-hour time', () => {
expect(formatTime('9', '30', true)).toBe('09:30');
expect(formatTime('14', '0', true)).toBe('14:00');
expect(formatTime('0', '0', true)).toBe('00:00');
expect(formatTime('23', '59', true)).toBe('23:59');
});
it('should format 12-hour time', () => {
expect(formatTime('9', '30', false)).toBe('9:30 AM');
expect(formatTime('14', '0', false)).toBe('2:00 PM');
expect(formatTime('0', '0', false)).toBe('12:00 AM');
expect(formatTime('12', '0', false)).toBe('12:00 PM');
expect(formatTime('23', '59', false)).toBe('11:59 PM');
});
it('should handle invalid inputs', () => {
expect(formatTime('invalid', '30', true)).toBe('');
expect(formatTime('9', 'invalid', true)).toBe('');
expect(formatTime('', '', true)).toBe('');
});
});
@@ -0,0 +1,14 @@
import { getOrdinalNumber } from '../getOrdinalNumber';
describe('getOrdinalNumber', () => {
it('should return correct ordinal numbers', () => {
expect(getOrdinalNumber(1)).toBe('1st');
expect(getOrdinalNumber(2)).toBe('2nd');
expect(getOrdinalNumber(3)).toBe('3rd');
expect(getOrdinalNumber(4)).toBe('4th');
expect(getOrdinalNumber(11)).toBe('11th');
expect(getOrdinalNumber(21)).toBe('21st');
expect(getOrdinalNumber(22)).toBe('22nd');
expect(getOrdinalNumber(23)).toBe('23rd');
});
});
@@ -0,0 +1,27 @@
import { isDefined } from 'twenty-shared/utils';
export const formatTime = (
hour: string,
minute: string,
use24HourFormat: boolean,
): string => {
if (!isDefined(hour) || !isDefined(minute)) {
return '';
}
const hourNum = parseInt(hour, 10);
const minuteNum = parseInt(minute, 10);
if (isNaN(hourNum) || isNaN(minuteNum)) {
return '';
}
if (use24HourFormat) {
return `${hourNum.toString().padStart(2, '0')}:${minuteNum.toString().padStart(2, '0')}`;
} else {
const period = hourNum >= 12 ? 'PM' : 'AM';
const displayHour =
hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
return `${displayHour}:${minuteNum.toString().padStart(2, '0')} ${period}`;
}
};
@@ -0,0 +1,31 @@
import { selectOrdinal } from '@lingui/core/macro';
export const getOrdinalNumber = (num: number): string => {
try {
return selectOrdinal(num, {
one: `${num}st`,
two: `${num}nd`,
few: `${num}rd`,
other: `${num}th`,
});
} catch {
// Fallback for test environment - basic English ordinals
const lastDigit = num % 10;
const lastTwoDigits = num % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 13) {
return `${num}th`;
}
switch (lastDigit) {
case 1:
return `${num}st`;
case 2:
return `${num}nd`;
case 3:
return `${num}rd`;
default:
return `${num}th`;
}
}
};
@@ -0,0 +1,16 @@
import { isListValue } from '../isListValue';
describe('isListValue', () => {
it('should detect list values', () => {
expect(isListValue('1,2,3')).toBe(true);
expect(isListValue('0,15,30,45')).toBe(true);
expect(isListValue('1,5')).toBe(true);
});
it('should reject non-list values', () => {
expect(isListValue('*')).toBe(false);
expect(isListValue('*/5')).toBe(false);
expect(isListValue('1-5')).toBe(false);
expect(isListValue('15')).toBe(false);
});
});
@@ -0,0 +1,21 @@
import { isNumericRange } from '../isNumericRange';
describe('isNumericRange', () => {
it('should detect numeric ranges', () => {
expect(isNumericRange('1-5')).toBe(true);
expect(isNumericRange('10-20')).toBe(true);
expect(isNumericRange('0-59')).toBe(true);
});
it('should detect single numbers', () => {
expect(isNumericRange('15')).toBe(true);
expect(isNumericRange('0')).toBe(true);
});
it('should reject non-numeric ranges', () => {
expect(isNumericRange('*')).toBe(false);
expect(isNumericRange('*/5')).toBe(false);
expect(isNumericRange('1,2,3')).toBe(false);
expect(isNumericRange('invalid')).toBe(false);
});
});
@@ -0,0 +1,16 @@
import { isStepValue } from '../isStepValue';
describe('isStepValue', () => {
it('should detect step values', () => {
expect(isStepValue('*/5')).toBe(true);
expect(isStepValue('1-10/2')).toBe(true);
expect(isStepValue('0-59/15')).toBe(true);
});
it('should reject non-step values', () => {
expect(isStepValue('*')).toBe(false);
expect(isStepValue('1-5')).toBe(false);
expect(isStepValue('1,2,3')).toBe(false);
expect(isStepValue('15')).toBe(false);
});
});
@@ -0,0 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
export const isListValue = (value: string): boolean => {
return isDefined(value) && value.includes(',');
};
@@ -0,0 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
export const isNumericRange = (value: string): boolean => {
return isDefined(value) && /^\d+(-\d+)?$/.test(value);
};
@@ -0,0 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
export const isStepValue = (value: string): boolean => {
return isDefined(value) && value.includes('/');
};