use new table virtualization

This commit updates the pink-svelte and pink-icons-svelte dependencies to new versions and refactors the collection table to use the new virtualized table component for improved performance.
This commit is contained in:
Torsten Dittmann
2025-06-18 11:28:15 +02:00
parent 3f3af27064
commit 486ab1a12f
6 changed files with 83 additions and 114 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
"@appwrite.io/pink-legacy": "^1.0.3",
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778",
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@efb9310",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
"@stripe/stripe-js": "^3.5.0",
+25 -10
View File
@@ -24,8 +24,8 @@ importers:
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778
version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@efb9310
version: https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@efb9310(svelte@5.25.3)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@@ -260,9 +260,8 @@ packages:
'@appwrite.io/console@1.9.0':
resolution: {integrity: sha512-g8+zfdBF8mz7tRUER4CGVe5FHWQVLp8TlY/UOGCZ2TgUF1qnpNu/DhiQgph8uF+QZ9jDKCLAAZlP5//9t7ckWA==}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870}
version: 2.0.0-RC.1
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
resolution: {integrity: sha512-iLFlV55hj8mGuAbmxJGenxN5RaZMmVT4GJb9dv/MP1xBAtYibFq7JvBcxm18qV2KU8c31Rntf+Ub4GL7HwqTYg==}
peerDependencies:
svelte: ^4.0.0
@@ -281,8 +280,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778':
resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778}
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@efb9310':
resolution: {tarball: https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@efb9310}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -1265,6 +1264,14 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tanstack/svelte-virtual@3.13.10':
resolution: {integrity: sha512-MVpi/yJpliu6ab7ScrYWk3VYhRh2gwyXZxEV12G5TYZqu/hNEqvw6GUTYBcAg6xzRq4VKtxt89rQSqlGyG4OMA==}
peerDependencies:
svelte: ^3.48.0 || ^4.0.0 || ^5.0.0
'@tanstack/virtual-core@3.13.10':
resolution: {integrity: sha512-sPEDhXREou5HyZYqSWIqdU580rsF6FGeN7vpzijmP3KTiOGjOMZASz4Y6+QKjiFQwhWrR58OP8izYaNGVxvViA==}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@@ -3637,7 +3644,7 @@ snapshots:
'@appwrite.io/console@1.9.0': {}
'@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)':
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3654,12 +3661,13 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
'@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@ee1b778(svelte@5.25.3)':
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/%40appwrite.io%2Fpink-svelte@efb9310(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@ee1b7788cd3f877c9aa1b6487976bd63a6196870(svelte@5.25.3)
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
'@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3)
'@melt-ui/svelte': 0.86.6(svelte@5.25.3)
'@tanstack/svelte-virtual': 3.13.10(svelte@5.25.3)
ansicolor: 2.0.3
d3: 7.9.0
fuse.js: 7.1.0
@@ -4710,6 +4718,13 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tanstack/svelte-virtual@3.13.10(svelte@5.25.3)':
dependencies:
'@tanstack/virtual-core': 3.13.10
svelte: 5.25.3
'@tanstack/virtual-core@3.13.10': {}
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
+2 -2
View File
@@ -19,7 +19,7 @@
} = $props();
let maxHeight = $state('none');
let containerRef;
let containerRef: HTMLDivElement = $state();
const calcMaxHeight = () => {
if (containerRef) {
@@ -67,7 +67,7 @@
calcMaxHeight();
});
let selectedColumnsNumber = $derived(
const selectedColumnsNumber = $derived(
$columns.reduce((acc, column) => {
if (column.hide === true) return acc;
+1 -1
View File
@@ -108,7 +108,7 @@ function createPreferences() {
);
},
getCustomCollectionColumns: (collectionId: string): Preferences['columns'] => {
return preferences?.collections?.[collectionId] ?? [];
return preferences?.collections?.[collectionId] ?? null;
},
setLimit: (limit: Preferences['limit']) =>
updateAndSync((n) => {
@@ -14,7 +14,6 @@
import CreateAttribute from './createAttribute.svelte';
import { collection, columns, isCsvImportInProgress } from './store';
import Table from './table.svelte';
import { writable } from 'svelte/store';
import FilePicker from '$lib/components/filePicker.svelte';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
@@ -32,28 +31,22 @@
let showCreateAttribute = false;
let selectedAttribute: Option['name'] = null;
const filterColumns = writable<Column[]>([]);
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
$: columns.set(
$collection.attributes.map((attribute) => ({
$: columns.set([
{ id: '$id', width: 200, title: 'Document ID', type: 'string' },
...$collection.attributes.map<Column>((attribute) => ({
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
show: selected?.includes(attribute.key) ?? true,
hide: !(selected?.includes(attribute.key) ?? true),
array: attribute?.array,
width: { min: 168 },
format: 'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
}))
);
$: filterColumns.set([
...$columns,
...['$id', '$createdAt', '$updatedAt'].map((id) => ({
id,
title: id,
show: true,
type: (id === '$id' ? 'string' : 'datetime') as ColumnType
}))
})),
{ id: '$created', width: 200, title: 'Created', type: 'datetime' },
{ id: '$updated', width: 200, title: 'Updated', type: 'datetime' }
]);
$: hasAttributes = !!$collection.attributes.length;
@@ -99,7 +92,7 @@
disabled={!(hasAttributes && hasValidAttributes)}
analyticsSource="database_documents" />
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={data.view} {columns} hideView />
<ViewSelector view={data.view} {columns} hideView isCustomCollection />
{#if flags.showCsvImport(data)}
<Button
secondary
@@ -133,7 +126,7 @@
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
<Table {data} />
<Table {data} {columns} />
<PaginationWithLimit
name="Documents"
@@ -10,7 +10,7 @@
import { preferences } from '$lib/stores/preferences';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { afterUpdate, onMount } from 'svelte';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import {
isRelationship,
@@ -18,8 +18,7 @@
isString
} from './document-[document]/attributes/store';
import RelationshipsModal from './relationshipsModal.svelte';
import { attributes, collection, columns } from './store';
import type { ColumnType } from '$lib/helpers/types';
import { attributes, collection } from './store';
import {
Tooltip,
Table,
@@ -33,8 +32,11 @@
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { flags } from '$lib/flags';
import type { Writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
export let data: PageData;
export let columns: Writable<Column[]>;
const databaseId = page.params.database;
const collectionId = page.params.collection;
@@ -45,23 +47,8 @@
onMount(async () => {
displayNames = preferences.getDisplayNames();
updateMaxWidth();
});
afterUpdate(() => updateMaxWidth());
function updateMaxWidth() {
const tableCells = Array.from(document.querySelectorAll('.less-width-truncated'));
const visibleColumnsCount = $columns.filter((col) => !col.hide).length;
const newMaxWidth = Math.max(50 - (visibleColumnsCount - 1) * 5, 25);
tableCells.forEach((cell) => {
const cellItem = cell as HTMLElement;
cellItem.style.maxWidth = `${newMaxWidth}vw`;
});
}
function formatArray(array: unknown[]) {
if (array.length === 0) return '[ ]';
@@ -100,24 +87,6 @@
};
}
$: selected = preferences.getCustomCollectionColumns(page.params.collection);
$: {
columns.set(
$collection.attributes.map((attribute) => ({
id: attribute.key,
title: attribute.key,
type: attribute.type as ColumnType,
show: selected?.includes(attribute.key) ?? true,
array: attribute?.array,
width: { min: 168 },
format:
'format' in attribute && attribute?.format === 'enum' ? attribute.format : null,
elements: 'elements' in attribute ? attribute.elements : null
}))
);
}
let selectedRows: string[] = [];
let showDelete = false;
@@ -173,41 +142,42 @@
const showEncrypt = flags.showAttributeEncrypt(data);
</script>
<Table.Root
let:root
allowSelection
bind:selectedRows
columns={[
{ id: '$id', width: 200 },
...$columns,
{ id: '$created', width: 200 },
{ id: '$updated', width: 200 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="$id" {root}>Document ID</Table.Header.Cell>
{#each $columns as column}
<Table.Header.Cell column={column.id} {root}>{column.title}</Table.Header.Cell>
<Table.VirtualRoot let:root let:virtualizer allowSelection bind:selectedRows columns={$columns}>
<svelte:fragment slot="header" let:root let:virtualizer>
{#each virtualizer.getVirtualItems() as item (item.index)}
{@const column = $columns[item.index]}
<Table.VirtualCell column={column.id} {root} virtualItem={item}>
{column.title}
</Table.VirtualCell>
{/each}
<Table.Header.Cell column="$created" {root}>Created</Table.Header.Cell>
<Table.Header.Cell column="$updated" {root}>Updated</Table.Header.Cell>
</svelte:fragment>
{#each data.documents.documents as document (document.$id)}
<Table.Row.Link
{root}
id={document.$id}
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${$collection.$id}/document-${document.$id}`}>
<Table.Cell column="$id" {root}>
{#key document.$id}
<Id value={document.$id}>
{document.$id}
</Id>
{/key}
</Table.Cell>
{#each $columns as { id } (id)}
{@const attr = $attributes.find((n) => n.key === id)}
{#if attr}
<Table.Cell column={id} {root}>
{#each virtualizer.getVirtualItems() as item (item.index)}
{@const column = $columns[item.index]}
{#if column.id === '$id'}
<Table.VirtualCell column="$id" {root} virtualItem={item}>
{#key document.$id}
<Id value={document.$id}>
{document.$id}
</Id>
{/key}
</Table.VirtualCell>
{:else if column.id === '$created'}
<Table.VirtualCell column="$created" {root} virtualItem={item}>
<DualTimeView time={document.$createdAt} />
</Table.VirtualCell>
{:else if column.id === '$updated'}
<Table.VirtualCell column="$updated" {root} virtualItem={item}>
<DualTimeView time={document.$updatedAt} />
</Table.VirtualCell>
{:else}
{@const id = column.id}
{@const attr = $attributes.find((n) => n.key === id)}
<Table.VirtualCell column={id} {root} virtualItem={item}>
{#if isRelationship(attr)}
{@const args = displayNames?.[attr.relatedCollection] ?? ['$id']}
{#if !isRelationshipToMany(attr)}
@@ -253,23 +223,20 @@
</Button.Button>
{/if}
{:else}
{@const datetime = document[id]}
{@const formatted = formatColumn(document[id])}
{@const isDatetimeAttribute = attr.type === 'datetime'}
{@const isEncryptedAttribute = isString(attr) && attr.encrypt}
{#if isDatetimeAttribute}
<DualTimeView time={datetime}>
<DualTimeView time={document[id]}>
<span slot="title">Timestamp</span>
{toLocaleDateTime(datetime, true)}
{toLocaleDateTime(document[id], true)}
</DualTimeView>
{:else if isEncryptedAttribute && showEncrypt}
<button on:click={(e) => e.preventDefault()}>
<InteractiveText
copy={false}
variant="secret"
isVisible={false}
text={formatted.value} />
</button>
<InteractiveText
copy={false}
variant="secret"
isVisible={false}
text={formatted.value} />
{:else if formatted.truncated}
<Tooltip placement="bottom" disabled={!formatted.truncated}>
<Typography.Text truncate>{formatted.value}</Typography.Text>
@@ -287,18 +254,12 @@
<Typography.Text truncate>{formatted.value}</Typography.Text>
{/if}
{/if}
</Table.Cell>
</Table.VirtualCell>
{/if}
{/each}
<Table.Cell column="$created" {root}>
<DualTimeView time={document.$createdAt} />
</Table.Cell>
<Table.Cell column="$updated" {root}>
<DualTimeView time={document.$updatedAt} />
</Table.Cell>
</Table.Row.Link>
{/each}
</Table.Root>
</Table.VirtualRoot>
<RelationshipsModal bind:show={showRelationships} {selectedRelationship} data={relationshipData} />