Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions src/lib/actions/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export enum Click {
DatabaseDatabaseDelete = 'click_database_delete',
DatabaseImportCsv = 'click_database_import_csv',
DatabaseExportCsv = 'click_database_export_csv',
DatabaseExportJson = 'click_database_export_json',
Comment thread
Divyansh2992 marked this conversation as resolved.
Comment thread
Divyansh2992 marked this conversation as resolved.
DomainCreateClick = 'click_domain_create',
DomainDeleteClick = 'click_domain_delete',
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
Expand Down Expand Up @@ -283,6 +284,7 @@ export enum Submit {
DatabaseUpdateName = 'submit_database_update_name',
DatabaseImportCsv = 'submit_database_import_csv',
DatabaseExportCsv = 'submit_database_export_csv',
DatabaseExportJson = 'submit_database_export_json',
DatabaseBackupDelete = 'submit_database_backup_delete',
DatabaseBackupPolicyCreate = 'submit_database_backup_policy_create',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
}



onDestroy(() => ($showCreateColumnSheet.show = false));
</script>

Expand Down Expand Up @@ -239,7 +241,7 @@
<Icon icon={IconDownload} size="s" />
</Button>

<svelte:fragment slot="tooltip">Export CSV</svelte:fragment>
<svelte:fragment slot="tooltip">Export</svelte:fragment>
</Tooltip>

<Tooltip disabled={isRefreshing || !data.rows?.total} placement="top">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
<script lang="ts">
import { onMount } from 'svelte';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { Wizard } from '$lib/layout';
import { Fieldset, Layout, Icon, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import { Button, InputCheckbox, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import { table } from '../store';
import { queries, type TagValue } from '$lib/components/filters/store';
import { TagList } from '$lib/components/filters';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { toLocalDateTimeISO } from '$lib/helpers/date';
import { writable } from 'svelte/store';
import { isSmallViewport } from '$lib/stores/viewport';
import { Query } from '@appwrite.io/console';

let showExitModal = $state(false);
let formComponent: Form;
let isSubmitting = $state(writable(false));

let localQueries = $state<Map<TagValue, string>>(new Map());
const localTags = $derived(Array.from(localQueries.keys()));

const timestamp = toLocalDateTimeISO(Date.now())
.replace(/[:.]/g, '-')
.split('T')
.join('_')
.slice(0, -4);
const filename = `${$table.name}_${timestamp}.json`;

let selectedColumns = $state<Record<string, boolean>>({});
let showAllColumns = $state(false);
let exportWithFilters = $state(false);

const columnLimit = $derived($isSmallViewport ? 6 : 9);
const visibleColumns = $derived(
showAllColumns ? $table.columns : $table.columns.slice(0, columnLimit)
);
const hasMoreColumns = $derived($table.columns.length > columnLimit);
const selectedColumnCount = $derived(Object.values(selectedColumns).filter(Boolean).length);

const tableUrl = $derived.by(() => {
const queryParam = page.url.searchParams.get('query');
const url = resolve(
'/(console)/project-[region]-[project]/databases/database-[database]/table-[table]',
{
region: page.params.region,
project: page.params.project,
database: page.params.database,
table: page.params.table
}
);
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
});

function removeLocalFilter(tag: TagValue) {
localQueries.delete(tag);
localQueries = new Map(localQueries);
}

function initializeColumns() {
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, true]));
}

function selectAllColumns() {
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, true]));
}

function deselectAllColumns() {
selectedColumns = Object.fromEntries($table.columns.map((col) => [col.key, false]));
}

async function handleExport() {
const selectedCols = Object.entries(selectedColumns)
.filter(([_, selected]) => selected)
.map(([key]) => key);

if (selectedCols.length === 0) {
addNotification({
type: 'error',
message: 'Please select at least one column to export'
});
return;
}

$isSubmitting = true;
try {
const activeQueries = exportWithFilters ? Array.from(localQueries.values()) : [];
const allRows: Record<string, unknown>[] = [];
const pageSize = 100;
let offset = 0;
let total = Infinity;

while (allRows.length < total) {
const response = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.listRows({
databaseId: page.params.database,
tableId: page.params.table,
queries: [
Query.limit(pageSize),
Query.offset(offset),
...activeQueries
]
});

total = response.total;

const filtered = response.rows.map((row) => {
const obj: Record<string, unknown> = {};
for (const col of selectedCols) {
obj[col] = row[col];
}
return obj;
});

allRows.push(...filtered);
offset += response.rows.length;

if (response.rows.length === 0) break;
}
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated

const json = JSON.stringify(allRows, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);

addNotification({
type: 'success',
message: `JSON export complete — ${allRows.length} row${allRows.length !== 1 ? 's' : ''} downloaded`
});

trackEvent(Submit.DatabaseExportJson);

await goto(tableUrl);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});

trackError(error, Submit.DatabaseExportJson);
} finally {
$isSubmitting = false;
}
}

onMount(() => {
initializeColumns();
localQueries = new Map($queries);
});
</script>

<Wizard title="Export JSON" columnSize="s" href={tableUrl} bind:showExitModal confirmExit column>
<Form bind:this={formComponent} bind:isSubmitting onSubmit={handleExport}>
<Layout.Stack gap="xxl">
<Fieldset legend="Columns">
<Layout.Stack gap="l">
<Layout.Stack direction="row" gap="s" alignItems="center">
<Button compact on:click={selectAllColumns}>Select all</Button>
<span style:height="20px">
<Divider vertical />
</span>
<Button compact on:click={deselectAllColumns}>Deselect all</Button>
</Layout.Stack>

<Layout.Grid columns={3} columnsS={1} gap="l">
{#each visibleColumns as column (column.key)}
<div style="min-width: 0;">
<InputCheckbox
id={`column-${column.key}`}
label={column.key}
bind:checked={selectedColumns[column.key]}
truncate />
</div>
{/each}
</Layout.Grid>

{#if hasMoreColumns}
<div style:margin-bottom="-0.5rem">
<Button compact on:click={() => (showAllColumns = !showAllColumns)}>
{showAllColumns ? 'Show less' : 'Show more'}
</Button>
</div>
{/if}
</Layout.Stack>
</Fieldset>

<Fieldset legend="Export options">
<Layout.Stack gap="l">
<Layout.Stack gap="m">
<div class:disabled-checkbox={localTags.length === 0}>
<InputCheckbox
id="exportWithFilters"
label="Export with filters"
description="Export rows that match the current table filters"
disabled={localTags.length === 0}
bind:checked={exportWithFilters} />
</div>

{#if localTags.length > 0}
<Layout.Stack
direction="row"
gap="xs"
alignItems="center"
style="padding-left: 1.75rem;"
wrap="wrap">
<TagList
tags={localTags}
on:remove={(e) => {
removeLocalFilter(e.detail);
}} />
</Layout.Stack>
{/if}
</Layout.Stack>
</Layout.Stack>
</Fieldset>
</Layout.Stack>
</Form>
<svelte:fragment slot="footer">
<Layout.Stack justifyContent="flex-end" direction="row">
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>
Cancel
</Button>
<Button
fullWidthMobile
on:click={() => formComponent.triggerSubmit()}
disabled={$isSubmitting || selectedColumnCount === 0}>
Export
</Button>
</Layout.Stack>
</svelte:fragment>
</Wizard>

<style>
.disabled-checkbox :global(*) {
cursor: unset;
}
</style>
Comment thread
Divyansh2992 marked this conversation as resolved.
Outdated
Loading