diff --git a/src/components/KTable/KTable.cy.ts b/src/components/KTable/KTable.cy.ts index 1d727740e1..59995ea97a 100644 --- a/src/components/KTable/KTable.cy.ts +++ b/src/components/KTable/KTable.cy.ts @@ -4,6 +4,15 @@ import KTable from '@/components/KTable/KTable.vue' import { offsetPaginationHeaders, offsetPaginationFetcher } from '../../../mocks/KTableMockData' import type { TableHeader } from '@/types' +interface FetchParams { + pageSize: number + page: number + query?: string + sortColumnKey?: string + sortColumnOrder?: 'asc' | 'desc' + offset?: string | null +} + const largeDataSet = [ { name: 'Basic Auth', @@ -666,6 +675,124 @@ describe('KTable', () => { cy.getTestId('table-pagination').should('be.visible') }) + + it('refetch with paginationOffset: true', () => { + const data: Array<{ name: string }> = [] + for (let i = 0; i < 12; i++) { + data.push({ name: 'row' + i }) + } + const fns = { + fetcher: (params: FetchParams) => { + const { pageSize, page, offset } = params + const start = offset ? Number(offset) : 0 + return { + data: data.slice(start, start + pageSize), + pagination: { + offset: `${start + pageSize}`, + page, + }, + } + }, + } + cy.spy(fns, 'fetcher').as('fetcher') + + mount(KTable, { + propsData: { + fetcher: fns.fetcher, + initialFetcherParams: { pageSize: 10 }, + loading: false, + headers: options.headers, + paginationPageSizes: [10], + paginationOffset: true, + hidePaginationWhenOptional: true, + fetcherCacheKey: '0', + }, + }) + + // page 1 + cy.getTestId('table-pagination').should('be.visible') + cy.get('.table tbody').find('tr').should('have.length', 10) + cy.get('.table tbody').should('contain.text', 'row0') + cy.get('@fetcher') + .should('have.callCount', 1) // ensure fetcher is NOT called twice on load + .should('have.been.calledWith', { pageSize: 10, page: 1, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + .then(() => cy.wrap(Cypress.vueWrapper.setProps({ fetcherCacheKey: '1' }))) // manually trigger refetch + .get('@fetcher') + .should('have.callCount', 2) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 1, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + + // page 2 + cy.getTestId('next-button').click() + cy.get('.table tbody').find('tr').should('have.length', 2) + cy.get('.table tbody').should('contain.text', 'row10') + cy.get('@fetcher') + .should('have.callCount', 3) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 2, offset: '10', query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + .then(() => cy.wrap(Cypress.vueWrapper.setProps({ fetcherCacheKey: '2' }))) // manually trigger refetch + .get('@fetcher') + .should('have.callCount', 4) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 2, offset: '10', query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + }) + + it('refetch with paginationOffset: false', () => { + const data: Array<{ name: string }> = [] + for (let i = 0; i < 12; i++) { + data.push({ name: 'row' + i }) + } + const fns = { + fetcher: (params: FetchParams) => { + const { pageSize, page } = params + return { + data: data.slice((page - 1) * pageSize, page * pageSize), + total: data.length, + } + }, + } + cy.spy(fns, 'fetcher').as('fetcher') + + mount(KTable, { + propsData: { + fetcher: fns.fetcher, + initialFetcherParams: { pageSize: 10 }, + loading: false, + headers: options.headers, + paginationPageSizes: [10], + paginationOffset: false, + hidePaginationWhenOptional: true, + fetcherCacheKey: '0', + }, + }) + + // page 1 + cy.getTestId('table-pagination').should('be.visible') + cy.get('.table tbody').find('tr').should('have.length', 10) + cy.get('.table tbody').should('contain.text', 'row0') + cy.get('@fetcher') + .should('have.callCount', 1) // ensure fetcher is NOT called twice on load + .should('have.been.calledWith', { pageSize: 10, page: 1, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + .then(() => cy.wrap(Cypress.vueWrapper.setProps({ fetcherCacheKey: '1' }))) // manually trigger refetch + .get('@fetcher') + .should('have.callCount', 2) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 1, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + + // page 2 + cy.getTestId('next-button').click() + cy.get('.table tbody').find('tr').should('have.length', 2) + cy.get('.table tbody').should('contain.text', 'row10') + cy.get('@fetcher') + .should('have.callCount', 3) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 2, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + .then(() => cy.wrap(Cypress.vueWrapper.setProps({ fetcherCacheKey: '2' }))) // manually trigger refetch + .get('@fetcher') + .should('have.callCount', 4) + .its('lastCall') + .should('have.been.calledWith', { pageSize: 10, page: 2, offset: null, query: '', sortColumnKey: '', sortColumnOrder: 'desc' }) + }) }) describe('misc', () => { diff --git a/src/components/KTable/KTable.vue b/src/components/KTable/KTable.vue index 3cc2f8febf..3a4ce96280 100644 --- a/src/components/KTable/KTable.vue +++ b/src/components/KTable/KTable.vue @@ -230,7 +230,7 @@ :initial-page-size="pageSize" :neighbors="paginationNeighbors" :offset="paginationOffset" - :offset-next-button-disabled="!offset || !hasNextPage" + :offset-next-button-disabled="!nextOffset || !hasNextPage" :offset-previous-button-disabled="!previousOffset" :page-sizes="paginationPageSizes" :total-count="total" @@ -552,7 +552,6 @@ const offsets: Ref> = ref([]) const hasNextPage = ref(true) const isClickable = ref(false) const hasInitialized = ref(false) -const nextPageClicked = ref(false) const hasToolbarSlot = computed((): boolean => !!slots.toolbar || hasColumnVisibilityMenu.value) const tableWrapperStyles = computed((): Record => ({ maxHeight: getSizeFromString(props.maxHeight), @@ -820,15 +819,9 @@ const fetchData = async () => { if (props.paginationOffset) { if (!res.pagination?.offset) { - offset.value = null - - // reset to first page if no pagiantion data is returned unless the "next page" button was clicked - // this will ensure buttons display the correct state for cases like search - if (!nextPageClicked.value) { - page.value = 1 - } + nextOffset.value = null } else { - offset.value = res.pagination.offset + nextOffset.value = res.pagination.offset if (!offsets.value[page.value]) { offsets.value.push(res.pagination.offset) @@ -838,7 +831,15 @@ const fetchData = async () => { hasNextPage.value = (res.pagination && 'hasNextPage' in res.pagination) ? res.pagination.hasNextPage : true } - nextPageClicked.value = false + // if the data is empty and the page is greater than 1, + // e.g. user deletes the last item on the last page, + // reset the page to 1 + if (data.value.length === 0 && page.value > 1) { + page.value = 1 + offsets.value = [null] + offset.value = null + } + isInitialFetch.value = false return res @@ -882,6 +883,7 @@ watch(() => props.headers, (newVal: TableHeader[]) => { }, { deep: true }) const previousOffset = computed((): string | null => offsets.value[page.value - 1]) +const nextOffset = ref(null) // once `initData()` finishes, setting tableFetcherCacheKey to non-falsey value triggers fetch of data const tableFetcherCacheKey = computed((): string => { @@ -1010,7 +1012,7 @@ const emitTablePreferences = (): void => { const getNextOffsetHandler = (): void => { page.value++ - nextPageClicked.value = true + offset.value = nextOffset.value } const getPrevOffsetHandler = (): void => { @@ -1026,7 +1028,7 @@ const getPrevOffsetHandler = (): void => { const shouldShowPagination = computed((): boolean => { return !!(props.fetcher && !props.disablePagination && !(!props.paginationOffset && props.hidePaginationWhenOptional && total.value <= props.paginationPageSizes[0]) && - !(props.paginationOffset && props.hidePaginationWhenOptional && !previousOffset.value && !offset.value && data.value.length < props.paginationPageSizes[0])) + !(props.paginationOffset && props.hidePaginationWhenOptional && !previousOffset.value && !nextOffset.value && data.value.length < props.paginationPageSizes[0])) }) const getTestIdString = (message: string): string => {