diff --git a/src/SearchFilter/SearchFilter.stories.tsx b/src/SearchFilter/SearchFilter.stories.tsx
index 7f43fe5..1b4b115 100644
--- a/src/SearchFilter/SearchFilter.stories.tsx
+++ b/src/SearchFilter/SearchFilter.stories.tsx
@@ -624,3 +624,44 @@ export const CustomRangeDebug: Story = {
},
render: (args) => ,
}
+
+const lazyPaletteValues = [
+ { id: 'red', label: 'Red', color: '#F44336', icon: 'circle' },
+ { id: 'green', label: 'Green', color: '#4CAF50', icon: 'circle' },
+ { id: 'blue', label: 'Blue', color: '#2196F3', icon: 'circle' },
+]
+
+export const LazyOptions: Story = {
+ args: {},
+ render: (args) => {
+ const [filters, setFilters] = useState([])
+
+ const lazyOptions: Option[] = [
+ ...options,
+ {
+ id: 'palette',
+ label: 'Palette (loads on open)',
+ icon: 'palette',
+ operator: 'OR',
+ allowExcludes: true,
+ allowHasValue: true,
+ allowNoValue: true,
+ values: [],
+ loadValues: () =>
+ new Promise((resolve) => setTimeout(() => resolve(lazyPaletteValues), 1500)),
+ },
+ {
+ id: 'broken',
+ label: 'Broken (fails to load)',
+ icon: 'error',
+ values: [],
+ loadValues: () =>
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Enum resolver timed out')), 1500),
+ ),
+ },
+ ]
+
+ return
+ },
+}
diff --git a/src/SearchFilter/SearchFilter.tsx b/src/SearchFilter/SearchFilter.tsx
index 208b2d9..027c1bc 100644
--- a/src/SearchFilter/SearchFilter.tsx
+++ b/src/SearchFilter/SearchFilter.tsx
@@ -16,6 +16,14 @@ import doesFilterExist from './doesFilterExist'
import { Icon, IconType } from '../Icon'
import clsx from 'clsx'
import { SEARCH_FILTER_ID } from './constants'
+import {
+ LazyValuesState,
+ getCurrentValues,
+ getDisplayValues,
+ getLoadErrorMessage,
+ mergeLoadedValues,
+ withLazyValues,
+} from './lazyValues'
const sortSelectedToTopFields = ['assignee', 'taskType']
@@ -73,7 +81,7 @@ export const SearchFilter = forwardRef(
filters = [],
onChange,
onFinish,
- options = [],
+ options: rawOptions = [],
groupOptions = [],
quickActions,
onQuickAction,
@@ -109,8 +117,55 @@ export const SearchFilter = forwardRef(
const { enableMultiple: enableGlobalSearchMultiple } = globalSearchConfig || {}
+ const [lazyValues, setLazyValues] = useState>({})
+
+ const options = useMemo(() => withLazyValues(rawOptions, lazyValues), [rawOptions, lazyValues])
+
+ // Caching is left to the consumer (e.g. RTK Query), so every call hits loadValues.
+ // Previously loaded values stay visible while reloading, unless loadValuesKey changed.
+ const loadOptionValues = (option?: Option) => {
+ if (!option?.loadValues) return
+ const { id, loadValues, loadValuesKey: key } = option
+ // only the latest load of an option may write its result (e.g. after a project switch)
+ const request = {}
+ const isLatest = (current: Record) =>
+ current[id]?.request === request
+
+ setLazyValues((current) => ({
+ ...current,
+ [id]: { status: 'loading', values: getCurrentValues(current[id], key), key, request },
+ }))
+ // started inside the chain so a synchronous throw also ends in the error state
+ Promise.resolve()
+ .then(loadValues)
+ .then((values) =>
+ setLazyValues((current) =>
+ isLatest(current)
+ ? { ...current, [id]: { status: 'loaded', values, key, request } }
+ : current,
+ ),
+ )
+ .catch((error: unknown) =>
+ setLazyValues((current) =>
+ isLatest(current)
+ ? {
+ ...current,
+ [id]: {
+ status: 'error',
+ values: current[id]?.values || [],
+ error: getLoadErrorMessage(error),
+ key,
+ request,
+ },
+ }
+ : current,
+ ),
+ )
+ }
+
const [dropdownParentId, setDropdownParentId] = useState(null)
- const [dropdownOptions, setOptions] = useState