Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(*): update useDebounce to fix delays in KTable after clearing the search #1220

Merged
merged 6 commits into from Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/components/KCatalog/KCatalog.vue
Expand Up @@ -486,7 +486,9 @@ export default defineComponent({
hasInitialized.value = true
}

const { query, search } = useDebounce('', 350)
const query = ref('')
const { debouncedFn: debouncedSearch } = useDebounce((q: string) => { query.value = q })

const { revalidate } = useRequest(
() => catalogFetcherCacheKey.value,
() => fetchData(),
Expand All @@ -506,7 +508,7 @@ export default defineComponent({
}

watch(() => props.searchInput, (newValue: string) => {
search(newValue)
debouncedSearch(newValue)
}, { immediate: true })

watch(() => [query.value, page.value, pageSize.value], () => {
Expand Down
44 changes: 44 additions & 0 deletions src/components/KTable/KTable.cy.ts
Expand Up @@ -459,4 +459,48 @@ describe('KTable', () => {
cy.getTestId('k-table-pagination').should('be.visible')
})
})

describe('misc', () => {
it('triggers the internal search and revalidate after clearing the search input', () => {
const fns = {
fetcher: ({ query }: { query: string }) => {
return { data: [{ query }] }
},
}

cy.spy(fns, 'fetcher').as('fetcher')

mount(KTable, {
propsData: {
testMode: 'true',
fetcher: fns.fetcher,
isLoading: false,
initialFetcherParams: { offset: 'abc' },
headers: options.headers,
pageSize: 15,
paginationPageSizes: [10, 15, 20],
hidePaginationWhenOptional: true,
paginationType: 'offset',
searchInput: '',
},
})

// wait for initial fetcher call
cy.get('@fetcher', { timeout: 1000 })
.should('have.callCount', 1) // fetcher's 1st call
.should('returned', { data: [{ query: '' }] })
.then(() => cy.wrap(Cypress.vueWrapper.setProps({ searchInput: 'some-keyword' })))

// fetcher call should be delayed (> 350ms for search func + 500ms for revalidate func)
cy.get('@fetcher', { timeout: 1000 }) // fetcher's 2nd call
.should('have.callCount', 2) // fetcher should be called once
.should('returned', { data: [{ query: 'some-keyword' }] })
.then(() => cy.wrap(Cypress.vueWrapper.setProps({ searchInput: '' })))

// fetcher should be called immediately (< 350ms for search func)
cy.get('@fetcher', { timeout: 350 })
.should('have.callCount', 3) // fetcher's 3rd call
.should('returned', { data: [{ query: '' }] })
})
})
})
42 changes: 35 additions & 7 deletions src/components/KTable/KTable.vue
Expand Up @@ -736,12 +736,20 @@ export default defineComponent({

return `k-table_${Math.floor(Math.random() * 1000)}_${props.fetcherCacheKey}` as string
})
const { query, search } = useDebounce('', 350)
const { revalidate } = useRequest(

const query = ref('')
const { debouncedFn: debouncedSearch, generateDebouncedFn: generateDebouncedSearch } = useDebounce((q: string) => { query.value = q }, 350)
const search = generateDebouncedSearch(0) // generate a debounced function with zero delay (immediate)

const { revalidate: _revalidate } = useRequest(
() => tableFetcherCacheKey.value,
() => fetchData(),
{ revalidateOnFocus: false },
{ revalidateOnFocus: false, revalidateDebounce: 0 },
)

const { debouncedFn: debouncedRevalidate, generateDebouncedFn: generateDebouncedRevalidate } = useDebounce(_revalidate, 500)
const revalidate = generateDebouncedRevalidate(0) // generate a debounced function with zero delay (immediate)

const sortClickHandler = (header: TableHeader) => {
const { key, useSortHandlerFn } = header
const prevKey = sortColumnKey.value + '' // avoid pass by ref
Expand Down Expand Up @@ -779,7 +787,7 @@ export default defineComponent({
defaultSorter(key, prevKey, sortColumnOrder.value, data.value)
}
} else if (props.paginationType !== 'offset') {
revalidate()
debouncedRevalidate()
}

// Emit an event whenever one of the tablePreferences are updated
Expand Down Expand Up @@ -845,11 +853,31 @@ export default defineComponent({
}

watch(() => props.searchInput, (newValue) => {
search(newValue)
if (newValue === '') {
// Immediately triggers the search, ...
// 1) on the 1st time (input is empty)
// 2) after clearing the input
search(newValue)
} else {
// Triggers a debounced search
debouncedSearch(newValue)
}
}, { immediate: true })

watch(() => [query.value, page.value, pageSize.value], () => {
revalidate()
watch(query, (newQuery) => {
if (newQuery === '') {
// Immediately triggers the revalidate, ...
// 1) on the 1st time (query is empty)
// 2) after clearing the input (query becomes empty)
revalidate()
} else {
// Triggers a debounced revalidate
debouncedRevalidate()
}
}, { deep: true, immediate: true })

watch(() => [page.value, pageSize.value], () => {
debouncedRevalidate()
}, { deep: true, immediate: true })

onMounted(() => {
Expand Down
107 changes: 73 additions & 34 deletions src/composables/useUtilities.ts
Expand Up @@ -14,20 +14,31 @@ const swrvState = {
}

export default function useUtilities() {
const useRequest = <Data = unknown, Error = { message: string }> (key: IKey, fn?: fetcherFn<AxiosResponse<Data>>, config?: IConfig) => {
const useSwrvFn = typeof useSWRV === 'function'
? useSWRV
: () => ({
data: {},
error: null,
isValidating: false,
mutate: () => ({}),
})

const { data: response, error, isValidating, mutate: revalidate } = useSwrvFn<
AxiosResponse<Data>,
AxiosError<Error>
>(key, fn, { revalidateDebounce: 500, dedupingInterval: 100, ...config })
const useRequest = <Data = unknown, Error = { message: string }>(
key: IKey,
fn?: fetcherFn<AxiosResponse<Data>>,
config?: IConfig,
) => {
const useSwrvFn =
typeof useSWRV === 'function'
? useSWRV
: () => ({
data: {},
error: null,
isValidating: false,
mutate: () => ({}),
})

const {
data: response,
error,
isValidating,
mutate: revalidate,
} = useSwrvFn<AxiosResponse<Data>, AxiosError<Error>>(key, fn, {
revalidateDebounce: 500,
dedupingInterval: 100,
...config,
})

const data = computed(() => {
// @ts-ignore
Expand All @@ -43,30 +54,50 @@ export default function useUtilities() {
}
}

const useDebounce = (initialQuery: string, delay = 300) => {
/**
* useDebounce accepts a function and a default delay.
* It returns a debounced function with the default delay and a debounced function
* wrapper which can be used to generate a debounced function with any delay.
* @param fn the function to wrap
* @param defaultDelay the default delay in milliseconds to use
* @returns a debounced function with default delay and a debounced function generator
*/
const useDebounce = <F extends (...args: any[]) => any>(
fn: F,
defaultDelay = 300,
): {
debouncedFn: (...args: Parameters<F>) => void
generateDebouncedFn: (delay: number) => (...args: Parameters<F>) => void
} => {
let timeout: any
const query = ref(initialQuery)

function search(q: string) {
clearTimeout(timeout)
timeout = setTimeout(() => {
query.value = q
}, delay)
}
const wrapDebouncedWithDelay =
(delay: number) =>
(...args: Parameters<F>) => {
clearTimeout(timeout)
if (delay > 0) {
timeout = setTimeout(() => {
fn(...args)
}, delay)
} else {
timeout = undefined
fn(...args)
}
}

return {
query,
search,
debouncedFn: wrapDebouncedWithDelay(defaultDelay),
generateDebouncedFn: wrapDebouncedWithDelay,
}
}

/**
* @param {String} key - the current key to sort by
* @param {String} previousKey - the previous key used to sort by
* @param {String} sortOrder - either ascending or descending
* @param {Array} items - the list of items to sort
* @return {Object} an object containing the previousKey and sortOrder
*/
* @param {String} key - the current key to sort by
* @param {String} previousKey - the previous key used to sort by
* @param {String} sortOrder - either ascending or descending
* @param {Array} items - the list of items to sort
* @return {Object} an object containing the previousKey and sortOrder
*/
const clientSideSorter = (key: string, previousKey: string, sortOrder: string, items: []) => {
let comparator = null

Expand Down Expand Up @@ -130,9 +161,11 @@ export default function useUtilities() {

watchEffect(() => {
const hasData =
response.value?.data?.length ||
response.value?.data?.data?.length ||
(!response.value?.data?.data && typeof response.value?.data === 'object' && Object.keys(response.value?.data).length)
response.value?.data?.length ||
response.value?.data?.data?.length ||
(!response.value?.data?.data &&
typeof response.value?.data === 'object' &&
Object.keys(response.value?.data).length)

if (response.value && hasData && isValidating.value) {
state.value = swrvState.VALIDATING_HAS_DATA
Expand Down Expand Up @@ -187,7 +220,13 @@ export default function useUtilities() {
* @returns A string to be used for the height of an element.
*/
const getSizeFromString = (sizeStr: string): string => {
return sizeStr === 'auto' || sizeStr.endsWith('%') || sizeStr.endsWith('vw') || sizeStr.endsWith('vh') || sizeStr.endsWith('px') ? sizeStr : sizeStr + 'px'
return sizeStr === 'auto' ||
sizeStr.endsWith('%') ||
sizeStr.endsWith('vw') ||
sizeStr.endsWith('vh') ||
sizeStr.endsWith('px')
? sizeStr
: sizeStr + 'px'
}

/**
Expand Down