From db5b4d9c6cdf8c066964cdc3efacf6ba00fc2c8e Mon Sep 17 00:00:00 2001 From: Hamza Faidi <66977086+hamzafa1d1@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:15:22 +0000 Subject: [PATCH] fix: replace unsafe JSON.parse casts with parseJson in filter dropdowns (#18513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Four filter dropdown components were calling `JSON.parse(filter.value) as string[]` to parse stored filter state. This throws a `SyntaxError` if the value is malformed (truncated URL, stale localStorage, migration artifact), crashing the entire dropdown with no recovery. ## Solution Replace with the existing `parseJson` utility from `twenty-shared`, which wraps `JSON.parse` in a try/catch and returns `null` on failure. The `?? []` fallback gracefully degrades to an empty selection instead of crashing. All four files had an explicit `// TODO: replace by a safe parse` marking this as a known issue. ## Testing No new tests — `parseJson` is already tested in `twenty-shared`. No new logic introduced. ## issue link #18514 --- .../components/ObjectFilterDropdownCountrySelect.tsx | 8 ++++++-- .../components/ObjectFilterDropdownCurrencySelect.tsx | 8 ++++++-- .../components/ObjectFilterDropdownOptionSelect.tsx | 10 ++++++---- .../components/ObjectFilterDropdownSourceSelect.tsx | 8 ++++++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx index 48d7811b34e..50f111c1373 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCountrySelect.tsx @@ -15,8 +15,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { type ChangeEvent, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, parseJson } from 'twenty-shared/utils'; import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation'; +import { z } from 'zod'; export const EMPTY_FILTER_VALUE = '[]'; export const MAX_ITEMS_TO_DISPLAY = 5; @@ -44,7 +45,10 @@ export const ObjectFilterDropdownCountrySelect = () => { const selectedCountryNames = isNonEmptyString( objectFilterDropdownCurrentRecordFilter?.value, ) - ? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse + ? (z + .array(z.string()) + .safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value)) + .data ?? []) : []; const filteredSelectableItems = countriesAsSelectableItems.filter( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx index a359fe41b3e..11d0647dc33 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownCurrencySelect.tsx @@ -14,8 +14,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { useLingui } from '@lingui/react/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { type ChangeEvent, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, parseJson } from 'twenty-shared/utils'; import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui/navigation'; +import { z } from 'zod'; export const EMPTY_FILTER_VALUE = '[]'; export const MAX_ITEMS_TO_DISPLAY = 3; @@ -41,7 +42,10 @@ export const ObjectFilterDropdownCurrencySelect = () => { const selectedCurrencies = isNonEmptyString( objectFilterDropdownCurrentRecordFilter?.value, ) - ? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse + ? (z + .array(z.string()) + .safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value)) + .data ?? []) : []; const filteredSelectableItems = currenciesAsSelectableItems.filter( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index 3e4a59e2d00..793f60b1871 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -22,8 +22,9 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, parseJson } from 'twenty-shared/utils'; import { MenuItem, MenuItemMultiSelect } from 'twenty-ui/navigation'; +import { z } from 'zod'; export const EMPTY_FILTER_VALUE = ''; @@ -58,9 +59,10 @@ export const ObjectFilterDropdownOptionSelect = ({ const selectedOptions = useMemo( () => isNonEmptyString(objectFilterDropdownCurrentRecordFilter?.value) - ? (JSON.parse( - objectFilterDropdownCurrentRecordFilter.value, - ) as string[]) // TODO: replace by a safe parse + ? (z + .array(z.string()) + .safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value)) + .data ?? []) : [], [objectFilterDropdownCurrentRecordFilter?.value], ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx index 11271dbf113..508db9d7b6d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -11,7 +11,8 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, parseJson } from 'twenty-shared/utils'; +import { z } from 'zod'; export const EMPTY_FILTER_VALUE = '[]'; export const MAX_ITEMS_TO_DISPLAY = 3; @@ -39,7 +40,10 @@ export const ObjectFilterDropdownSourceSelect = ({ const selectedSources = isNonEmptyString( objectFilterDropdownCurrentRecordFilter?.value, ) - ? (JSON.parse(objectFilterDropdownCurrentRecordFilter.value) as string[]) // TODO: replace by a safe parse + ? (z + .array(z.string()) + .safeParse(parseJson(objectFilterDropdownCurrentRecordFilter.value)) + .data ?? []) : []; const sourceTypes = getActorSourceMultiSelectOptions(selectedSources);