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'
57import { createHeaders , formatNumber , getShortNumber } from ' ../helpers'
68import { FIELDTYPES } from ' ../helpers/constants'
79import {
@@ -18,8 +20,11 @@ import {
1820 rank_rules ,
1921 text_rules ,
2022} from ' ../query/components/formatting_utils'
23+ import { matchesFilter , parseFilterString } from ' ../query/helpers'
2124import { QueryResultColumn , QueryResultRow , SortDirection , SortOrder } from ' ../types/query.types'
2225import DataTableColumn from ' ./DataTableColumn.vue'
26+ import DataTableFooter from ' ./DataTableFooter.vue'
27+ import LazyTextInput from ' ./LazyTextInput.vue'
2328
2429const 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
4860const 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
183181const 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
246226const 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