Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libs/features/deploy/src/utils/deploy-metadata.utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { composeQuery, getField, Query } from '@jetstreamapp/soql-parser-js';
import { formatDate } from 'date-fns/format';
import { formatISO } from 'date-fns/formatISO';
import { isValid as isDateValid } from 'date-fns/isValid';
import { parseISO } from 'date-fns/parseISO';
import JSZip from 'jszip';
import localforage from 'localforage';
Expand Down Expand Up @@ -387,10 +388,16 @@ const dataTableDateFormatter = (dateOrDateTime: Maybe<Date | string>): ReactNode
let showWarning = false;
let value: string;
if (isDate(dateOrDateTime)) {
if (!isDateValid(dateOrDateTime)) {
return String(dateOrDateTime);
}
value = formatDate(dateOrDateTime, DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a);
showWarning = dateOrDateTime.getFullYear() <= 1970;
} else if (dateOrDateTime.length === 28) {
const date = parseISO(dateOrDateTime);
if (!isDateValid(date)) {
return dateOrDateTime;
}
value = formatDate(date, DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a);
showWarning = date.getFullYear() <= 1970;
} else {
Expand Down
16 changes: 11 additions & 5 deletions libs/shared/ui-core-shared/src/query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { groupByFlat } from '@jetstream/shared/utils';
import { ErrorResult, ExpressionType, Field, FieldType, Maybe, SalesforceRecord, SoqlQueryFormatOptions } from '@jetstream/types';
import { FieldSubquery, HavingClause, Query, WhereClause, composeQuery, getFlattenedFields } from '@jetstreamapp/soql-parser-js';
import { formatISO } from 'date-fns/formatISO';
import { isValid as isDateValid } from 'date-fns/isValid';
import { parseISO } from 'date-fns/parseISO';
import isNil from 'lodash/isNil';
import isNumber from 'lodash/isNumber';
Expand Down Expand Up @@ -55,11 +56,16 @@ export function transformEditForm(sobjectFields: Field[], record: SalesforceReco
// ensure value is not empty string, as that ends up being coerced to 0
record[fieldName] = null;
} else if (DATE_FIELD_TYPES.has(field.type) && isString(value) && value) {
if (field.type === 'date') {
record[fieldName] = formatISO(parseISO(value), { representation: 'date' });
} else {
// this converts to UTC
record[fieldName] = parseISO(value).toISOString();
// Only transform when the value is a parseable ISO date; otherwise leave it untouched so an
// unparseable value surfaces as a Salesforce validation error rather than throwing here.
const parsedValue = parseISO(value);
if (isDateValid(parsedValue)) {
if (field.type === 'date') {
record[fieldName] = formatISO(parsedValue, { representation: 'date' });
} else {
// this converts to UTC
record[fieldName] = parsedValue.toISOString();
}
}
} else if (TIME_FIELD_TYPES.has(field.type) && isString(value) && value) {
// Without Z, salesforce will modify the timezone
Expand Down
76 changes: 76 additions & 0 deletions libs/ui/src/lib/data-table/__tests__/data-table-formatters.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, expect, test } from 'vitest';
import { dataTableDateFormatter, dataTableTimeFormatter } from '../data-table-formatters';

describe('dataTableDateFormatter', () => {
test('returns null for empty values', () => {
expect(dataTableDateFormatter(null)).toBeNull();
expect(dataTableDateFormatter(undefined)).toBeNull();
expect(dataTableDateFormatter('')).toBeNull();
});

test('formats a valid ISO date (length 10)', () => {
expect(dataTableDateFormatter('2023-11-12')).toBe('2023-11-12');
});

test('formats a valid ISO datetime (length 28)', () => {
const value = '2023-11-12T15:30:45.000+0000';
expect(value.length).toBe(28);
// Exact time depends on the runner's timezone, so assert the formatted shape rather than the wall-clock time.
expect(dataTableDateFormatter(value)).toMatch(/^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2} (AM|PM)$/);
});

test('formats a valid Date instance', () => {
const result = dataTableDateFormatter(new Date(2023, 10, 12, 15, 30, 45));
expect(result).toBe('2023-11-12 3:30:45 PM');
});

// Regression: an invalid Date instance previously caused formatDate to throw "RangeError: Invalid time value".
// It should fall back to the raw value (coerced to a string) rather than throwing or hiding data.
test('returns the raw value for an invalid Date instance instead of throwing', () => {
const value = new Date('invalid');
expect(() => dataTableDateFormatter(value)).not.toThrow();
expect(dataTableDateFormatter(value)).toBe('Invalid Date');
});

// Regression: a 10-char MM/DD/YYYY value (e.g. from a formula field) is not ISO and previously
// caused parseISO -> Invalid Date -> formatDate to throw "RangeError: Invalid time value".
test('returns the raw value for a 10-char non-ISO date instead of throwing', () => {
expect(() => dataTableDateFormatter('12/11/2023')).not.toThrow();
expect(dataTableDateFormatter('12/11/2023')).toBe('12/11/2023');
expect(dataTableDateFormatter('10/15/2019')).toBe('10/15/2019');
});

test('returns the raw value for an unparseable 28-char value instead of throwing', () => {
const value = 'x'.repeat(28);
expect(() => dataTableDateFormatter(value)).not.toThrow();
expect(dataTableDateFormatter(value)).toBe(value);
});

test('returns other-length strings unchanged', () => {
expect(dataTableDateFormatter('some text')).toBe('some text');
});
});

describe('dataTableTimeFormatter', () => {
test('returns null for empty values', () => {
expect(dataTableTimeFormatter(null)).toBeNull();
expect(dataTableTimeFormatter(undefined)).toBeNull();
expect(dataTableTimeFormatter('')).toBeNull();
});

test('formats a valid 13-char Salesforce time value', () => {
expect(dataTableTimeFormatter('15:30:45.000Z')).toBe('3:30:45 PM');
});

// Regression: a 13-char value that does not match the expected time format should not throw.
test('returns the raw value for an unparseable 13-char time instead of throwing', () => {
const value = 'not-valid-tim';
expect(value.length).toBe(13);
expect(() => dataTableTimeFormatter(value)).not.toThrow();
expect(dataTableTimeFormatter(value)).toBe(value);
});

test('returns other-length strings unchanged', () => {
expect(dataTableTimeFormatter('15:30')).toBe('15:30');
});
});
16 changes: 12 additions & 4 deletions libs/ui/src/lib/data-table/data-table-formatters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tracker } from '@jetstream/shared/ui-utils';
import { getErrorMessage } from '@jetstream/shared/utils';
import { Maybe } from '@jetstream/types';
import { formatDate } from 'date-fns/format';
import { isValid as isDateValid } from 'date-fns/isValid';
import { parse as parseDate } from 'date-fns/parse';
import { parseISO } from 'date-fns/parseISO';
import { startOfDay } from 'date-fns/startOfDay';
Expand All @@ -18,11 +19,17 @@ export const dataTableDateFormatter = (dateOrDateTime: Maybe<Date | string>): st
if (!dateOrDateTime) {
return null;
} else if (isDate(dateOrDateTime)) {
return formatDate(dateOrDateTime, DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a);
// Guard against an invalid Date instance so formatDate never throws; fall back to the raw value (coerced to a
// string) instead of null to stay consistent with the string branches and avoid hiding data.
return isDateValid(dateOrDateTime) ? formatDate(dateOrDateTime, DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a) : String(dateOrDateTime);
} else if (dateOrDateTime.length === 28) {
return formatDate(parseISO(dateOrDateTime), DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a);
const parsedDateTime = parseISO(dateOrDateTime);
return isDateValid(parsedDateTime) ? formatDate(parsedDateTime, DATE_FORMATS.YYYY_MM_DD_HH_mm_ss_a) : dateOrDateTime;
} else if (dateOrDateTime.length === 10) {
return formatDate(startOfDay(parseISO(dateOrDateTime)), DATE_FORMATS.yyyy_MM_dd);
// A 10-char value is assumed to be an ISO date, but values like "12/11/2023" (MM/DD/YYYY, e.g. from a
// formula field) also match this length and are not parseable as ISO. Fall back to the raw value instead of throwing.
const parsedDate = parseISO(dateOrDateTime);
return isDateValid(parsedDate) ? formatDate(startOfDay(parsedDate), DATE_FORMATS.yyyy_MM_dd) : dateOrDateTime;
} else {
return dateOrDateTime;
}
Expand All @@ -42,7 +49,8 @@ export const dataTableTimeFormatter = (value: Maybe<string>): string | null => {
if (!time) {
return null;
} else if (time.length === 13) {
return formatDate(parseDate(time, DATE_FORMATS.HH_mm_ss_ssss_z, new Date()), DATE_FORMATS.HH_MM_SS_a);
const parsedTime = parseDate(time, DATE_FORMATS.HH_mm_ss_ssss_z, new Date());
return isDateValid(parsedTime) ? formatDate(parsedTime, DATE_FORMATS.HH_MM_SS_a) : time;
} else {
return time;
}
Expand Down
Loading