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 @@ + + + +
    + +
    + + + +