Files
windmill/frontend/src/lib/components/table/DataTable.svelte
T
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* Remove $$props.field usage

* Rename slots to ensure no hyphen

* _props

* _trigger

* OnSelectedIteration type correct capitalization

* rename _content

* Remove afterUpdate

* Migrate everything to svelte 5

* array bind

* Fix popover

* type never

* nit fixes

* Fixed many trivial errors

* onClick

* Fix errors

* use let:

* nit typing

* fix: wrap state_referenced_locally vars with untrack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add untrack import

* Fix all syntax errors due to untrack migration

* Fix undefined errors

* Fix more undefined errors

* untrack(() => initialOpen)

* svelte-ignore

* Fix state_descriptors_fixed error in Chart.svelte

Use $state.snapshot() to pass plain copies of data/options to Chart.js
instead of $state proxies. Chart.js's listenArrayEvents tries to define
property descriptors on data arrays, which Svelte 5 proxies reject.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit typing

* Merge issue

* Fix "path is not set" error in resource picker / editor

* Fix InputTransformForm error when rerunning some flows

* fix npm run check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:11:40 +01:00

214 lines
5.2 KiB
Svelte

<script module lang="ts">
export type DatatableContext = {
size: 'xs' | 'sm' | 'md' | 'lg'
}
</script>
<script lang="ts">
import { createEventDispatcher, setContext, untrack } from 'svelte'
import Button from '../common/button/Button.svelte'
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, Loader2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import List from '$lib/components/common/layout/List.svelte'
let footerHeight: number = $state(0)
let tableHeight: number = $state(0)
const dispatch = createEventDispatcher()
let tableContainer: HTMLDivElement | undefined = $state()
interface Props {
paginated?: boolean
currentPage?: number
showNext?: boolean
showPrev?: boolean
loadMore?: number
shouldLoadMore?: boolean
rounded?: boolean
size?: 'xs' | 'sm' | 'md' | 'lg'
perPage?: number | undefined
shouldHidePagination?: boolean
noBorder?: boolean
rowCount?: number | undefined
hasMore?: boolean
contentHeight?: number
tableFixed?: boolean
infiniteScroll?: boolean | undefined
neverShowLoader?: boolean
loading?: boolean
loadingMore?: boolean
containerClass?: string
preventXOverflow?: boolean
children?: import('svelte').Snippet
emptyMessage?: import('svelte').Snippet
}
let {
paginated = false,
currentPage = $bindable(1),
showNext = true,
showPrev = true,
loadMore = 0,
shouldLoadMore = false,
rounded = true,
size = 'md',
perPage = $bindable(undefined),
shouldHidePagination = false,
noBorder = false,
rowCount = undefined,
hasMore = true,
contentHeight = $bindable(0),
tableFixed = false,
infiniteScroll = undefined,
neverShowLoader = false,
loading = false,
loadingMore = false,
containerClass = '',
children,
emptyMessage,
preventXOverflow = false
}: Props = $props()
setContext<DatatableContext>('datatable', {
size: untrack(() => size)
})
$effect(() => {
contentHeight = tableHeight - footerHeight
})
function checkScrollStatus() {
if (!infiniteScroll || loading || !tableContainer) return
const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight
if (!hasScrollbar && hasMore) {
triggerLoadMore()
}
}
function handleScroll() {
if (!infiniteScroll || loading) {
if (loading) {
const checkAgain = () => {
if (!loading) {
handleScroll()
}
}
setTimeout(checkAgain, 200)
}
return
}
if (!tableContainer) return
const { scrollTop, scrollHeight, clientHeight } = tableContainer
if (scrollHeight - (scrollTop + clientHeight) === 0) {
triggerLoadMore()
}
}
let lastLoadMoreDispatch: number | undefined = $state(undefined)
function triggerLoadMore() {
if (lastLoadMoreDispatch && Date.now() - lastLoadMoreDispatch < 1000) {
return
}
dispatch('loadMore')
lastLoadMoreDispatch = Date.now()
}
$effect(() => {
if (tableContainer && hasMore && !loading) {
untrack(() => checkScrollStatus())
}
})
</script>
<div
class={twMerge(
'h-full',
rounded ? 'rounded-md overflow-hidden' : '',
noBorder ? 'border-0' : 'border',
containerClass
)}
bind:clientHeight={tableHeight}
>
<List justify="between" gap="none" hFull={true}>
<div
class={twMerge('w-full overflow-auto h-fit', preventXOverflow ? 'overflow-x-hidden' : '')}
bind:this={tableContainer}
onscroll={handleScroll}
>
<table class={tableFixed ? 'table-fixed w-full' : 'min-w-full'}>
{@render children?.()}
</table>
{@render emptyMessage?.()}
</div>
{#if paginated && !shouldHidePagination}
<div
class="w-full bg-surface border-t flex flex-row justify-between p-1 items-center gap-2 sticky bottom-0"
bind:clientHeight={footerHeight}
>
<div>
{#if rowCount}
<span class="text-xs mx-2"> {rowCount} items</span>
{/if}
</div>
<div class="flex flex-row gap-2 items-center">
<span class="text-xs">
Page: {currentPage}
{perPage && rowCount ? `/ ${Math.ceil(rowCount / perPage)}` : ''}
</span>
{#if perPage !== undefined}
<select class="!text-xs !w-16" bind:value={perPage}>
<option value={25}>25</option>
<option value={100}>100</option>
<option value={1000}>1000</option>
</select>
{/if}
{#if showPrev}
<Button
color="light"
size="xs2"
on:click={() => dispatch('previous')}
disabled={currentPage === 1}
startIcon={{ icon: ArrowLeftIcon }}
>
Previous
</Button>
{/if}
{#if showNext}
<Button
color="light"
size="xs2"
on:click={() => dispatch('next')}
endIcon={{ icon: ArrowRightIcon }}
disabled={!hasMore}
>
Next
</Button>
{/if}
</div>
</div>
{:else if shouldLoadMore}
<div class="bg-surface border-t flex flex-row justify-center py-4 items-center gap-2">
<Button
color="light"
size="xs2"
on:click={() => triggerLoadMore()}
endIcon={{ icon: ArrowDownIcon }}
>
Load {loadMore} more
</Button>
</div>
{/if}
{#if (loading || loadingMore) && !neverShowLoader}
<div
class="text-primary bg-surface border-t flex flex-row justify-center py-2 items-center gap-2"
>
<Loader2 class="animate-spin" size={14} />
{#if loadingMore}
<span class="text-xs">Loading more...</span>
{/if}
</div>
{/if}
</List>
</div>