fix: replace unsafe JSON.parse casts with parseJson in filter dropdowns (#18513)

## 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<string[]>` 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
This commit is contained in:
Hamza Faidi
2026-03-12 13:15:22 +01:00
committed by GitHub
parent 660536d6bb
commit db5b4d9c6c
4 changed files with 24 additions and 10 deletions
@@ -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(
@@ -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(
@@ -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],
);
@@ -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);