Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions frontend/common/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { useState } from 'react'
import { useCallback, useEffect, useRef } from 'react'

export default function useDebounce(func: any, delay: number) {
const [timeout, saveTimeout] = useState<NodeJS.Timeout | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const funcRef = useRef(func)

const debouncedFunc = function () {
//eslint-disable-next-line
const args = arguments
if (timeout) {
clearTimeout(timeout)
}
// Keep the latest callback without re-creating the debounced function,
// so the caller doesn't have to memoise `func` themselves.
useEffect(() => {
funcRef.current = func
}, [func])

const newTimeout = setTimeout(function () {
func(...args)
if (newTimeout === timeout) {
saveTimeout(null)
}
}, delay)
useEffect(
() => () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
},
[],
)

saveTimeout(newTimeout)
}

return debouncedFunc as typeof func
return useCallback(
(...args: any[]) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
timeoutRef.current = setTimeout(() => {
funcRef.current(...args)
}, delay)
},
[delay],
) as typeof func
}
/* Usage example:
const searchItems = useDebounce((search:string) => {
Expand Down
3 changes: 3 additions & 0 deletions frontend/web/components/pages/features/FeaturesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const FeaturesPage: FC<FeaturesPageProps> = ({
handleFilterChange,
hasFilters,
page,
searchResetKey,
} = useFeatureFilters(history)

const {
Expand Down Expand Up @@ -208,6 +209,7 @@ const FeaturesPage: FC<FeaturesPageProps> = ({
onClearFilters={clearFilters}
viewMode={viewMode}
onViewModeChange={handleViewModeChange}
searchResetKey={searchResetKey}
/>
),
[
Expand All @@ -220,6 +222,7 @@ const FeaturesPage: FC<FeaturesPageProps> = ({
clearFilters,
viewMode,
handleViewModeChange,
searchResetKey,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type FeaturesTableFiltersProps = {
viewMode?: ViewMode
onViewModeChange?: (value: ViewMode) => void
excludeTag?: (tag: { type: string; is_permanent: boolean }) => boolean
searchResetKey?: number
}

export const FeaturesTableFilters: FC<FeaturesTableFiltersProps> = ({
Expand All @@ -55,6 +56,7 @@ export const FeaturesTableFilters: FC<FeaturesTableFiltersProps> = ({
onViewModeChange,
orgId,
projectId,
searchResetKey,
viewMode,
}) => {
const {
Expand Down Expand Up @@ -107,6 +109,7 @@ export const FeaturesTableFilters: FC<FeaturesTableFiltersProps> = ({
<Row className='table-header'>
<div className='table-column flex-row flex-fill'>
<TableSearchFilter
key={searchResetKey}
onChange={(v) => onFilterChange({ search: v || null })}
value={search}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function useFeatureFilters(history: History): {
filters: FilterState
page: number
hasFilters: boolean
searchResetKey: number
handleFilterChange: (updates: Partial<FilterState>) => void
clearFilters: () => void
goToPage: (newPage: number) => void
Expand All @@ -29,6 +30,10 @@ export function useFeatureFilters(history: History): {

const [filters, setFilters] = useState<FilterState>(initialFilters)
const [page, setPage] = useState<number>(initialFilters.page)
// Bumped whenever `filters.search` is reset externally (e.g. Clear Filters),
// so the search input can remount to the fresh value rather than syncing
// state on every keystroke echo.
const [searchResetKey, setSearchResetKey] = useState(0)

const updateURLParams = useCallback(() => {
const currentParams = Utils.fromParam()
Expand Down Expand Up @@ -59,6 +64,7 @@ export function useFeatureFilters(history: History): {
const newFilters = getFiltersFromParams({})
setFilters(newFilters)
setPage(1)
setSearchResetKey((k) => k + 1)
}, [history])

const goToPage = (newPage: number) => {
Expand All @@ -72,5 +78,6 @@ export function useFeatureFilters(history: History): {
handleFilterChange,
hasFilters: hasActiveFilters(filters),
page,
searchResetKey,
}
}
51 changes: 18 additions & 33 deletions frontend/web/components/tables/TableSearchFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useState } from 'react'
import React, { FC, useState } from 'react'
import Input from 'components/base/forms/Input'
import Utils from 'common/utils/utils'
import useDebounce from 'common/useDebounce'
Expand All @@ -9,41 +9,26 @@ type TableFilterType = {
onChange: (v: string) => void
}

const TableSearchFilter: FC<TableFilterType> = ({ exact, onChange, value }) => {
const [localValue, setLocalValue] = useState(value)
const searchItems = useDebounce(
useCallback((search: string) => {
if (value !== search) {
onChange(search)
}
//Adding onChange as a dependency here would make this prone to infinite recursion issues
//eslint-disable-next-line
}, []),
100,
const TableSearchFilter: FC<TableFilterType> = ({ onChange, value }) => {
const [localValue, setLocalValue] = useState(
(value || '').replace(/^"+|"+$/g, ''),
)
const debouncedOnChange = useDebounce((v: string) => onChange(v), 100)

useEffect(() => {
searchItems(localValue)
}, [localValue])

useEffect(() => {
setLocalValue(value || '')
}, [value])
return (
<>
<Input
onChange={(e: InputEvent) => {
const v = Utils.safeParseEventValue(e)
setLocalValue(v)
}}
value={localValue?.replace(/^"+|"+$/g, '')}
type='text'
className='me-3'
size='xSmall'
placeholder='Search'
search
/>
</>
<Input
onChange={(e: InputEvent) => {
const v = Utils.safeParseEventValue(e)
setLocalValue(v)
debouncedOnChange(v)
}}
value={localValue}
type='text'
className='me-3'
size='xSmall'
placeholder='Search'
search
/>
)
}

Expand Down
Loading