diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts
index cfd42ae8f2..3c8b068b7f 100644
--- a/src/lib/actions/analytics.ts
+++ b/src/lib/actions/analytics.ts
@@ -198,7 +198,16 @@ export enum Click {
WebsiteOpenClick = 'click_open_website',
CopyPromptStarterKitClick = 'click_copy_prompt_starter_kit',
OpenInCursorClick = 'click_open_in_cursor',
- OpenInLovableClick = 'click_open_in_lovable'
+ OpenInLovableClick = 'click_open_in_lovable',
+ RowCopyUrl = 'click_row_copy_url',
+ RowCopyJson = 'click_row_copy_json',
+ RowCopySnippet = 'click_row_copy_snippet',
+ RowContextMenuOpen = 'click_row_context_menu_open',
+ RowUpdate = 'click_row_update',
+ RowDuplicate = 'click_row_duplicate',
+ RowDelete = 'click_row_delete',
+ RowPermissions = 'click_row_permissions',
+ RowActivity = 'click_row_activity'
}
export enum Submit {
diff --git a/src/lib/components/copySnippetModal.svelte b/src/lib/components/copySnippetModal.svelte
new file mode 100644
index 0000000000..c6c10c8da9
--- /dev/null
+++ b/src/lib/components/copySnippetModal.svelte
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+ {#key language}
+
+
+ Get row
+
+
+
+
+ List rows
+
+
+
+
+ Update row
+
+
+
+
+ Delete row
+
+
+
+ {/key}
+
diff --git a/src/lib/elements/forms/inputSelect.svelte b/src/lib/elements/forms/inputSelect.svelte
index de977bfc14..6341dbe874 100644
--- a/src/lib/elements/forms/inputSelect.svelte
+++ b/src/lib/elements/forms/inputSelect.svelte
@@ -20,6 +20,7 @@
badge?: string;
}[];
export let leadingIcon: ComponentType | undefined = undefined;
+ export let showLabel: boolean = true;
let error: string;
@@ -47,6 +48,7 @@
import { base } from '$app/paths';
+ import { goto } from '$app/navigation';
import { page } from '$app/state';
import type { PageData } from './$types';
import { showSubNavigation } from '$lib/stores/layout';
@@ -23,12 +24,21 @@
IconTable
} from '@appwrite.io/pink-icons-svelte';
import { isTabletViewport } from '$lib/stores/viewport';
- import { BottomSheet } from '$lib/components';
+ import { BottomSheet, Confirm } from '$lib/components';
import Button from '$lib/elements/forms/button.svelte';
import { type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { subNavigation } from '$lib/stores/database';
+ import TableContextMenu, { type TableAction } from './tableContextMenu.svelte';
+ import CopySnippetModal from '$lib/components/copySnippetModal.svelte';
+ import { addNotification } from '$lib/stores/notifications';
+ import { trackEvent, trackError, Submit } from '$lib/actions/analytics';
+ import { isCsvImportInProgress } from './table-[table]/store';
+ import FilePicker from '$lib/components/filePicker.svelte';
+
+ import { preferences } from '$lib/stores/preferences';
+ import { organization } from '$lib/stores/organization';
const data = $derived(page.data) as PageData;
@@ -45,10 +55,25 @@
tables: []
});
- const sortedTables = $derived.by(() =>
- tables?.tables?.slice().sort((a, b) => a.name.localeCompare(b.name))
+ const pinnedTablesKey = $derived(`pinned_tables_${databaseId}`);
+ const pinnedTableIds = $derived(
+ ($preferences.miscellaneous?.[pinnedTablesKey]?.toString()?.split(',') ?? []).filter(
+ Boolean
+ )
);
+ const sortedTables = $derived.by(() => {
+ return tables?.tables?.slice().sort((a, b) => {
+ const aPinned = pinnedTableIds.includes(a.$id);
+ const bPinned = pinnedTableIds.includes(b.$id);
+
+ if (aPinned && !bPinned) return -1;
+ if (!aPinned && bPinned) return 1;
+
+ return a.name.localeCompare(b.name);
+ });
+ });
+
const selectedTable = $derived.by(() =>
sortedTables?.find((table: Models.Table) => table.$id === tableId)
);
@@ -84,6 +109,122 @@
openBottomSheet = false;
}
}
+
+ let showCopySnippetModal = $state(false);
+ let showImportCSV = $state(false);
+ let selectedTableForAction = $state(null);
+
+ async function onSelectFile(file: Models.File, localFile = false) {
+ if (!selectedTableForAction) return;
+
+ $isCsvImportInProgress = true;
+
+ try {
+ await sdk.forProject(region, project).migrations.createCSVImport({
+ bucketId: file.bucketId,
+ fileId: file.$id,
+ resourceId: `${databaseId}:${selectedTableForAction.$id}`,
+ internalFile: localFile
+ });
+
+ addNotification({
+ type: 'success',
+ message: 'Rows import from csv has started'
+ });
+
+ trackEvent(Submit.DatabaseImportCsv);
+ } catch (e) {
+ trackError(e, Submit.DatabaseImportCsv);
+ addNotification({
+ type: 'error',
+ message: e.message
+ });
+ } finally {
+ $isCsvImportInProgress = false;
+ }
+ }
+
+ let showDelete = $state(false);
+ let deleteError = $state();
+
+ async function deleteTable() {
+ if (!selectedTableForAction) return;
+ try {
+ await sdk.forProject(region, project).tablesDB.deleteTable({
+ databaseId,
+ tableId: selectedTableForAction.$id
+ });
+
+ showDelete = false;
+ subNavigation.update();
+
+ addNotification({
+ type: 'success',
+ message: `${selectedTableForAction.name} has been deleted`
+ });
+
+ trackEvent(Submit.TableDelete);
+
+ await preferences.deleteTableDetails($organization.$id, selectedTableForAction.$id);
+
+ if (tableId === selectedTableForAction.$id) {
+ await goto(`${base}/project-${region}-${project}/databases/database-${databaseId}`);
+ }
+ } catch (e) {
+ deleteError = e.message;
+ trackError(e, Submit.TableDelete);
+ }
+ }
+
+ async function handleTableAction(action: TableAction, table: Models.Table) {
+ selectedTableForAction = table;
+ switch (action) {
+ case 'copy-snippet':
+ showCopySnippetModal = true;
+ break;
+ case 'upload-csv':
+ showImportCSV = true;
+ break;
+ case 'copy-url':
+ await navigator.clipboard.writeText(
+ `${window.location.origin}${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`
+ );
+ addNotification({
+ type: 'success',
+ message: 'URL copied to clipboard'
+ });
+ break;
+ case 'copy-json':
+ await navigator.clipboard.writeText(JSON.stringify(table, null, 2));
+ addNotification({
+ type: 'success',
+ message: 'JSON copied to clipboard'
+ });
+ break;
+ case 'pin': {
+ const isPinned = pinnedTableIds.includes(table.$id);
+ const newPinnedIds = isPinned
+ ? pinnedTableIds.filter((id) => id !== table.$id)
+ : [...pinnedTableIds, table.$id];
+ await preferences.setKey(pinnedTablesKey, newPinnedIds.join(','));
+ addNotification({
+ type: 'success',
+ message: `Table ${isPinned ? 'unpinned' : 'pinned'}`
+ });
+ break;
+ }
+ case 'permissions':
+ await goto(
+ `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}/settings`
+ );
+ break;
+ case 'delete':
+ showDelete = true;
+ break;
+ default:
+ break;
+ }
+ }
@@ -122,23 +263,27 @@
{@const isLast = index === sortedTables.length - 1}
-
-
-
- {table.name}
-
-
+ handleTableAction(action, table)}
+ isPinned={pinnedTableIds.includes(table.$id)}>
+
+
+
+ {table.name}
+
+
+
{/each}
@@ -254,6 +399,37 @@
}}>
{/if}
+
+
+{#if showImportCSV}
+
+{/if}
+
+
+
+ Are you sure you want to delete {selectedTableForAction?.name}?
+
+
+
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte
index 3fdf50d14a..d26e006610 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/sheetOptions.svelte
@@ -16,7 +16,7 @@
| 'activity'
| 'copy-url'
| 'copy-json'
- // | 'copy-snippet'
+ | 'copy-snippet'
| 'delete';
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
index dabc3955cf..a3ef8a27b5 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte
@@ -1,7 +1,7 @@
+
+
+
+
+
+
+
+
+
+