From 429d6f04807e244d636a5955fb552e63d4618c50 Mon Sep 17 00:00:00 2001 From: "chihao.ou" Date: Sat, 12 Sep 2026 13:44:33 +0800 Subject: [PATCH] fix(usage): keep export filters consistent across pages --- frontend/src/views/user/UsageView.vue | 5 +- .../views/user/__tests__/UsageView.spec.ts | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/user/UsageView.vue b/frontend/src/views/user/UsageView.vue index 7a645cfe7f..033064d109 100644 --- a/frontend/src/views/user/UsageView.vue +++ b/frontend/src/views/user/UsageView.vue @@ -641,9 +641,10 @@ const exportToCSV = async () => { try { const allLogs: UsageLog[] = [] const pageSize = 100 + const exportParams = buildUsageListParams(1, pageSize) const totalPages = Math.ceil(pagination.total / pageSize) for (let page = 1; page <= totalPages; page++) { - const response = await usageAPI.query(buildUsageListParams(page, pageSize)) + const response = await usageAPI.query({ ...exportParams, page }) allLogs.push(...response.items) } if (allLogs.length === 0) { @@ -696,7 +697,7 @@ const exportToCSV = async () => { const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url - link.download = `usage_${startDate.value}_to_${endDate.value}.csv` + link.download = `usage_${exportParams.start_date}_to_${exportParams.end_date}.csv` link.click() window.URL.revokeObjectURL(url) appStore.showSuccess(t('usage.exportSuccess')) diff --git a/frontend/src/views/user/__tests__/UsageView.spec.ts b/frontend/src/views/user/__tests__/UsageView.spec.ts index 44f62000d8..737d88aa70 100644 --- a/frontend/src/views/user/__tests__/UsageView.spec.ts +++ b/frontend/src/views/user/__tests__/UsageView.spec.ts @@ -3,6 +3,8 @@ import { flushPromises, mount } from '@vue/test-utils' import UsageView from '../UsageView.vue' import Select, { type SelectOption } from '@/components/common/Select.vue' +import DateRangePicker from '@/components/common/DateRangePicker.vue' +import UsageTable from '@/components/admin/usage/UsageTable.vue' const { query, @@ -413,6 +415,68 @@ describe('user UsageView', () => { clickSpy.mockRestore() }) + it('keeps the initial filters, sort, and filename while exporting multiple pages', async () => { + const pageResponse = { items: [usageLog], total: 101, pages: 2 } + query.mockResolvedValue(pageResponse) + const wrapper = mountUsageView() + await flushPromises() + + const datePicker = wrapper.findComponent(DateRangePicker) + datePicker.vm.$emit('change', { startDate: '2026-03-01', endDate: '2026-03-08', preset: null }) + await flushPromises() + + let resolveFirstPage!: (value: typeof pageResponse) => void + const firstPage = new Promise((resolve) => { resolveFirstPage = resolve }) + query.mockClear() + query.mockImplementation((params, options) => + !options && params.page === 1 ? firstPage : Promise.resolve(pageResponse) + ) + const originalCreateObjectURL = window.URL.createObjectURL + const originalRevokeObjectURL = window.URL.revokeObjectURL + window.URL.createObjectURL = vi.fn(() => 'blob:usage-export') + window.URL.revokeObjectURL = vi.fn() + let filename = '' + const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function () { + filename = this.download + }) + + try { + await wrapper.findAll('button').find((button) => button.text() === 'Export CSV')!.trigger('click') + const initialParams = { ...query.mock.calls[0][0] } + expect(initialParams).toMatchObject({ + page: 1, page_size: 100, start_date: '2026-03-01', end_date: '2026-03-08', + sort_by: 'created_at', sort_order: 'desc', + }) + + const keySelect = wrapper.findAllComponents(Select).find((select) => + select.props('options').some((option: SelectOption) => option.label === 'All API Keys') + )! + keySelect.vm.$emit('update:modelValue', 1) + keySelect.vm.$emit('change', 1) + datePicker.vm.$emit('change', { startDate: '2026-04-01', endDate: '2026-04-08', preset: null }) + wrapper.findComponent(UsageTable).vm.$emit('sort', 'actual_cost', 'asc') + await flushPromises() + expect(query).toHaveBeenCalledWith(expect.objectContaining({ + api_key_id: 1, start_date: '2026-04-01', end_date: '2026-04-08', + sort_by: 'actual_cost', sort_order: 'asc', + }), expect.anything()) + + resolveFirstPage(pageResponse) + await flushPromises() + + const exportCalls = query.mock.calls.filter((call) => call.length === 1) + expect.soft(exportCalls).toEqual([[initialParams], [{ ...initialParams, page: 2 }]]) + expect.soft(filename).toBe('usage_2026-03-01_to_2026-03-08.csv') + expect(showSuccess).toHaveBeenCalledWith('Export success') + expect(showError).not.toHaveBeenCalled() + } finally { + window.URL.createObjectURL = originalCreateObjectURL + window.URL.revokeObjectURL = originalRevokeObjectURL + clickSpy.mockRestore() + wrapper.unmount() + } + }) + it('exports historical image rows with image billing mode derived from image_count', async () => { query.mockResolvedValue({ items: [