Skip to content

Commit d9b678b

Browse files
Merge pull request #1010 from frappe/backend-pagination
feat: query pagination
2 parents 41d390d + 390bcf7 commit d9b678b

File tree

17 files changed

+496
-211
lines changed

17 files changed

+496
-211
lines changed

frontend/.prettierrc.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"semi": false,
33
"singleQuote": true,
44
"useTabs": true,
5+
"tabWidth": 4,
56
"printWidth": 100,
6-
"vueIndentScriptAndStyle": false
7-
}
7+
"vueIndentScriptAndStyle": false,
8+
"trailingComma": "all"
9+
}

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"eslint-plugin-vue": "^9.1.1",
5252
"patch-package": "^8.0.1",
5353
"postcss": "^8.4.14",
54-
"prettier": "2.8.4",
54+
"prettier": "3.1.0",
5555
"sass": "^1.54.9",
5656
"tailwindcss": "3.4.17",
5757
"typescript": "^5.5.2",

frontend/src2/charts/chart.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,23 @@ function makeChart(name: string) {
7272
addFilterOperation(query)
7373
addChartOperation(query)
7474
addOrderByOperation(query)
75-
addLimitOperation(query)
7675

76+
const currentLimit = chart.doc.config.limit || 100
7777
const shouldExecute =
7878
force ||
7979
reload ||
8080
!dataQuery.value.result.executedSQL ||
8181
dataQuery.value.adhocFilters ||
82-
JSON.stringify(query.doc.operations) !== JSON.stringify(dataQuery.value.doc.operations)
82+
JSON.stringify(query.doc.operations) !== JSON.stringify(dataQuery.value.doc.operations) ||
83+
currentLimit !== dataQuery.value.pageSize
8384

8485
if (!shouldExecute) {
8586
return
8687
}
8788

8889
dataQuery.value.setOperations(copy(query.doc.operations))
8990
dataQuery.value.doc.use_live_connection = query.doc.use_live_connection
90-
return dataQuery.value.execute(force)
91+
return dataQuery.value.execute(force, chart.doc.config.limit)
9192
}
9293

9394
function validateConfig() {
@@ -362,12 +363,6 @@ function makeChart(name: string) {
362363
})
363364
}
364365

365-
function addLimitOperation(query: Query) {
366-
if (chart.doc.config.limit) {
367-
query.addLimit(chart.doc.config.limit)
368-
}
369-
}
370-
371366
function updateGranularity(column_name: string, granularity: GranularityType) {
372367
if ('x_axis' in chart.doc.config) {
373368
if (chart.doc.config.x_axis?.dimension?.dimension_name === column_name) {

frontend/src2/charts/components/ChartRenderer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ const showExpandedChartDialog = ref(false)
197197
:result="result"
198198
@drill-down="onNumberChartDrillDown"
199199
/>
200-
<TableChart v-else-if="!loading && chart_type == 'Table'" :chart="props.chart" />
200+
<TableChart v-else-if="chart_type == 'Table'" :chart="props.chart" />
201201

202202
<div v-else class="flex h-full flex-1 flex-col items-center justify-center rounded border">
203203
<template v-if="loading">

frontend/src2/components/DataTable.vue

Lines changed: 73 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
2-
import { Button, FormControl, LoadingIndicator } from 'frappe-ui'
3-
import { ChevronLeft, ChevronRight, Plus, Search, Table2Icon } from 'lucide-vue-next'
4-
import { computed, nextTick, reactive, ref } from 'vue'
2+
import { watchDebounced } from '@vueuse/core'
3+
import { Button, LoadingIndicator } from 'frappe-ui'
4+
import { Plus, Search, Table2Icon } from 'lucide-vue-next'
5+
import { computed, nextTick, ref } from 'vue'
6+
import { usePagination } from '../composables/usePagination'
57
import { createHeaders, formatNumber, getShortNumber } from '../helpers'
68
import { FIELDTYPES } from '../helpers/constants'
79
import {
@@ -18,8 +20,11 @@ import {
1820
rank_rules,
1921
text_rules,
2022
} from '../query/components/formatting_utils'
23+
import { matchesFilter, parseFilterString } from '../query/helpers'
2124
import { QueryResultColumn, QueryResultRow, SortDirection, SortOrder } from '../types/query.types'
2225
import DataTableColumn from './DataTableColumn.vue'
26+
import DataTableFooter from './DataTableFooter.vue'
27+
import LazyTextInput from './LazyTextInput.vue'
2328
2429
const props = defineProps<{
2530
columns: QueryResultColumn[] | undefined
@@ -33,6 +38,7 @@ const props = defineProps<{
3338
replaceNullsWithZeros?: boolean
3439
compactNumbers?: boolean
3540
loading?: boolean
41+
filtering?: boolean
3642
onExport?: Function
3743
downloading?: boolean
3844
formatGroup?: FormatGroupArgs
@@ -43,6 +49,12 @@ const props = defineProps<{
4349
stickyColumns?: string[]
4450
columnWidths?: Record<string, number>
4551
textWrap?: Record<string, boolean>
52+
pageSize?: number
53+
totalRowCount?: number
54+
onPageChange?: (page: number) => void
55+
currentPage?: number
56+
onFetchCount?: () => Promise<void> | void
57+
onFilterChange?: (filters: Record<string, string>) => void
4658
}>()
4759
4860
const headers = computed(() => {
@@ -145,40 +157,26 @@ const visibleRows = computed(() => {
145157
const rows = props.rows
146158
if (!columns?.length || !rows?.length || !props.showFilterRow) return rows
147159
160+
if (props.onFilterChange) return rows
161+
148162
const filters = filterPerColumn.value
149163
return rows.filter((row) => {
150-
return Object.entries(filters).every(([col, filter]) => {
151-
if (!filter) return true
152-
const isNumber = isNumberColumn(col)
153-
const value = row[col]
154-
return applyFilter(value, isNumber, filter)
164+
return Object.entries(filters).every(([col, filterStr]) => {
165+
if (!filterStr) return true
166+
const parsed = parseFilterString(filterStr)
167+
if (!parsed) return true
168+
return matchesFilter(row[col], parsed)
155169
})
156170
})
157171
})
158172
159-
function applyFilter(value: any, isNumber: boolean, filter: string) {
160-
if (isNumber) {
161-
const operator = ['>', '<', '>=', '<=', '=', '!='].find((op) => filter.startsWith(op))
162-
if (operator) {
163-
const num = Number(filter.replace(operator, ''))
164-
switch (operator) {
165-
case '>':
166-
return Number(value) > num
167-
case '<':
168-
return Number(value) < num
169-
case '>=':
170-
return Number(value) >= num
171-
case '<=':
172-
return Number(value) <= num
173-
case '=':
174-
return Number(value) === num
175-
case '!=':
176-
return Number(value) !== num
177-
}
178-
}
179-
}
180-
return String(value).toLowerCase().includes(filter.toLowerCase())
181-
}
173+
watchDebounced(
174+
filterPerColumn,
175+
(filters) => {
176+
props.onFilterChange?.(filters)
177+
},
178+
{ deep: true, debounce: 300 },
179+
)
182180
183181
const totalPerColumn = computed(() => {
184182
const columns = props.columns
@@ -216,32 +214,14 @@ const totalColumnTotal = computed(() => {
216214
return Object.values(totalPerColumn.value).reduce((acc, val) => acc + val, 0)
217215
})
218216
219-
const page = reactive({
220-
current: 1,
221-
size: 100,
222-
total: 1,
223-
startIndex: 0,
224-
endIndex: 99,
225-
next() {
226-
if (page.current < page.total) {
227-
page.current++
228-
}
229-
},
230-
prev() {
231-
if (page.current > 1) {
232-
page.current--
233-
}
234-
},
235-
})
236-
// @ts-ignore
237-
page.total = computed(() => {
238-
if (!visibleRows.value?.length) return 1
239-
return Math.ceil(visibleRows.value.length / page.size)
217+
const pagination = usePagination({
218+
pageSize: computed(() => props.pageSize ?? 100),
219+
rowCount: computed(() => visibleRows.value?.length ?? 0),
220+
totalRowCount: computed(() => props.totalRowCount),
221+
currentPage: computed(() => props.currentPage),
222+
onPageChange: props.onPageChange,
223+
enabled: computed(() => Boolean(props.enablePagination)),
240224
})
241-
// @ts-ignore
242-
page.startIndex = computed(() => (page.current - 1) * page.size)
243-
// @ts-ignore
244-
page.endIndex = computed(() => Math.min(page.current * page.size, visibleRows.value?.length || 0))
245225
246226
const colorByPercentage = {
247227
0: 'bg-white text-gray-900',
@@ -661,16 +641,23 @@ function toggleNewColumn() {
661641
...getColumnWidthStyle(column.name),
662642
}"
663643
>
664-
<FormControl
665-
type="text"
666-
v-model="filterPerColumn[column.name]"
667-
autocomplete="off"
644+
<LazyTextInput
645+
:model-value="filterPerColumn[column.name]"
646+
@update:model-value="
647+
(value) => (filterPerColumn[column.name] = value)
648+
"
668649
class="[&_input]:h-6 [&_input]:bg-gray-200/80"
669650
>
670651
<template #prefix>
671-
<Search class="h-4 w-4 text-gray-500" stroke-width="1.5" />
652+
<Search class="size-3.5 text-gray-500" :stroke-width="1.5" />
653+
</template>
654+
<template #suffix>
655+
<LoadingIndicator
656+
v-if="props.loading || props.filtering"
657+
class="size-3.5 text-gray-500"
658+
/>
672659
</template>
673-
</FormControl>
660+
</LazyTextInput>
674661
</td>
675662
<td
676663
v-if="props.showRowTotals"
@@ -681,17 +668,24 @@ function toggleNewColumn() {
681668
</td>
682669
</tr>
683670
</thead>
684-
<tbody>
671+
<tbody
672+
:class="
673+
props.filtering ? 'opacity-60 transition-opacity' : 'transition-opacity'
674+
"
675+
>
685676
<tr
686-
v-for="(row, idx) in visibleRows?.slice(page.startIndex, page.endIndex)"
677+
v-for="(row, idx) in visibleRows?.slice(
678+
pagination.startIndex.value,
679+
pagination.endIndex.value,
680+
)"
687681
:key="idx"
688682
>
689683
<td
690684
class="tnum sticky left-0 h-8 whitespace-nowrap border-b border-r bg-white px-3 text-right text-xs"
691685
width="1px"
692686
height="30px"
693687
>
694-
{{ idx + page.startIndex + 1 }}
688+
{{ idx + pagination.rowDisplayOffset.value + 1 }}
695689
</td>
696690

697691
<td
@@ -777,45 +771,20 @@ function toggleNewColumn() {
777771
</table>
778772
</div>
779773
<slot name="footer">
780-
<div class="flex flex-shrink-0 items-center justify-between border-t px-2 py-1">
781-
<slot name="footer-left">
782-
<div></div>
783-
</slot>
784-
<slot name="footer-right">
785-
<div class="flex items-center gap-2">
786-
<div
787-
v-if="props.enablePagination && visibleRows?.length && page.total > 1"
788-
class="flex flex-shrink-0 items-center justify-end gap-2"
789-
>
790-
<p class="tnum text-sm text-gray-600">
791-
{{ page.startIndex + 1 }} - {{ page.endIndex }} of
792-
{{ visibleRows.length }}
793-
</p>
794-
795-
<div class="flex gap-2">
796-
<Button
797-
variant="ghost"
798-
@click="page.prev"
799-
:disabled="page.current === 1"
800-
>
801-
<ChevronLeft class="h-4 w-4 text-gray-700" stroke-width="1.5" />
802-
</Button>
803-
<Button
804-
variant="ghost"
805-
@click="page.next"
806-
:disabled="page.current === page.total"
807-
>
808-
<ChevronRight
809-
class="h-4 w-4 text-gray-700"
810-
stroke-width="1.5"
811-
/>
812-
</Button>
813-
</div>
814-
</div>
815-
<slot name="footer-right-actions"></slot>
816-
</div>
817-
</slot>
818-
</div>
774+
<DataTableFooter
775+
:pagination="props.enablePagination ? pagination : undefined"
776+
:total-row-count="props.totalRowCount"
777+
:on-fetch-count="props.onFetchCount"
778+
@prev="pagination.prev"
779+
@next="pagination.next"
780+
>
781+
<template #left>
782+
<slot name="footer-left" />
783+
</template>
784+
<template #actions>
785+
<slot name="footer-right-actions" />
786+
</template>
787+
</DataTableFooter>
819788
</slot>
820789
</div>
821790

@@ -825,11 +794,4 @@ function toggleNewColumn() {
825794
<p class="text-center text-gray-500">No data to display.</p>
826795
</div>
827796
</div>
828-
829-
<div
830-
v-if="props.loading"
831-
class="absolute top-10 flex h-[calc(100%-2rem)] w-full items-center justify-center rounded bg-white/30 backdrop-blur-sm"
832-
>
833-
<LoadingIndicator class="h-8 w-8 text-gray-700" />
834-
</div>
835797
</template>

0 commit comments

Comments
 (0)