Skip to content

Commit 833563e

Browse files
authored
Merge pull request #2401 from appwrite/feat-SER-290-Improve-csv-import-error-flow
2 parents dedf77f + eba8b09 commit 833563e

2 files changed

Lines changed: 111 additions & 45 deletions

File tree

src/lib/components/csvImportBox.svelte

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,25 @@
66
import { Dependencies } from '$lib/constants';
77
import { goto, invalidate } from '$app/navigation';
88
import { getProjectId } from '$lib/helpers/project';
9-
import { writable, type Writable } from 'svelte/store';
109
import { addNotification } from '$lib/stores/notifications';
11-
import { Layout, Typography } from '@appwrite.io/pink-svelte';
10+
import { Layout, Typography, Icon } from '@appwrite.io/pink-svelte';
11+
import { IconExclamationCircle } from '@appwrite.io/pink-icons-svelte';
12+
import { Modal, Code } from '$lib/components';
1213
import { type Models, type Payload, Query } from '@appwrite.io/console';
1314
1415
// re-render the key for sheet UI.
1516
import { hash } from '$lib/helpers/string';
1617
import { spreadsheetRenderKey } from '$routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store';
18+
import { Link } from '$lib/elements';
19+
20+
type CsvImportError = {
21+
[key: string]: number | string | null;
22+
};
1723
1824
type ImportItem = {
1925
status: string;
2026
table?: string;
27+
errors?: string[];
2128
};
2229
2330
type ImportItemsMap = Map<string, ImportItem>;
@@ -28,7 +35,7 @@
2835
* The structure is as follows -
2936
* `{ migrationId: { status: status, table: table } }`
3037
*/
31-
const importItems: Writable<ImportItemsMap> = writable(new Map());
38+
let importItems = $state<ImportItemsMap>(new Map());
3239
3340
async function showCompletionNotification(database: string, table: string, payload: Payload) {
3441
const isSuccess = payload.status === 'completed';
@@ -37,13 +44,9 @@
3744
if (!isSuccess && !isError) return;
3845
3946
let errorMessage = 'Import failed. Check your CSV for correct fields and required values.';
40-
if (isError && Array.isArray(payload.errors)) {
41-
try {
42-
// the `errors` is a list of json encoded string.
43-
errorMessage = JSON.parse(payload.errors[0]).message;
44-
} catch {
45-
// do nothing, fallback to default message.
46-
}
47+
const errors = getErrors(payload);
48+
if (errors) {
49+
errorMessage = extractErrorMessage(errors);
4750
}
4851
4952
const type = isSuccess ? 'success' : 'error';
@@ -73,7 +76,7 @@
7376
const resourceId = importData.resourceId ?? '';
7477
const [databaseId, tableId] = resourceId.split(':') ?? [];
7578
76-
const current = $importItems.get(importData.$id);
79+
const current = importItems.get(importData.$id);
7780
let tableName = current?.table ?? null;
7881
7982
if (!tableName && tableId) {
@@ -91,41 +94,55 @@
9194
}
9295
9396
if (tableId && tableName === null) {
94-
importItems.update((items) => {
95-
const next = new Map(items);
96-
next.delete(importData.$id);
97-
return next;
98-
});
97+
const next = new Map(importItems);
98+
next.delete(importData.$id);
99+
importItems = next;
99100
return;
100101
}
101102
102-
importItems.update((items) => {
103-
const existing = items.get(importData.$id);
104-
105-
const isDone = (s: string) => s === 'completed' || s === 'failed';
106-
const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s);
103+
const existing = importItems.get(importData.$id);
107104
108-
const shouldSkip =
109-
(existing && isDone(existing.status) && isInProgress(status)) ||
110-
existing?.status === status;
105+
const isDone = (s: string) => s === 'completed' || s === 'failed';
106+
const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s);
111107
112-
if (shouldSkip) return items;
108+
const shouldSkip =
109+
(existing && isDone(existing.status) && isInProgress(status)) ||
110+
existing?.status === status;
113111
114-
const next = new Map(items);
115-
next.set(importData.$id, { status, table: tableName ?? undefined });
116-
return next;
117-
});
112+
if (!shouldSkip) {
113+
const next = new Map(importItems);
114+
const errors = getErrors(importData);
115+
next.set(importData.$id, { status, table: tableName ?? undefined, errors });
116+
importItems = next;
117+
}
118118
119119
if (status === 'completed' || status === 'failed') {
120120
await showCompletionNotification(databaseId, tableId, importData);
121121
}
122122
}
123123
124124
function clear() {
125-
importItems.update((items) => {
126-
items.clear();
127-
return items;
128-
});
125+
importItems = new Map();
126+
}
127+
128+
function getErrors(importData: Payload | Models.Migration): string[] | undefined {
129+
return Array.isArray(importData.errors) ? importData.errors : undefined;
130+
}
131+
132+
function parseError(error: string): string | CsvImportError {
133+
try {
134+
return JSON.parse(error) as CsvImportError;
135+
} catch {
136+
return error;
137+
}
138+
}
139+
140+
function extractErrorMessage(errors: string[]): string {
141+
try {
142+
return JSON.parse(errors[0]).message;
143+
} catch {
144+
return 'Import failed. Check your CSV for correct fields and required values.';
145+
}
129146
}
130147
131148
function graphSize(status: string): number {
@@ -148,8 +165,9 @@
148165
const name = collectionName ? `<b>${collectionName}</b>` : '';
149166
switch (status) {
150167
case 'completed':
168+
return `CSV import completed${name ? ` to ${name}` : ''}`;
151169
case 'failed':
152-
return `Import to ${name} ${status}`;
170+
return `CSV import failed${name ? ` to ${name}` : ''}`;
153171
case 'processing':
154172
return `Importing CSV file${name ? ` to ${name}` : ''}`;
155173
default:
@@ -177,8 +195,18 @@
177195
});
178196
});
179197
180-
$: isOpen = true;
181-
$: showCsvImportBox = $importItems.size > 0;
198+
let isOpen = $state(true);
199+
let showCsvImportBox = $derived(importItems.size > 0);
200+
201+
let showDetails = $state(false);
202+
let selectedErrors = $state<string[]>([]);
203+
let parsedErrors = $state<Array<string | CsvImportError>>([]);
204+
205+
function openDetails(errors: string[] | undefined) {
206+
selectedErrors = errors ?? [];
207+
parsedErrors = selectedErrors.map(parseError);
208+
showDetails = true;
209+
}
182210
</script>
183211

184212
{#if showCsvImportBox}
@@ -187,26 +215,23 @@
187215
<header class="upload-box-header">
188216
<h4 class="upload-box-title">
189217
<Typography.Text variant="m-500">
190-
Importing rows ({$importItems.size})
218+
Importing rows ({importItems.size})
191219
</Typography.Text>
192220
</h4>
193221
<button
194222
class="upload-box-button"
195223
class:is-open={isOpen}
196224
aria-label="toggle upload box"
197-
on:click={() => (isOpen = !isOpen)}>
225+
onclick={() => (isOpen = !isOpen)}>
198226
<span class="icon-cheveron-up" aria-hidden="true"></span>
199227
</button>
200-
<button
201-
class="upload-box-button"
202-
aria-label="close backup restore box"
203-
on:click={clear}>
228+
<button class="upload-box-button" aria-label="close CSV import box" onclick={clear}>
204229
<span class="icon-x" aria-hidden="true"></span>
205230
</button>
206231
</header>
207232

208233
<div class="upload-box-content-list">
209-
{#each [...$importItems.entries()] as [key, value] (key)}
234+
{#each [...importItems.entries()] as [key, value] (key)}
210235
<div class="upload-box-content" class:is-open={isOpen}>
211236
<ul class="upload-box-list">
212237
<li class="upload-box-item">
@@ -222,6 +247,25 @@
222247
class:is-danger={value.status === 'failed'}
223248
style="--graph-size:{graphSize(value.status)}%">
224249
</div>
250+
{#if value.status === 'failed'}
251+
<Layout.Stack
252+
direction="row"
253+
gap="xs"
254+
alignItems="center"
255+
inline>
256+
<Icon
257+
icon={IconExclamationCircle}
258+
color="--fgcolor-error"
259+
size="s" />
260+
<Typography.Text color="--fgcolor-error">
261+
There was an import issue.
262+
<Link
263+
style="color: inherit"
264+
onclick={() => openDetails(value.errors)}
265+
>View details</Link>
266+
</Typography.Text>
267+
</Layout.Stack>
268+
{/if}
225269
</section>
226270
</li>
227271
</ul>
@@ -232,6 +276,18 @@
232276
</Layout.Stack>
233277
{/if}
234278

279+
<Modal title="Import error" bind:show={showDetails} hideFooter>
280+
<Layout.Stack gap="m">
281+
<Layout.Stack>
282+
<Code
283+
language="json"
284+
code={JSON.stringify(parsedErrors, null, 2)}
285+
withCopy
286+
allowScroll />
287+
</Layout.Stack>
288+
</Layout.Stack>
289+
</Modal>
290+
235291
<style lang="scss">
236292
.upload-box {
237293
display: flex;
@@ -252,7 +308,7 @@
252308
}
253309
254310
.upload-box-content {
255-
width: 304px;
311+
width: 324px;
256312
}
257313
258314
.upload-box-button {

src/lib/elements/link.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
{#if href}
3131
<Link.Anchor
32+
{...$$restProps}
3233
on:click
3334
on:mousedown
3435
on:click={track}
@@ -42,7 +43,16 @@
4243
<slot />
4344
</Link.Anchor>
4445
{:else}
45-
<Link.Button on:click on:mousedown on:click={track} {type} {disabled} {variant} {size} {icon}>
46+
<Link.Button
47+
{...$$restProps}
48+
on:click
49+
on:mousedown
50+
on:click={track}
51+
{type}
52+
{disabled}
53+
{variant}
54+
{size}
55+
{icon}>
4656
<slot />
4757
</Link.Button>
4858
{/if}

0 commit comments

Comments
 (0)