-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1005 from rainlanguage/move-tanstackapptable
Move TanstackAppTable into ui-components and webapp
- Loading branch information
Showing
35 changed files
with
646 additions
and
518 deletions.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
packages/ui-components/src/lib/components/TanstackAppTable.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<script lang="ts" generics="T"> | ||
import Refresh from './icon/Refresh.svelte'; | ||
import type { CreateInfiniteQueryResult, InfiniteData } from '@tanstack/svelte-query'; | ||
import { Button, Table, TableBody, TableBodyRow, TableHead } from 'flowbite-svelte'; | ||
import { createEventDispatcher } from 'svelte'; | ||
const dispatch = createEventDispatcher(); | ||
// eslint-disable-next-line no-undef | ||
export let query: CreateInfiniteQueryResult<InfiniteData<T[], unknown>, Error>; | ||
export let emptyMessage: string = 'None found'; | ||
export let rowHoverable = true; | ||
</script> | ||
|
||
<div data-testid="title" class="flex h-16 w-full items-center justify-end"> | ||
<slot name="info" /> | ||
<slot name="timeFilter" /> | ||
<slot name="title" /> | ||
<Refresh | ||
data-testid="refreshButton" | ||
class="ml-2 h-8 w-5 cursor-pointer text-gray-400 dark:text-gray-400" | ||
spin={$query.isLoading || $query.isFetching} | ||
on:click={() => { | ||
$query.refetch(); | ||
}} | ||
/> | ||
</div> | ||
{#if $query.data?.pages[0].length === 0} | ||
<div data-testid="emptyMessage" class="text-center text-gray-900 dark:text-white"> | ||
{emptyMessage} | ||
</div> | ||
{:else if $query.data} | ||
<Table | ||
divClass="cursor-pointer rounded-lg overflow-hidden dark:border-none border" | ||
hoverable={rowHoverable} | ||
> | ||
<TableHead data-testid="head"> | ||
<slot name="head" /> | ||
</TableHead> | ||
<TableBody> | ||
{#each $query.data?.pages as page} | ||
{#each page as item} | ||
<TableBodyRow | ||
data-testid="bodyRow" | ||
on:click={() => { | ||
dispatch('clickRow', { item }); | ||
}} | ||
> | ||
<slot name="bodyRow" {item} /> | ||
</TableBodyRow> | ||
{/each} | ||
{/each} | ||
</TableBody> | ||
</Table> | ||
<div class="mt-2 flex justify-center"> | ||
<Button | ||
data-testid="loadMoreButton" | ||
size="xs" | ||
color="dark" | ||
on:click={() => $query.fetchNextPage()} | ||
disabled={!$query.hasNextPage || $query.isFetchingNextPage} | ||
> | ||
{#if $query.isFetchingNextPage} | ||
Loading more... | ||
{:else if $query.hasNextPage} | ||
Load More | ||
{:else}Nothing more to load{/if} | ||
</Button> | ||
</div> | ||
{/if} |
17 changes: 17 additions & 0 deletions
17
packages/ui-components/src/lib/components/TanstackAppTable.test.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<script lang="ts"> | ||
import TanstackAppTable from './TanstackAppTable.svelte'; | ||
export let query; | ||
export let emptyMessage: string; | ||
export let title: string; | ||
export let head: string; | ||
</script> | ||
|
||
<TanstackAppTable {query} {emptyMessage} rowHoverable> | ||
<svelte:fragment slot="title"> | ||
<h2>{title}</h2> | ||
</svelte:fragment> | ||
<svelte:fragment slot="head">{head}</svelte:fragment> | ||
<svelte:fragment slot="bodyRow" let:item> | ||
{item} | ||
</svelte:fragment> | ||
</TanstackAppTable> |
237 changes: 237 additions & 0 deletions
237
packages/ui-components/src/lib/components/TanstackAppTable.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
import { render, screen, waitFor } from '@testing-library/svelte'; | ||
import { test, expect } from 'vitest'; | ||
import TanstackAppTableTest from './TanstackAppTable.test.svelte'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { createResolvableInfiniteQuery } from '../mocks/queries'; | ||
|
||
test('shows head and title', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { | ||
return ['page' + pageParam]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('head')).toHaveTextContent('Test head')); | ||
expect(screen.getByTestId('title')).toHaveTextContent('Test Table'); | ||
}); | ||
|
||
test('renders rows', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { | ||
return ['page' + pageParam]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('page0')); | ||
}); | ||
|
||
test('shows empty message', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery(() => { | ||
return []; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('emptyMessage')).toHaveTextContent('No rows')); | ||
}); | ||
|
||
test('loads more rows', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { | ||
return ['page' + pageParam]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('page0')); | ||
|
||
// loading more rows | ||
const loadMoreButton = screen.getByTestId('loadMoreButton'); | ||
await userEvent.click(loadMoreButton); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => { | ||
expect(screen.getAllByTestId('bodyRow')).toHaveLength(2); | ||
}); | ||
|
||
let rows = screen.getAllByTestId('bodyRow'); | ||
|
||
expect(rows).toHaveLength(2); | ||
expect(rows[0]).toHaveTextContent('page0'); | ||
expect(rows[1]).toHaveTextContent('page1'); | ||
|
||
// loading more rows | ||
await userEvent.click(loadMoreButton); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => { | ||
expect(screen.getAllByTestId('bodyRow')).toHaveLength(3); | ||
}); | ||
|
||
rows = screen.getAllByTestId('bodyRow'); | ||
|
||
expect(rows).toHaveLength(3); | ||
expect(rows[0]).toHaveTextContent('page0'); | ||
expect(rows[1]).toHaveTextContent('page1'); | ||
expect(rows[2]).toHaveTextContent('page2'); | ||
}); | ||
|
||
test('load more button message changes when loading', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { | ||
return ['page' + pageParam]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
expect(await screen.findByTestId('loadMoreButton')).toHaveTextContent('Load More'); | ||
|
||
// loading more rows | ||
const loadMoreButton = screen.getByTestId('loadMoreButton'); | ||
await userEvent.click(loadMoreButton); | ||
|
||
expect(await screen.findByTestId('loadMoreButton')).toHaveTextContent('Loading more...'); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('loadMoreButton')).toHaveTextContent('Load More'); | ||
}); | ||
}); | ||
|
||
test('load more buttton is disabled when loading', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery((pageParam) => { | ||
return ['page' + pageParam]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('loadMoreButton')).not.toHaveAttribute('disabled')); | ||
|
||
// loading more rows | ||
const loadMoreButton = screen.getByTestId('loadMoreButton'); | ||
loadMoreButton.click(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('loadMoreButton')).toHaveAttribute('disabled')); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('loadMoreButton')).not.toHaveAttribute('disabled')); | ||
}); | ||
|
||
test('load more buttton is disabled when there are no more pages', async () => { | ||
const { query, resolve } = createResolvableInfiniteQuery( | ||
(pageParam) => { | ||
if (!pageParam) return ['page' + pageParam]; | ||
return []; | ||
}, | ||
(_lastPage, _allPages, lastPageParam) => { | ||
if (lastPageParam === 0) return 1; | ||
return undefined; | ||
} | ||
); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('loadMoreButton')).not.toHaveAttribute('disabled')); | ||
|
||
// loading more rows | ||
const loadMoreButton = screen.getByTestId('loadMoreButton'); | ||
loadMoreButton.click(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('loadMoreButton')).toHaveAttribute('disabled')); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => | ||
expect(screen.getByTestId('loadMoreButton')).toHaveTextContent('Nothing more to load') | ||
); | ||
}); | ||
|
||
test('refetches data when refresh button is clicked', async () => { | ||
let refreshCount = 0; | ||
const { query, resolve } = createResolvableInfiniteQuery(() => { | ||
refreshCount++; | ||
return ['refresh' + refreshCount]; | ||
}); | ||
|
||
render(TanstackAppTableTest, { | ||
query, | ||
emptyMessage: 'No rows', | ||
title: 'Test Table', | ||
head: 'Test head' | ||
}); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('refresh1')); | ||
|
||
// refreshing | ||
const refreshButton = screen.getByTestId('refreshButton'); | ||
await userEvent.click(refreshButton); | ||
|
||
// refreshButton should have the class animate-spin | ||
await waitFor(() => expect(refreshButton).toHaveClass('animate-spin')); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('refresh2')); | ||
|
||
// refreshButton should not have the class animate-spin | ||
await waitFor(() => expect(refreshButton).not.toHaveClass('animate-spin')); | ||
|
||
// refreshing | ||
await userEvent.click(refreshButton); | ||
|
||
resolve(); | ||
|
||
await waitFor(() => expect(screen.getByTestId('bodyRow')).toHaveTextContent('refresh3')); | ||
}); |
55 changes: 55 additions & 0 deletions
55
packages/ui-components/src/lib/components/icon/Refresh.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<script lang="ts"> | ||
import { getContext } from 'svelte'; | ||
import type { AriaRole } from 'svelte/elements'; | ||
import { twMerge } from 'tailwind-merge'; | ||
const ctx: { size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; role: AriaRole | undefined } = | ||
getContext('iconCtx') ?? {}; | ||
const sizes = { | ||
xs: 'w-3 h-3', | ||
sm: 'w-4 h-4', | ||
md: 'w-5 h-5', | ||
lg: 'w-6 h-6', | ||
xl: 'w-8 h-8' | ||
}; | ||
export let size = ctx.size || 'md'; | ||
export let role = ctx.role || 'img'; | ||
export let ariaLabel = 'refresh'; | ||
export let spin = false; | ||
</script> | ||
|
||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
{...$$restProps} | ||
class={twMerge('shrink-0', sizes[size], $$props.class, spin ? 'animate-spin' : '')} | ||
{role} | ||
aria-label={ariaLabel} | ||
viewBox="0 0 24 24" | ||
on:click | ||
on:keydown | ||
on:keyup | ||
on:focus | ||
on:blur | ||
on:mouseenter | ||
on:mouseleave | ||
on:mouseover | ||
on:mouseout | ||
> | ||
<path | ||
stroke="currentColor" | ||
stroke-linecap="round" | ||
stroke-linejoin="round" | ||
stroke-width="2" | ||
d="M17.651 7.65a7.131 7.131 0 0 0-12.68 3.15M18.001 4v4h-4m-7.652 8.35a7.13 7.13 0 0 0 12.68-3.15M6 20v-4h4" | ||
/> | ||
</svg> | ||
|
||
<!-- | ||
@component | ||
[Go to docs](https://flowbite-svelte-icons.vercel.app/) | ||
## Props | ||
@prop export let size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' = ctx.size || 'md'; | ||
@prop export let role = ctx.role || 'img'; | ||
@prop export let ariaLabel = 'refresh'; | ||
--> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,15 @@ | ||
import './app.css'; | ||
export { default as CardProperty } from './components/CardProperty.svelte'; | ||
export { default as Hash, HashType } from './components/Hash.svelte'; | ||
export { default as TanstackAppTable } from './components/TanstackAppTable.svelte'; | ||
export { | ||
formatTimestampSecondsAsLocal, | ||
timestampSecondsToUTCTimestamp, | ||
promiseTimeout | ||
} from './utils/time'; | ||
export { default as Refresh } from './components/icon/Refresh.svelte'; | ||
export { | ||
createResolvableQuery, | ||
createResolvableInfiniteQuery, | ||
createResolvableMockQuery | ||
} from './mocks/queries'; |
Oops, something went wrong.