mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat(migrations): expose Fail/Skip/Overwrite radios for import flows
Reworks the import-options UI in three places to match the new OnDuplicate enum (Fail / Skip / Overwrite) shipped by the cloud worker and migration package: - Settings → Migrations wizard (Appwrite source): three-option radio group with Fail pre-selected. Wording reflects that overwrite/skip apply to the entire resource tree (databases, tables, columns, indexes, rows), not just rows. - Tables (TablesDB) CSV import: same three-option radio replacing the prior two checkboxes. Wording stays document-centric since CSV import is row-only. - Collections (DocumentsDB) JSON import: previously imported with no onDuplicate (silent default to fail). Now opens the same Import options dialog with Fail/Skip/Overwrite radios. Also bumps @appwrite.io/console SDK to https://pkg.vc/-/@appwrite/ @appwrite.io/console@341620a so the OnDuplicate enum exports match the merged backend (post 1.9.x). Renamed OnDuplicate.Upsert -> OnDuplicate.Overwrite throughout the console (the enum was renamed in upstream PR #11910 / migration 1.9.7; the prior reference compiled to undefined and silently defaulted the backend to Fail).
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
@@ -124,7 +124,7 @@
|
||||
|
||||
"@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="],
|
||||
|
||||
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95", { "dependencies": { "json-bigint": "1.0.0" } }],
|
||||
"@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a", { "dependencies": { "json-bigint": "1.0.0" } }],
|
||||
|
||||
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8f00f95",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@341620a",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
|
||||
+76
-6
@@ -5,7 +5,15 @@
|
||||
import type { Column, ColumnType } from '$lib/helpers/types';
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Icon,
|
||||
Layout,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Selector,
|
||||
Typography,
|
||||
Dialog
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import { page } from '$app/state';
|
||||
@@ -21,7 +29,7 @@
|
||||
IconUpload,
|
||||
IconDownload
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
import { OnDuplicate, type Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
@@ -49,7 +57,11 @@
|
||||
|
||||
let isRefreshing = $state(false);
|
||||
let showImportJson = $state(false);
|
||||
let showImportOptions = $state(false);
|
||||
let showCustomColumnsModal = $state(false);
|
||||
let importOnDuplicate: OnDuplicate = $state(OnDuplicate.Fail);
|
||||
let pendingFile: Models.File | null = $state(null);
|
||||
let pendingLocalFile = $state(false);
|
||||
|
||||
let columnsError: string = $state(null);
|
||||
let spreadsheet: SpreadSheet | null = $state(null);
|
||||
@@ -74,17 +86,28 @@
|
||||
return queryParam ? `${url}?query=${encodeURIComponent(queryParam)}` : url;
|
||||
}
|
||||
|
||||
async function onSelect(file: Models.File, localFile = false) {
|
||||
function onSelect(file: Models.File, localFile = false) {
|
||||
pendingFile = file;
|
||||
pendingLocalFile = localFile;
|
||||
importOnDuplicate = OnDuplicate.Fail;
|
||||
showImportOptions = true;
|
||||
}
|
||||
|
||||
async function startImport() {
|
||||
if (!pendingFile) return;
|
||||
|
||||
showImportOptions = false;
|
||||
$isCollectionsJsonImportInProgress = true;
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.migrations.createJSONImport({
|
||||
bucketId: file.bucketId,
|
||||
fileId: file.$id,
|
||||
bucketId: pendingFile.bucketId,
|
||||
fileId: pendingFile.$id,
|
||||
resourceId: `${page.params.database}:${page.params.collection}`,
|
||||
internalFile: localFile
|
||||
internalFile: pendingLocalFile,
|
||||
onDuplicate: importOnDuplicate
|
||||
});
|
||||
|
||||
addNotification({
|
||||
@@ -101,6 +124,7 @@
|
||||
});
|
||||
} finally {
|
||||
$isCollectionsJsonImportInProgress = false;
|
||||
pendingFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +367,52 @@
|
||||
}} />
|
||||
{/if}
|
||||
|
||||
<Dialog title="Import options" bind:open={showImportOptions}>
|
||||
<Layout.Stack gap="l">
|
||||
<Typography.Text variant="m-400">
|
||||
Choose how to handle documents that already exist in this collection.
|
||||
</Typography.Text>
|
||||
<Layout.Stack gap="m">
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Fail}
|
||||
label="Fail on duplicate (default)">
|
||||
<svelte:fragment slot="description">
|
||||
Import aborts on the first document with a matching ID.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Skip}
|
||||
label="Skip existing documents">
|
||||
<svelte:fragment slot="description">
|
||||
Documents with matching IDs will be silently skipped.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Overwrite}
|
||||
label="Overwrite existing documents">
|
||||
<svelte:fragment slot="description">
|
||||
Documents with matching IDs will be updated with the imported data.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
|
||||
<Button text on:click={() => (showImportOptions = false)}>Cancel</Button>
|
||||
<Button on:click={startImport}>Start import</Button>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
|
||||
<Modal
|
||||
title="Custom columns"
|
||||
bind:error={columnsError}
|
||||
|
||||
+42
-24
@@ -8,7 +8,16 @@
|
||||
import { Container } from '$lib/layout';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
|
||||
import { Dialog, Icon, Layout, Divider, Selector, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Dialog,
|
||||
Icon,
|
||||
Layout,
|
||||
Divider,
|
||||
Selector,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Link
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
tableColumns,
|
||||
@@ -34,7 +43,7 @@
|
||||
IconUpload,
|
||||
IconDownload
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { OnDuplicate, type Models } from '@appwrite.io/console';
|
||||
import CreateRow from '$database/table-[table]/rows/create.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { isCloud } from '$lib/system';
|
||||
@@ -57,8 +66,7 @@
|
||||
let isRefreshing = false;
|
||||
let showImportCSV = false;
|
||||
let showImportOptions = false;
|
||||
let importOverwrite = false;
|
||||
let importSkip = false;
|
||||
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
|
||||
let pendingFile: Models.File | null = null;
|
||||
let pendingLocalFile = false;
|
||||
|
||||
@@ -115,8 +123,7 @@
|
||||
function onSelect(file: Models.File, localFile = false) {
|
||||
pendingFile = file;
|
||||
pendingLocalFile = localFile;
|
||||
importOverwrite = false;
|
||||
importSkip = false;
|
||||
importOnDuplicate = OnDuplicate.Fail;
|
||||
showImportOptions = true;
|
||||
}
|
||||
|
||||
@@ -134,8 +141,7 @@
|
||||
fileId: pendingFile.$id,
|
||||
resourceId: `${page.params.database}:${page.params.table}`,
|
||||
internalFile: pendingLocalFile,
|
||||
overwrite: importOverwrite,
|
||||
skip: importSkip
|
||||
onDuplicate: importOnDuplicate
|
||||
});
|
||||
|
||||
addNotification({
|
||||
@@ -459,24 +465,36 @@
|
||||
Choose how to handle documents that already exist in this table.
|
||||
</Typography.Text>
|
||||
<Layout.Stack gap="m">
|
||||
<Selector.Checkbox
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
checked={importOverwrite}
|
||||
on:change={(e) => {
|
||||
importOverwrite = e.detail;
|
||||
if (e.detail) importSkip = false;
|
||||
}}
|
||||
label="Overwrite existing documents"
|
||||
description="Documents with matching IDs will be updated with the imported data." />
|
||||
<Selector.Checkbox
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Fail}
|
||||
label="Fail on duplicate (default)">
|
||||
<svelte:fragment slot="description">
|
||||
Migration aborts on the first row with a matching ID.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
checked={importSkip}
|
||||
on:change={(e) => {
|
||||
importSkip = e.detail;
|
||||
if (e.detail) importOverwrite = false;
|
||||
}}
|
||||
label="Skip existing documents"
|
||||
description="Documents with matching IDs will be silently skipped." />
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Skip}
|
||||
label="Skip existing documents">
|
||||
<svelte:fragment slot="description">
|
||||
Documents with matching IDs will be silently skipped.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Overwrite}
|
||||
label="Overwrite existing documents">
|
||||
<svelte:fragment slot="description">
|
||||
Documents with matching IDs will be updated with the imported data.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
+34
-20
@@ -11,6 +11,7 @@
|
||||
AppwriteMigrationResource,
|
||||
FirebaseMigrationResource,
|
||||
NHostMigrationResource,
|
||||
OnDuplicate,
|
||||
SupabaseMigrationResource
|
||||
} from '@appwrite.io/console';
|
||||
import { started } from '../stores';
|
||||
@@ -39,8 +40,7 @@
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let importOverwrite = false;
|
||||
let importSkip = false;
|
||||
let importOnDuplicate: OnDuplicate = OnDuplicate.Fail;
|
||||
|
||||
const onExit = () => {
|
||||
resetImportStores();
|
||||
@@ -51,8 +51,7 @@
|
||||
const resources = migrationFormToResources($formData, $provider.provider);
|
||||
|
||||
const importOptions = {
|
||||
overwrite: importOverwrite,
|
||||
skip: importSkip
|
||||
onDuplicate: importOnDuplicate
|
||||
};
|
||||
|
||||
switch ($provider.provider) {
|
||||
@@ -213,24 +212,39 @@
|
||||
{#if $formData.databases.root}
|
||||
<Fieldset legend="Import options">
|
||||
<Layout.Stack gap="m">
|
||||
<Selector.Checkbox
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
checked={importOverwrite}
|
||||
on:change={(e) => {
|
||||
importOverwrite = e.detail;
|
||||
if (e.detail) importSkip = false;
|
||||
}}
|
||||
label="Overwrite existing documents"
|
||||
description="Documents with matching IDs will be updated with the imported data." />
|
||||
<Selector.Checkbox
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Fail}
|
||||
label="Fail on duplicate (default)">
|
||||
<svelte:fragment slot="description">
|
||||
Migration aborts on the first existing resource (database,
|
||||
table, column, index, or row).
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
checked={importSkip}
|
||||
on:change={(e) => {
|
||||
importSkip = e.detail;
|
||||
if (e.detail) importOverwrite = false;
|
||||
}}
|
||||
label="Skip existing documents"
|
||||
description="Documents with matching IDs will be silently skipped." />
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Skip}
|
||||
label="Skip existing resources">
|
||||
<svelte:fragment slot="description">
|
||||
Existing resources are left untouched. Only resources missing on
|
||||
the destination are created.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
<Selector.Radio
|
||||
size="s"
|
||||
bind:group={importOnDuplicate}
|
||||
name="importOnDuplicate"
|
||||
value={OnDuplicate.Overwrite}
|
||||
label="Overwrite existing resources">
|
||||
<svelte:fragment slot="description">
|
||||
Existing resources are updated to match the source. Schema drift
|
||||
and row data are both reconciled.
|
||||
</svelte:fragment>
|
||||
</Selector.Radio>
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user