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:
+6
-2
@@ -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(
|
||||
|
||||
+6
-2
@@ -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(
|
||||
|
||||
+6
-4
@@ -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],
|
||||
);
|
||||
|
||||
+6
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user