8000 feat: split pivoted columns into multiple header rows · frappe/insights@6b5d711 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6b5d711

Browse files
feat: split pivoted columns into multiple header rows
1 parent ea285c3 commit 6b5d711

File tree

6 files changed

+238
-30
lines changed

6 files changed

+238
-30
lines changed

frontend/src2/charts/components/TableChart.vue

+19-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { TableChartConfig } from '../../types/chart.types'
55
import { QueryResult } from '../../types/query.types'
66
import { WorkbookChart } from '../../types/workbook.types'
77
import { Chart } from '../chart'
8-
import ChartBuilderTableColumn from './ChartBuilderTableColumn.vue'
98
import ChartTitle from './ChartTitle.vue'
9+
import { column } from '../../query/helpers'
1010
1111
const props = defineProps<{
1212
title: string
@@ -16,23 +16,36 @@ const props = defineProps<{
1616
1717
const chart = inject<Chart>('chart')!
1818
const tableConfig = computed(() => props.config as TableChartConfig)
19+
20+
const sortOrder = computed(() => {
21+
const order_by = props.config.order_by
22+
const sort_order: Record<string, 'asc' | 'desc'> = {}
23+
order_by?.forEach((order) => {
24+
sort_order[order.column.column_name] = order.direction
25+
})
26+
return sort_order
27+
})
28+
function onSort(sort_order: Record<string, 'asc' | 'desc'>) {
29+
props.config.order_by = Object.keys(sort_order).map((column_name) => ({
30+
column: column(column_name),
31+
direction: sort_order[column_name],
32+
}))
33+
}
1934
</script>
2035

2136
<template>
22-
<div class="flex h-full w-full flex-col overflow-hidden rounded bg-white shadow">
37+
<div class="flex h-full w-full flex-col divide-y overflow-hidden rounded bg-white shadow">
2338
<ChartTitle v-if="props.title" :title="props.title" />
2439
<DataTable
25-
class="w-full flex-1 overflow-hidden border-t"
2640
:columns="props.result.columns"
2741
:rows="props.result.formattedRows"
2842
:show-filter-row="tableConfig.show_filter_row"
2943
:show-column-totals="tableConfig.show_column_totals"
3044
:show-row-totals="tableConfig.show_row_totals"
3145
:on-export="chart ? chart.dataQuery.downloadResults : undefined"
46+
:sort-order="sortOrder"
47+
@sort="onSort"
3248
>
33-
<template #column-header="{ column }">
34-
<ChartBuilderTableColumn :config="props.config" :column="column" />
35-
</template>
3649
</DataTable>
3750
</div>
3851
</template>

frontend/src2/components/DataTable.vue

+58-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script setup lang="ts">
22
import { Download, Search, Table2Icon } from 'lucide-vue-next'
33
import { computed, ref } from 'vue'
4-
import { formatNumber } from '../helpers'
4+
import { createHeaders, formatNumber } from '../helpers'
55
import { FIELDTYPES } from '../helpers/constants'
66
import { QueryResultColumn, QueryResultRow } from '../types/query.types'
7+
import DataTableColumn from './DataTableColumn.vue'
78
89
const emit = defineEmits({
10+
sort: (sort_order: Record<string, 'asc' | 'desc'>) => true,
911
'cell-dbl-click': (row: QueryResultRow, column: QueryResultColumn) => true,
1012
})
1113
const props = defineProps<{
@@ -16,10 +18,17 @@ const props = defineProps<{
1618
showFilterRow?: boolean
1719
loading?: boolean
1820
onExport?: Function
21+
sortOrder?: Record<string, 'asc' | 'desc'>
1922
}>()
2023
21-
const isNumberColumn = (col: QueryResultColumn) => FIELDTYPES.NUMBER.includes(col.type)
24+
const headers = computed(() => {
25+
if (!props.columns?.length) return []
26+
return createHeaders(props.columns)
27+
})
28+
29+
const isNumberColumn = (col: QueryResultColumn): boolean => FIELDTYPES.NUMBER.includes(col.type)
2230
31+
const filterPerColumn = ref<Record<string, string>>({})
2332
const visibleRows = computed(() => {
2433
const columns = props.columns
2534
const rows = props.rows
@@ -96,33 +105,61 @@ const totalColumnTotal = computed(() => {
96105
return Object.values(totalPerColumn.value).reduce((acc, val) => acc + val, 0)
97106
})
98107
99-
const filterPerColumn = ref<Record<string, string>>({})
108+
const sortOrder = ref<Record<string, 'asc' | 'desc'>>({ ...props.sortOrder })
109+
function getSortOrder(column: QueryResultColumn) {
110+
return sortOrder.value[column.name]
111+
}
112+
function sortBy(column: QueryResultColumn, direction: 'asc' | 'desc' | '') {
113+
if (!direction) {
114+
delete sortOrder.value[column.name]
115+
} else {
116+
sortOrder.value[column.name] = direction
117+
}
118+
emit('sort', sortOrder.value)
119+
}
100120
</script>
101121

102122
<template>
103123
<div
104124
v-if="columns?.length || rows?.length"
105-
class="flex h-full w-full flex-col overflow-hidden font-mono text-sm"
125+
class="flex h-full w-full flex-col overflow-hidden text-sm"
106126
>
107127
<div class="w-full flex-1 overflow-y-auto">
108128
<table class="relative h-full w-full border-separate border-spacing-0">
109129
<thead class="sticky top-0 z-10 bg-white">
110-
<tr>
130+
<tr v-for="headerRow in headers">
111131
<td
112132
class="sticky left-0 z-10 whitespace-nowrap border-b border-r bg-white"
113133
width="1%"
114134
></td>
115135
<td
116-
v-for="(column, idx) in props.columns"
136+
v-for="(header, idx) in headerRow"
117137
:key="idx"
118138
class="border-b border-r"
119-
:class="isNumberColumn(column) ? 'text-right' : 'text-left'"
139+
:class="[
140+
header.isLast && isNumberColumn(header.column)
141+
? 'text-right'
142+
: 'text-left',
143+
]"
144+
:colspan="header.colspan"
120145
>
121-
<slot name="column-header" :column="column">
122-
<div class="truncate py-2 px-3">
123-
{{ column.name }}
124-
</div>
146+
<slot
147+
v-if="header.isLast"
148+
name="column-header"
149+
:column="header.column"
150+
:label="header.label"
151+
>
152+
<DataTableColumn
153+
:column="header.column"
154+
:label="header.label"
155+
:sort-order="getSortOrder(header.column)"
156+
@sort-change="sortBy(header.column, $event)"
157+
/>
125158
</slot>
159+
160+
<div v-else class="flex h-7 items-center truncate px-3">
161+
{{ header.label }}
162+
</div>
126163
</td>
127164

128165
<td
@@ -133,6 +170,7 @@ const filterPerColumn = ref<Record<string, string>>({})
133170
<div class="truncate pl-3 pr-20"></div>
134171
</td>
135172
</tr>
173+
136174
<tr v-if="props.showFilterRow">
137175
<td
138176
class="sticky left-0 z-10 whitespace-nowrap border-b border-r bg-white"
@@ -141,13 +179,13 @@ const filterPerColumn = ref<Record<string, string>>({})
141179
<td
142180
v-for="(column, idx) in props.columns"
143181
:key="idx"
144-
class="border-b border-r p-0.5"
182+
class="border-b border-r p-1"
145183
>
146184
<FormControl
147185
type="text"
148186
v-model="filterPerColumn[column.name]"
149187
autocomplete="off"
150-
class="[&_input]:bg-gray-200/80"
188+
class="[&_input]:h-6 [&_input]:bg-gray-200/80"
151189
>
152190
<template #prefix>
153191
<Search class="h-4 w-4 text-gray-500" stroke-width="1.5" />
@@ -235,17 +273,17 @@ const filterPerColumn = ref<Record<string, string>>({})
235273
</slot>
236274
</div>
237275

238-
<div
239-
v-else-if="props.loading"
240-
class="absolute top-10 z-10 flex h-[calc(100%-2rem)] w-full items-center justify-center rounded bg-white/30 backdrop-blur-sm"
241-
>
242-
<LoadingIndicator class="h-8 w-8 text-gray-700" />
243-
</div>
244-
245276
<div v-else class="flex h-full w-full items-center justify-center">
246277
<div class="flex flex-col items-center gap-2">
247278
<Table2Icon class="h-16 w-16 text-gray-300" stroke-width="1.5" />
248279
<p class="text-center text-gray-500">No data to display.</p>
249280
</div>
250281
</div>
282+
283+
<div
284+
v-if="props.loading"
285+
class="absolute top-10 z-10 flex h-[calc(100%-2rem)] w-full items-center justify-center rounded bg-white/30 backdrop-blur-sm"
286+
>
287+
<LoadingIndicator class="h-8 w-8 text-gray-700" />
288+
</div>
251289
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, XIcon } from 'lucide-vue-next'
3+
import { h } from 'vue'
4+
import { QueryResultColumn } from '../types/query.types'
5+
6+
const props = defineProps<{
7+
label: string
8+
column: QueryResultColumn
9+
sortOrder?: 'asc' | 'desc'
10+
onSortChange?: (sort_order: 'asc' | 'desc' | '') => void
11+
}>()
12+
13+
const sortOptions = [
14+
{
15+
label: 'Sort Ascending',
16+
icon: h(ArrowUpNarrowWide, { class: 'h-4 w-4 text-gray-700', strokeWidth: 1.5 }),
17+
onClick: () => props.onSortChange?.('asc'),
18+
},
19+
{
20+
label: 'Sort Descending',
21+
icon: h(ArrowDownWideNarrow, { class: 'h-4 w-4 text-gray-700', strokeWidth: 1.5 }),
22+
onClick: () => props.onSortChange?.('desc'),
23+
},
24+
{
25+
label: 'Remove Sort',
26+
icon: h(XIcon, { class: 'h-4 w-4 text-gray-700', strokeWidth: 1.5 }),
27+
onClick: () => props.onSortChange?.(''),
28+
},
29+
]
30+
</script>
31+
32+
<template>
33+
<div class="flex items-center gap-3 pl-3">
34+
<span class="truncate">
35+
{{ props.label }}
36+
</span>
37+
38+
<div class="flex">
39+
<!-- Sort -->
40+
<Dropdown :options="sortOptions">
41+
<Button variant="ghost" class="rounded-none">
42+
<template #icon>
43+
<component
44+
:is="
45+
!props.sortOrder
46+
? ArrowUpDown
47+
: props.sortOrder === 'asc'
48+
? ArrowUpNarrowWide
49+
: ArrowDownWideNarrow
50+
"
51+
class="h-3.5 w-3.5 text-gray-700"
52+
stroke-width="1.5"
53+
/>
54+
</template>
55+
</Button>
56+
</Dropdown>
57+
</div>
58+
</div>
59+
</template>

frontend/src2/dashboard/DashboardItem.vue

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ChartRenderer from '../charts/components/ChartRenderer.vue'
55
import { WorkbookDashboardChart, WorkbookDashboardItem } from '../types/workbook.types'
66
import { Dashboard } from './dashboard'
77
import DashboardItemActions from './DashboardItemActions.vue'
8+
import { watchDebounced } from '@vueuse/core'
89
910
const props = defineProps<{
1011
index: number
@@ -19,6 +20,15 @@ const chart = computed(() => {
1920
return getCachedChart(item.chart) as Chart
2021
})
2122
23+
watchDebounced(
24+
() => chart.value?.doc.config.order_by,
25+
() => chart.value?.refresh(),
26+
{
27+
deep: true,
28+
debounce: 500,
29+
}
30+
)
31+
2232
let timer: any
2333
const wasDragging = ref(false)
2434
const showPopover = ref(false)

0 commit comments

Comments
 (0)
0