diff --git a/src/lib/components/csvImportBox.svelte b/src/lib/components/csvImportBox.svelte index ac8561019..d0b3ac032 100644 --- a/src/lib/components/csvImportBox.svelte +++ b/src/lib/components/csvImportBox.svelte @@ -6,18 +6,25 @@ import { Dependencies } from '$lib/constants'; import { goto, invalidate } from '$app/navigation'; import { getProjectId } from '$lib/helpers/project'; - import { writable, type Writable } from 'svelte/store'; import { addNotification } from '$lib/stores/notifications'; - import { Layout, Typography } from '@appwrite.io/pink-svelte'; + import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte'; + import { IconExclamationCircle } from '@appwrite.io/pink-icons-svelte'; + import { Modal, Code } from '$lib/components'; import { type Models, type Payload, Query } from '@appwrite.io/console'; // re-render the key for sheet UI. import { hash } from '$lib/helpers/string'; import { spreadsheetRenderKey } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store'; + import { Link } from '$lib/elements'; + + type CsvImportError = { + [key: string]: number | string | null; + }; type ImportItem = { status: string; table?: string; + errors?: string[]; }; type ImportItemsMap = Map; @@ -28,7 +35,7 @@ * The structure is as follows - * `{ migrationId: { status: status, table: table } }` */ - const importItems: Writable = writable(new Map()); + let importItems = $state(new Map()); async function showCompletionNotification(database: string, table: string, payload: Payload) { const isSuccess = payload.status === 'completed'; @@ -37,13 +44,9 @@ if (!isSuccess && !isError) return; let errorMessage = 'Import failed. Check your CSV for correct fields and required values.'; - if (isError && Array.isArray(payload.errors)) { - try { - // the `errors` is a list of json encoded string. - errorMessage = JSON.parse(payload.errors[0]).message; - } catch { - // do nothing, fallback to default message. - } + const errors = getErrors(payload); + if (errors) { + errorMessage = extractErrorMessage(errors); } const type = isSuccess ? 'success' : 'error'; @@ -73,7 +76,7 @@ const resourceId = importData.resourceId ?? ''; const [databaseId, tableId] = resourceId.split(':') ?? []; - const current = $importItems.get(importData.$id); + const current = importItems.get(importData.$id); let tableName = current?.table ?? null; if (!tableName && tableId) { @@ -91,30 +94,27 @@ } if (tableId && tableName === null) { - importItems.update((items) => { - const next = new Map(items); - next.delete(importData.$id); - return next; - }); + const next = new Map(importItems); + next.delete(importData.$id); + importItems = next; return; } - importItems.update((items) => { - const existing = items.get(importData.$id); + const existing = importItems.get(importData.$id); - const isDone = (s: string) => s === 'completed' || s === 'failed'; - const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s); + const isDone = (s: string) => s === 'completed' || s === 'failed'; + const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s); - const shouldSkip = - (existing && isDone(existing.status) && isInProgress(status)) || - existing?.status === status; + const shouldSkip = + (existing && isDone(existing.status) && isInProgress(status)) || + existing?.status === status; - if (shouldSkip) return items; - - const next = new Map(items); - next.set(importData.$id, { status, table: tableName ?? undefined }); - return next; - }); + if (!shouldSkip) { + const next = new Map(importItems); + const errors = getErrors(importData); + next.set(importData.$id, { status, table: tableName ?? undefined, errors }); + importItems = next; + } if (status === 'completed' || status === 'failed') { await showCompletionNotification(databaseId, tableId, importData); @@ -122,10 +122,27 @@ } function clear() { - importItems.update((items) => { - items.clear(); - return items; - }); + importItems = new Map(); + } + + function getErrors(importData: Payload | Models.Migration): string[] | undefined { + return Array.isArray(importData.errors) ? importData.errors : undefined; + } + + function parseError(error: string): string | CsvImportError { + try { + return JSON.parse(error) as CsvImportError; + } catch { + return error; + } + } + + function extractErrorMessage(errors: string[]): string { + try { + return JSON.parse(errors[0]).message; + } catch { + return 'Import failed. Check your CSV for correct fields and required values.'; + } } function graphSize(status: string): number { @@ -148,8 +165,9 @@ const name = collectionName ? `${collectionName}` : ''; switch (status) { case 'completed': + return `CSV import completed${name ? ` to ${name}` : ''}`; case 'failed': - return `Import to ${name} ${status}`; + return `CSV import failed${name ? ` to ${name}` : ''}`; case 'processing': return `Importing CSV file${name ? ` to ${name}` : ''}`; default: @@ -177,8 +195,18 @@ }); }); - $: isOpen = true; - $: showCsvImportBox = $importItems.size > 0; + let isOpen = $state(true); + let showCsvImportBox = $derived(importItems.size > 0); + + let showDetails = $state(false); + let selectedErrors = $state([]); + let parsedErrors = $state>([]); + + function openDetails(errors: string[] | undefined) { + selectedErrors = errors ?? []; + parsedErrors = selectedErrors.map(parseError); + showDetails = true; + } {#if showCsvImportBox} @@ -187,26 +215,23 @@

- Importing rows ({$importItems.size}) + Importing rows ({importItems.size})

-
- {#each [...$importItems.entries()] as [key, value] (key)} + {#each [...importItems.entries()] as [key, value] (key)}
  • @@ -222,6 +247,25 @@ class:is-danger={value.status === 'failed'} style="--graph-size:{graphSize(value.status)}%">
+ {#if value.status === 'failed'} + + + + There was an import issue. + openDetails(value.errors)} + >View details + + + {/if} @@ -232,6 +276,18 @@ {/if} + + + + + + + +