feat(frontend): Add List pagination + add loading state in tables (#2096)

* feat(frontend): Add List pagination + add loading state in tables

* feat(frontend): Use the same configuration as the Table, and managed pagination properly

* feat(frontend): Fix wording + update default code + preconnect page output

* feat(frontend): Fix wording + update default code + preconnect page output

* feat(frontend): fix default code

* feat(frontend): add comment in the default code to explain what the page parameter is

* feat(frontend): revert changes
This commit is contained in:
Faton Ramadani
2023-08-16 15:00:07 +02:00
committed by GitHub
parent a8f0b9aeff
commit 3a92d092d4
6 changed files with 245 additions and 60 deletions
@@ -65,6 +65,7 @@
})
let inputs = {}
let loading: boolean = false
$: setSearch(searchValue)
@@ -251,7 +252,15 @@
/>
{/each}
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper
{outputs}
{render}
{componentInput}
{id}
bind:initializing
bind:result
bind:loading
>
{#if Array.isArray(result) && result.every(isObject)}
<div
class={twMerge(
@@ -528,6 +537,7 @@
{table}
class={css?.tableFooter?.class}
style={css?.tableFooter?.style}
{loading}
/>
</div>
{:else if result != undefined}
@@ -2,7 +2,7 @@
import Button from '$lib/components/common/button/Button.svelte'
import { faDownload } from '@fortawesome/free-solid-svg-icons'
import type { Table } from '@tanstack/svelte-table'
import { ChevronLeft, ChevronRight } from 'lucide-svelte'
import { ChevronLeft, ChevronRight, Loader2 } from 'lucide-svelte'
import type { Readable } from 'svelte/store'
import { twMerge } from 'tailwind-merge'
@@ -16,6 +16,7 @@
export { c as class }
export let style = ''
export let download: boolean = true
export let loading: boolean = false
function convertJSONToCSV(objArray: Record<string, any>[]) {
let str = ''
@@ -45,6 +46,14 @@
downloadAnchorNode.click()
downloadAnchorNode.remove()
}
let isPreviousLoading = false
let isNextLoading = false
$: if (!loading) {
isPreviousLoading = false
isNextLoading = false
}
</script>
{#if result.length > pageSize || manualPagination || download}
@@ -55,26 +64,43 @@
{#if result.length > pageSize || manualPagination}
<div class="flex items-center gap-2 flex-row">
<Button
size="xs"
size="xs2"
variant="border"
color="light"
btnClasses="!py-1 !pl-1"
on:click={() => $table.previousPage()}
on:click={() => {
isPreviousLoading = true
$table.previousPage()
}}
disabled={!$table.getCanPreviousPage()}
>
<ChevronLeft size={14} />
Previous
<div class="flex flex-row gap-1 items-center">
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</div>
</Button>
<Button
size="xs"
size="xs2"
variant="border"
color="light"
btnClasses="!py-1 !pr-1"
on:click={() => $table.nextPage()}
on:click={() => {
isNextLoading = true
$table.nextPage()
}}
disabled={!$table.getCanNextPage()}
>
Next
<ChevronRight size={14} />
<div class="flex flex-row gap-1 items-center">
Next
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</div>
</Button>
{$table.getState().pagination.pageIndex + 1} of {$table.getPageCount()}
</div>
@@ -10,6 +10,8 @@
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { components } from '../../editor/component'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { Button } from '$lib/components/common'
import { Loader2, ChevronLeft, ChevronRight } from 'lucide-svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -20,11 +22,13 @@
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
let page = 0
const outputs = initOutput($worldStore, id, {
result: undefined,
loading: false,
inputs: {}
inputs: {},
page: 0
})
let resolvedConfig = initConfig(
@@ -45,6 +49,57 @@
$: isCard = resolvedConfig.width?.selected == 'card'
let inputs = {}
let loading: boolean = false
let isPreviousLoading = false
let isNextLoading = false
$: if (!loading) {
isPreviousLoading = false
isNextLoading = false
}
function getPagination(
configuration: {
auto: { pageSize: number | undefined }
manual: { pageCount: number | undefined }
},
mode: 'auto' | 'manual' = 'auto',
initialData: Array<any> | undefined = [],
page: number = 0
) {
if (mode === 'auto') {
const pageSize: number = configuration.auto.pageSize ?? 0
const data = initialData?.slice(0 + page * pageSize, pageSize + page * pageSize) ?? []
const shouldDisplayPagination = pageSize < initialData?.length ?? false
const total = Math.ceil(initialData?.length / pageSize ?? 0)
return {
data,
shouldDisplayPagination,
indexOffset: page * pageSize,
disableNext: pageSize > 0 && (page + 1) * pageSize >= initialData?.length,
total: total
}
} else {
const pageCount = configuration.manual.pageCount ?? 0
const total = pageCount
return {
shouldDisplayPagination: true,
data: initialData ?? [],
indexOffset: 0,
disableNext: page + 1 >= pageCount,
total: total
}
}
}
$: pagination = getPagination(
resolvedConfig.pagination?.configuration,
resolvedConfig.pagination?.selected,
result,
page
)
</script>
{#each Object.keys(components['listcomponent'].initialData.configuration) as key (key)}
@@ -66,54 +121,103 @@
{id}
bind:initializing
bind:result
bind:loading
>
<div
class="w-full flex flex-wrap overflow-auto {isCard ? 'h-full gap-2' : 'divide-y max-h-full'}"
>
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(result) && result.length > 0}
{#each result ?? [] as value, index}
<div
style={`${
isCard
? `min-width: ${resolvedConfig.width?.configuration?.card?.minWidthPx}px; `
: ''
} max-height: ${resolvedConfig.heightPx}px;`}
class="overflow-auto {!isCard ? 'w-full' : 'border'}"
>
<ListWrapper
on:inputsChange={() => {
outputs?.inputs.set(inputs, true)
}}
bind:inputs
{value}
{index}
<div class="flex flex-col divide-y h-full">
<div
class="w-full flex flex-wrap overflow-auto {isCard ? 'h-full gap-2' : 'divide-y max-h-full'}"
>
{#if $app.subgrids?.[`${id}-0`]}
{#if Array.isArray(pagination.data) && pagination.data.length > 0}
{#each pagination?.data ?? [] as value, index}
<div
style={`${
isCard
? `min-width: ${resolvedConfig.width?.configuration?.card?.minWidthPx}px; `
: ''
} max-height: ${resolvedConfig.heightPx}px;`}
class="overflow-auto {!isCard ? 'w-full' : 'border'}"
>
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={resolvedConfig.heightPx}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
<ListWrapper
on:inputsChange={() => {
outputs?.inputs.set(inputs, true)
}}
/>
</ListWrapper>
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
bind:inputs
{value}
index={index + pagination.indexOffset}
>
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={resolvedConfig.heightPx}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
}
onFocus()
}}
/>
</ListWrapper>
</div>
{/each}
{:else}
<ListWrapper disabled value={undefined} index={0}>
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
</ListWrapper>
{#if !Array.isArray(result)}
<div class="text-center text-tertiary">Input data is not an array</div>
{/if}
{/if}
{/if}
</div>
{#if pagination.shouldDisplayPagination}
<div class="bg-surface-secondary h-8 flex flex-row gap-1 p-1 items-center">
<Button
size="xs2"
variant="border"
color="light"
on:click={() => {
isPreviousLoading = true
page = page - 1
outputs?.page.set(page, true)
}}
disabled={page === 0}
>
<div class="flex flex-row gap-1 items-center">
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</div>
</Button>
<Button
size="xs2"
variant="border"
color="light"
on:click={() => {
isNextLoading = true
page = page + 1
outputs?.page.set(page, true)
}}
disabled={pagination.disableNext}
>
<div class="flex flex-row gap-1 items-center">
Next
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</div>
</Button>
<div class="text-xs">{page + 1} of {pagination.total}</div>
</div>
{/if}
</div>
</RunnableWrapper>
@@ -431,9 +431,11 @@ const paginationOneOf = {
type: 'oneOf',
selected: 'auto',
labels: {
auto: 'Auto',
manual: 'Manual'
auto: 'Managed by component',
manual: 'Managed by runnable'
},
tooltip:
'Pagination can be managed using two methods: By the component: Based on a specified page size, the component divides the array into several pages. By the runnable: The component shows all items, leaving the task of pagination to the runnable. The current page number is available in the component outputs.',
configuration: {
auto: {
pageSize: {
@@ -603,7 +605,36 @@ export const components = {
fieldType: 'number',
value: 280,
tooltip: 'Height in pixels'
}
},
pagination: {
type: 'oneOf',
selected: 'auto',
labels: {
auto: 'Managed by component',
manual: 'Managed by runnable'
},
tooltip:
'Pagination can be managed using two methods: By the component: Based on a specified page size, the component divides the array into several pages. By the runnable: The component shows all items, leaving the task of pagination to the runnable. The current page number is available in the component outputs.',
configuration: {
manual: {
pageCount: {
type: 'static',
fieldType: 'number',
value: -1,
tooltip: 'Number of pages (-1 if you do not know)'
}
},
auto: {
pageSize: {
type: 'static',
fieldType: 'number',
value: 20,
tooltip: 'Number of items per page'
}
}
}
} as const
},
componentInput: {
type: 'static',
@@ -530,5 +530,18 @@ return {
"required": []
}
`
},
listcomponent: {
deno: `export async function main() {
return [{
"foo": 1,
}, {
"foo": 2,
}, {
"foo": 3,
}];
}`,
python3: `def main():
return [{"foo": 1}, {"foo": 2}, {"foo": 3}]`
}
} as const
@@ -33,7 +33,7 @@
<div class="mb-2 text-sm font-semibold">
{capitalize(addWhitespaceBeforeCapitals(key))}&nbsp;
{#if tooltip}
<Tooltip>{tooltip}</Tooltip>
<Tooltip light>{tooltip}</Tooltip>
{/if}
</div>
<select
@@ -69,6 +69,7 @@
onlyStatic={config?.['onlyStatic']}
customTitle={config?.['customTitle']}
noVariablePicker={config?.['noVariablePicker']}
tooltip={config?.['tooltip']}
/>
{/if}
{/each}