Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code b308817086 fix: convert ISO datetime to plain date string before passing to DatePicker
https://sonarly.com/issue/15175?type=bug

The DatePicker component crashes with a RangeError when editing DATE fields on custom objects because the raw value from the record store is a full ISO datetime string (e.g. `2026-06-17T22:00:00.000Z`) but `Temporal.PlainDate.from()` only accepts date-only strings.

Fix: Replaced `Temporal.PlainDate.from(string)` with the team's existing `parseToPlainDateOrThrow(string)` utility at three sites where raw record store values (full ISO datetime strings like `2026-06-17T22:00:00.000Z` from TypeORM) could be passed.

`parseToPlainDateOrThrow` (from `twenty-shared/utils`) first attempts `Temporal.Instant.from(string).toZonedDateTimeISO('UTC').toPlainDate()` (handles ISO datetime), then falls back to `Temporal.PlainDate.from(string)` (handles plain dates). This matches the existing pattern used in `buildFilterFromChartBucket.ts`.

**Files changed:**

1. **`DatePicker.tsx`** (line 359) — The primary crash site from Sentry. Added `parseToPlainDateOrThrow` import and replaced `Temporal.PlainDate.from(plainDateString)` with `parseToPlainDateOrThrow(plainDateString)`.

2. **`calendarDayRecordsComponentFamilySelector.ts`** (line 66) — Same bug in the calendar view's record-to-day matching logic for DATE fields. Added import and replaced the call.

3. **`DatePickerWithoutCalendar.tsx`** (line 326) — Defensive fix for the calendar top bar's date picker, which accepts `Nullable<string>` and could receive a datetime string. Added import and replaced the call.
2026-03-16 12:56:58 +00:00
Sonarly Claude Code 36934df3bc fix: handle missing morph relation field in timeline activity for custom objects
https://sonarly.com/issue/15168?type=bug

The timeline activity worker crashes when processing events for custom objects whose morph relation field is missing from the timelineActivity object metadata, causing timeline activity recording to fail silently for affected workspaces.

Fix: **Fix:** Added a metadata validation check in `TimelineActivityRepository.upsertTimelineActivities()` that verifies the morph relation field exists in the `timelineActivity` object metadata before constructing queries with it.

**What changed:**

1. **`timeline-activity.repository.ts`**: Added `hasTimelineActivityMorphRelationField()` method that loads the workspace's flat entity maps, finds the `timelineActivity` object metadata, and checks whether the expected join column name exists in the field maps. If the field is missing, `upsertTimelineActivities()` logs a warning and returns early instead of letting `formatData` crash.

2. **`object-metadata-repository.module.ts`**: Updated the factory to inject `WorkspaceManyOrAllFlatEntityMapsCacheService` into repositories created through this module, so `TimelineActivityRepository` can access workspace metadata for validation.

**Why this approach:**
- Fixes the crash at the right layer (timeline repository) rather than suppressing errors in the shared `formatData` utility
- Preserves the strict validation in `formatData` (which catches other metadata inconsistencies)
- Logs a clear warning with actionable guidance (run workspace:sync-metadata)
- Matches the team's pattern of using `WorkspaceManyOrAllFlatEntityMapsCacheService` for metadata lookups (already used in `TimelineActivityService`)
2026-03-16 12:51:23 +00:00
3 changed files with 14 additions and 5 deletions
@@ -9,7 +9,11 @@ import { createAtomComponentFamilySelector } from '@/ui/utilities/state/jotai/ut
import { isNonEmptyString } from '@sniptt/guards';
import { Temporal } from 'temporal-polyfill';
import { isDefined, isSamePlainDate } from 'twenty-shared/utils';
import {
isDefined,
isSamePlainDate,
parseToPlainDateOrThrow,
} from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const calendarDayRecordIdsComponentFamilySelector =
@@ -63,7 +67,7 @@ export const calendarDayRecordIdsComponentFamilySelector =
const recordDateAsPlainDateInTimeZone =
fieldMetadataItem.type === FieldMetadataType.DATE
? Temporal.PlainDate.from(recordDate)
? parseToPlainDateOrThrow(recordDate)
: Temporal.Instant.from(recordDate)
.toZonedDateTimeISO(timeZone)
.toPlainDate();
@@ -21,6 +21,7 @@ import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import {
isDefined,
parseToPlainDateOrThrow,
turnJSDateToPlainDate,
type RelativeDateFilter,
} from 'twenty-shared/utils';
@@ -356,7 +357,7 @@ export const DatePicker = ({
}: DatePickerProps) => {
const { theme } = useContext(ThemeContext);
const plainDate = isDefined(plainDateString)
? Temporal.PlainDate.from(plainDateString)
? parseToPlainDateOrThrow(plainDateString)
: Temporal.Now.plainDateISO();
const { userTimezone } = useUserTimezone();
@@ -15,7 +15,11 @@ import 'react-datepicker/dist/react-datepicker.css';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Temporal } from 'temporal-polyfill';
import { type Nullable } from 'twenty-shared/types';
import { isDefined, turnJSDateToPlainDate } from 'twenty-shared/utils';
import {
isDefined,
parseToPlainDateOrThrow,
turnJSDateToPlainDate,
} from 'twenty-shared/utils';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
@@ -323,7 +327,7 @@ export const DatePickerWithoutCalendar = ({
onClose,
}: DatePickerWithoutCalendarProps) => {
const { theme } = useContext(ThemeContext);
const plainDate = isDefined(date) ? Temporal.PlainDate.from(date) : null;
const plainDate = isDefined(date) ? parseToPlainDateOrThrow(date) : null;
const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown();