Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 21 additions & 68 deletions frontend/__tests__/components/Vuetify.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,84 +127,38 @@ describe('components', () => {
})

describe('v-data-table-virtual', () => {
// These tests basically test that nothing changes in the way vuetify renders the virtual table
// as the way it behaves in case no item-height is defined is kind of undeterministic
const TestTableRow = {
name: 'TestTableRow',
props: {
item: {
type: Object,
required: true,
},
},
template: '<tr><td>{{ item.name }}</td></tr>',
}

function mountDataTableVirtual ({ itemHeight, itemsCount = 50 } = {}) {
const items = Array.from({ length: itemsCount }).map((_, i) => ({
id: i,
name: `Item ${i}`,
}))
const columns = [
{ key: 'name', title: 'Name' },
]

it('should be able to find v-table__wrapper element', () => {
const Component = {
name: 'TestVirtualTable',
components: {
TestTableRow,
},
template: `
<v-app>
<v-main>
<v-data-table-virtual
:items="items"
:columns="columns"
:item-height="itemHeight"
:height="400"
>
<template #item="{ item }">
<TestTableRow :item="item" />
</template>
</v-data-table-virtual>
</v-main>
</v-app>
`,
data () {
return {
items,
columns,
itemHeight,
}
},
template: '<v-data-table-virtual />',
}

return mount(Component, {
const wrapper = mount(Component, {
global: {
plugins: [
createVuetifyPlugin(),
],
},
})
}

it('should render default number of rows if item-height is not defined', async () => {
const wrapper = mountDataTableVirtual({ itemsCount: 50 })
await nextTick()

const rows = wrapper.findAllComponents(TestTableRow)
expect(rows).toHaveLength(25) // by default, the table expects 16px tall rows
const footer = wrapper.find('.v-table__wrapper')
expect(footer.exists()).toBe(true)
})
})

it('should only render the visible rows if item-height is defined', async () => {
const wrapper = mountDataTableVirtual({
itemsCount: 50,
itemHeight: 40,
describe('v-data-table', () => {
it('should be able to find v-data-table-footer classes', () => {
const Component = {
template: '<v-data-table />',
}
const wrapper = mount(Component, {
global: {
plugins: [
createVuetifyPlugin(),
],
},
})
await nextTick()

const rows = wrapper.findAllComponents(TestTableRow)
expect(rows).toHaveLength(10) // 400px / 40px = 10 rows
const footer = wrapper.find('.v-data-table-footer')
expect(footer.exists()).toBe(true)
const footerInfo = wrapper.find('.v-data-table-footer__info')
expect(footerInfo.exists()).toBe(true)
})
})

Expand Down Expand Up @@ -240,7 +194,6 @@ describe('components', () => {
})
const iconItem = wrapper.find('.v-icon')
expect(iconItem.exists()).toBe(true)
expect(iconItem.classes()).toContain('v-icon')
})
})
})
107 changes: 6 additions & 101 deletions frontend/src/components/GDataTableFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,122 +7,27 @@ SPDX-License-Identifier: Apache-2.0
<template>
<v-divider />
<div class="v-data-table-footer">
<template v-if="pageCount">
<div class="v-data-table-footer__items-per-page">
<span>Rows per page:</span>
<v-select
:items="itemsPerPageOptions"
:model-value="itemsPerPage"
density="compact"
variant="solo"
flat
single-line
hide-details
width="105px"
@update:model-value="value => setItemsPerPage(Number(value))"
/>
</div>
<div class="v-data-table-footer__info">
{{ !itemsLength ? 0 : startIndex + 1 }}-{{ stopIndex }} of {{ itemsLength }}
</div>
<div class="v-data-table-footer__pagination">
<v-btn
icon="mdi-page-first"
variant="plain"
:disabled="page === 1"
aria-label="First page"
@click="setPage(1)"
/>
<v-btn
icon="mdi-chevron-left"
variant="plain"
:disabled="page === 1"
aria-label="Previous page"
@click="setPage(Math.max(1, page - 1))"
/>
<v-btn
icon="mdi-chevron-right"
variant="plain"
:disabled="page === pageCount"
aria-label="Next page"
@click="setPage(Math.min(pageCount, page + 1))"
/>
<v-btn
icon="mdi-page-last"
variant="plain"
:disabled="page === pageCount"
aria-label="Last page"
@click="setPage(pageCount)"
/>
</div>
</template>
<div
v-else
class="v-data-table-footer__info"
>
{{ itemsLength }} Rows
{{ itemsLength }} {{ itemsLabel }}
</div>
</div>
</template>

<script setup>
import {
computed,
toRefs,
} from 'vue'
import { toRefs } from 'vue'

const props = defineProps({
itemsLength: {
type: Number,
required: true,
},
itemsPerPage: {
type: Number,
required: false,
},
itemsPerPageOptions: {
type: Array,
default: () => ([
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' },
{ value: 100, title: '100' },
]),
},
page: {
type: Number,
required: false,
},
pageCount: {
type: Number,
required: false,
itemsLabel: {
type: String,
default: 'Rows',
},
})

const { page, pageCount, itemsLength, itemsPerPage, itemsPerPageOptions } = toRefs(props)

const startIndex = computed(() => {
if (itemsPerPage.value === -1) return 0

return itemsPerPage.value * (page.value - 1)
})
const stopIndex = computed(() => {
if (itemsPerPage.value === -1) return itemsLength.value

return Math.min(itemsLength.value, startIndex.value + itemsPerPage.value)
})

const emit = defineEmits([
'update:page',
'update:itemsPerPage',
])

function setItemsPerPage (value) {
emit('update:itemsPerPage', value)
}

function setPage (value) {
emit('update:page', value)
}

const { itemsLength, itemsLabel } = toRefs(props)
</script>
66 changes: 66 additions & 0 deletions frontend/src/components/GScrollContainer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!--
SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors

SPDX-License-Identifier: Apache-2.0
-->

<template>
<div
ref="scrollRef"
class="scrollable-container"
:style="{ '--fadeOpacity': fadeOpacity, '--fadeHeight': `${fadeHeight}px` }"
>
<slot />
</div>
</template>

<script setup>
import {
ref,
computed,
} from 'vue'
import {
useScroll,
useElementSize,
} from '@vueuse/core'

const scrollRef = ref(null)
const { y } = useScroll(scrollRef)
const { height } = useElementSize(scrollRef)

const fadeHeight = Math.max(40, height.value / 2)

const fadeOpacity = computed(() => {
const el = scrollRef.value
if (!el) {
return false
}

const remainingY = el.scrollHeight - (y.value + height.value)
if (remainingY >= fadeHeight) {
return 1
}

return remainingY / fadeHeight
})

</script>

<style scoped>
.scrollable-container {
overflow-y: auto;
}

.scrollable-container::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: var(--fadeHeight);
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(var(--v-theme-surface)));
pointer-events: none;
opacity: var(--fadeOpacity);
}

</style>
Loading