Skip to content

Commit bb65ce2

Browse files
committed
Merge branch 'main' into fix-realtime-logic
2 parents 6cd6310 + 8bc5fa2 commit bb65ce2

40 files changed

Lines changed: 2076 additions & 2351 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
"@ai-sdk/svelte": "^1.1.24",
2525
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e",
2626
"@appwrite.io/pink-icons": "0.25.0",
27-
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@50b60cc",
27+
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7",
2828
"@appwrite.io/pink-legacy": "^1.0.3",
29-
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@10305c4",
29+
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@46f65c7",
3030
"@faker-js/faker": "^9.9.0",
3131
"@popperjs/core": "^2.11.8",
3232
"@sentry/sveltekit": "^8.38.0",

pnpm-lock.yaml

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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 { realtime, sdk } from '$lib/stores/sdk';
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/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,5 @@ export { default as ExpirationInput } from './expirationInput.svelte';
8787
export { default as EstimatedCard } from './estimatedCard.svelte';
8888
export { default as SortButton, type SortDirection } from './sortButton.svelte';
8989
export { default as SendVerificationEmailModal } from './account/sendVerificationEmailModal.svelte';
90+
export { default as MultiSelectionTable } from './multiSelectTable.svelte';
91+
export * from './multiSelectTable.svelte';

0 commit comments

Comments
 (0)