diff --git a/package.json b/package.json index 557835f92..5a2e77773 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "@ai-sdk/svelte": "^1.1.24", "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c7ed1e60..82b9a6afc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,14 +18,14 @@ importers: specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3709,7 +3709,7 @@ snapshots: dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@85d1b43(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3722,7 +3722,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@85d1b43(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 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} + + + + + + + +