Skip to content

Conversation

@abnegate
Copy link
Member

@abnegate abnegate commented Jan 20, 2026

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

Summary by CodeRabbit

  • New Features

    • Full CSV export wizard for tables (delimiter, header, filter, notifications).
    • Column selection UI with select/deselect and responsive show more/less.
    • Export and Import CSV controls in the table header and an expand/collapse toggle.
    • Real-time CSV export progress box with counts, progress bars, automatic downloads, and error details.
    • Added analytics events for CSV export actions.
    • Checkbox option to enable label truncation.
  • Chores

    • Minor package manager configuration adjustment.

✏️ Tip: You can customize this high-level summary in your review settings.

@appwrite
Copy link

appwrite bot commented Jan 20, 2026

Console (appwrite/console)

Project ID: 688b7bf400350cbd60e9

Sites (1)
Site Status Logs Preview QR
 console-stage
688b7cf6003b1842c9dc
Building Building View Logs Preview URL QR Code

Tip

Storage files get ClamAV malware scanning and encryption by default

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

Walkthrough

Updates introduce CSV export functionality and related UI/analytics: a new CSV export wizard page for tables; a runtime CSV export progress component (csvExportBox.svelte) with real-time migration updates, download links, and error modal; added CSV export analytics enum entries; Export/Import CSV buttons and an expand/collapse control in the table header; CsvExportBox exported from the components barrel; a new truncate prop on the checkbox input component; and package.json packageManager changed from "pnpm@10.20.0" to "pnpm@10.18.3".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'CSV export' is vague and generic, using non-descriptive terms that don't convey meaningful information about the specific changes in the changeset. Use a more specific title that reflects the main change, such as 'Add CSV export feature with progress tracking' or 'Implement database table CSV export wizard'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/lib/components/csvExportBox.svelte`:
- Around line 26-32: The automatic call to window.open in downloadExportedFile
can be blocked by popup blockers; change the behavior to not auto-open but
instead expose the download URL to the component state and render a visible
fallback (e.g., a "Download CSV" anchor/button) when the URL becomes available,
and only call window.open() (or programmatic click) in response to a user
gesture via that button; update any code paths that invoke downloadExportedFile
(status 'completed' handling) to set the URL state instead of directly opening
it and ensure the UI shows the fallback link/button bound to
downloadExportedFile.
- Around line 121-133: The text() function is injecting unescaped tableName into
an HTML string used with {`@html`}, creating an XSS risk; fix by either
HTML-escaping tableName inside text() (replace &, <, >, ", ' with entities
before wrapping in <b>) or, better, stop returning HTML from text() and instead
return plain strings and render the bold label in the Svelte template (e.g. use
<b>{tableName}</b> in the markup and remove {`@html` text(...)}), and update all
call sites that use {`@html` text(...)} to the new safe rendering approach; the
function name to change is text() and the template usage to update is where
{`@html`} renders its result.
- Around line 135-145: The initial fetch in onMount calling
sdk.forProject(...).migrations.list(...) lacks error handling; update the
onMount logic (the sdk.forProject(...).migrations.list call) to handle failures
by adding a .catch(...) to the promise chain (or convert to async/await with
try/catch) and call a handler such as processLogger.error or set a component
error state; ensure the catch logs the error and does not leave the UI in a
silent failed state while still calling updateOrAddItem in the successful .then
branch.

In
`@src/routes/`(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte:
- Around line 56-68: The constructed tableUrl currently appends raw query text
from page.url.searchParams.get('query') which can break URLs; update the
tableUrl derivation (the const tableUrl = $derived.by(...) block) to URL-encode
the query using encodeURIComponent (same approach as getTableExportUrl) when
building the returned string so the query param is safe (i.e., call
encodeURIComponent on the queryParam before interpolating into
`${url}?query=...`).
🧹 Nitpick comments (2)
src/routes/(console)/project-[region]-[project]/+layout.svelte (1)

2-2: Inconsistent import style for CSV components.

CsvExportBox is imported from the barrel ($lib/components), but CsvImportBox is imported directly from its file path. For consistency and per coding guidelines, consider importing CsvImportBox from the barrel as well.

Suggested fix
-    import { BackupRestoreBox, MigrationBox, UploadBox, CsvExportBox } from '$lib/components';
+    import { BackupRestoreBox, MigrationBox, UploadBox, CsvExportBox, CsvImportBox } from '$lib/components';
...
-import CsvImportBox from '$lib/components/csvImportBox.svelte';

Based on coding guidelines: use $lib alias for module imports.

Also applies to: 27-27

src/lib/components/csvExportBox.svelte (1)

51-77: Redundant variable and expression.

existing (line 65) duplicates current (line 55). Also, tableName is assigned from current?.table on line 56, making tableName ?? current?.table on line 81 always evaluate to current?.table.

Suggested cleanup
     async function updateOrAddItem(exportData: Payload | Models.Migration) {
         if (exportData.destination?.toLowerCase() !== 'csv') return;

         const status = exportData.status;
         const current = exportItems.get(exportData.$id);
-        let tableName = current?.table;

         // Get bucket, filename, and download URL from migration options
         const options = ('options' in exportData ? exportData.options : {}) || {};
         const bucketId = options.bucketId || '';
         const fileName = options.filename || '';
         const downloadUrl = options.downloadUrl || '';
         let bucketName = current?.bucketName;

-        const existing = exportItems.get(exportData.$id);
-
         const isDone = (s: string) => ['completed', 'failed'].includes(s);
         const isInProgress = (s: string) => ['pending', 'processing'].includes(s);

         // Skip if we're trying to set an in-progress status on a completed migration
-        const shouldSkip = existing && isDone(existing.status) && isInProgress(status);
+        const shouldSkip = current && isDone(current.status) && isInProgress(status);

         const hasNewData =
-            downloadUrl && (!existing?.downloadUrl || existing.downloadUrl !== downloadUrl);
-        const shouldSkipDuplicate = existing?.status === status && !hasNewData;
+            downloadUrl && (!current?.downloadUrl || current.downloadUrl !== downloadUrl);
+        const shouldSkipDuplicate = current?.status === status && !hasNewData;

         if (shouldSkip || shouldSkipDuplicate) return;

         exportItems.set(exportData.$id, {
             status,
-            table: tableName ?? current?.table,
+            table: current?.table,

…base-[database]/table-[table]/export/+page.svelte

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@src/routes/`(console)/project-[region]-[project]/databases/database-[database]/table-[table]/export/+page.svelte:
- Around line 121-125: The catch block that calls addNotification({ type:
'error', message: error.message }) may assume the caught value is an Error;
update the handler used in the try/catch around the export logic so it safely
extracts a message (e.g., if (error instanceof Error) use error.message else use
String(error) or a default like 'Unknown error') before passing to
addNotification, ensuring the message field is always a string; locate the catch
and addNotification call in the export flow (the catch block shown) to implement
this safe fallback.
- Around line 70-73: In removeLocalFilter, after deleting the tag from
localQueries and before/after recreating the Map, detect if localQueries is
empty and if so set exportWithFilters = false so the checkbox is automatically
unchecked when the last filter is removed; update the function that references
localQueries (removeLocalFilter) to clear exportWithFilters when
localQueries.size === 0 to ensure the UI disables and unchecks the
export-with-filters checkbox.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/lib/components/csvExportBox.svelte`:
- Around line 34-41: The showErrorNotification function currently indexes
payload.errors[0] without confirming payload.errors exists which can throw;
update showErrorNotification to defensively check that payload.errors is an
array and has at least one element before attempting to parse or read
payload.errors[0] (e.g., use Array.isArray(payload.errors) &&
payload.errors.length > 0), and fall back to the default errorMessage when that
check fails; ensure both the try and catch branches reference the guarded value
so neither JSON.parse nor the fallback reads an undefined index.
♻️ Duplicate comments (3)
src/lib/components/csvExportBox.svelte (3)

91-105: Auto-open download still risks popup blockers; prefer user-gesture only.


132-144: Avoid {@html} with unescaped tableName (XSS risk).


146-156: Handle migrations.list errors to avoid silent failures.

🧹 Nitpick comments (2)
src/lib/components/csvExportBox.svelte (2)

1-10: Align component filename/location with PascalCase + feature folder.

Current file path src/lib/components/csvExportBox.svelte is camelCase and not in a feature subdirectory. Please rename to PascalCase and place it under a feature folder (e.g., src/lib/components/csv/CsvExportBox.svelte) and update imports/exports accordingly. As per coding guidelines and learnings, ...


181-189: Consider adding type="button" to header controls.

Avoid unintended form submissions if this component is ever rendered inside a <form>.

♻️ Suggested tweak
-                <button
+                <button
+                    type="button"
                     class="upload-box-button"
                     class:is-open={isOpen}
                     aria-label="toggle upload box"
                     onclick={() => (isOpen = !isOpen)}>
@@
-                <button class="upload-box-button" aria-label="close export box" onclick={clear}>
+                <button
+                    type="button"
+                    class="upload-box-button"
+                    aria-label="close export box"
+                    onclick={clear}>

Comment on lines +34 to +41
async function showErrorNotification(payload: Payload) {
let errorMessage = 'Export failed. Please try again.';
try {
const parsed = JSON.parse(payload.errors[0]);
errorMessage = parsed?.message || errorMessage;
} catch {
errorMessage = payload.errors[0] || errorMessage;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against missing payload.errors before indexing.

If payload.errors is undefined, the catch block still accesses payload.errors[0] and can throw. Add a defensive guard.

💡 Suggested fix
 async function showErrorNotification(payload: Payload) {
     let errorMessage = 'Export failed. Please try again.';
+    const rawError = payload.errors?.[0];
     try {
-        const parsed = JSON.parse(payload.errors[0]);
-        errorMessage = parsed?.message || errorMessage;
+        if (rawError) {
+            const parsed = JSON.parse(rawError);
+            errorMessage = parsed?.message || rawError || errorMessage;
+        }
     } catch {
-        errorMessage = payload.errors[0] || errorMessage;
+        errorMessage = rawError || errorMessage;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function showErrorNotification(payload: Payload) {
let errorMessage = 'Export failed. Please try again.';
try {
const parsed = JSON.parse(payload.errors[0]);
errorMessage = parsed?.message || errorMessage;
} catch {
errorMessage = payload.errors[0] || errorMessage;
}
async function showErrorNotification(payload: Payload) {
let errorMessage = 'Export failed. Please try again.';
const rawError = payload.errors?.[0];
try {
if (rawError) {
const parsed = JSON.parse(rawError);
errorMessage = parsed?.message || rawError || errorMessage;
}
} catch {
errorMessage = rawError || errorMessage;
}
🤖 Prompt for AI Agents
In `@src/lib/components/csvExportBox.svelte` around lines 34 - 41, The
showErrorNotification function currently indexes payload.errors[0] without
confirming payload.errors exists which can throw; update showErrorNotification
to defensively check that payload.errors is an array and has at least one
element before attempting to parse or read payload.errors[0] (e.g., use
Array.isArray(payload.errors) && payload.errors.length > 0), and fall back to
the default errorMessage when that check fails; ensure both the try and catch
branches reference the guarded value so neither JSON.parse nor the fallback
reads an undefined index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants