diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte index b8ab6faf31..07f9bd7819 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte @@ -47,14 +47,21 @@ import { page } from '$app/state'; import { base } from '$app/paths'; import { canWriteTables } from '$lib/stores/roles'; - import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte'; + import { + IconChevronDown, + IconChevronUp, + IconEye, + IconLockClosed, + IconPlus, + IconPuzzle + } from '@appwrite.io/pink-icons-svelte'; import SideSheet from './layout/sidesheet.svelte'; import EditRow from './rows/edit.svelte'; import EditRelatedRow from './rows/editRelated.svelte'; import EditColumn from './columns/edit.svelte'; import RowActivity from './rows/activity.svelte'; import EditRowPermissions from './rows/editPermissions.svelte'; - import { Dialog, Layout, Typography, Selector } from '@appwrite.io/pink-svelte'; + import { Dialog, Layout, Typography, Selector, Icon } from '@appwrite.io/pink-svelte'; import { Button, Seekbar } from '$lib/elements/forms'; import { generateFakeRecords, generateColumns } from '$lib/helpers/faker'; import { addNotification } from '$lib/stores/notifications'; @@ -65,6 +72,7 @@ import { chunks } from '$lib/helpers/array'; import { Submit, trackEvent } from '$lib/actions/analytics'; + import { isTabletViewport } from '$lib/stores/viewport'; import IndexesSuggestions from '../(suggestions)/indexes.svelte'; let editRow: EditRow; @@ -78,6 +86,33 @@ let columnCreationHandler: ((response: RealtimeResponse) => void) | null = null; + // manual management of focus is needed! + const autoFocusAction = (node: HTMLElement, shouldFocus: boolean) => { + const button = node.querySelector('button'); + if (!button) return; + + const handleBlur = () => button.classList.remove('focus-visible'); + const applyFocus = (focus: boolean) => { + if (focus) { + button.classList.add('focus-visible'); + button.focus(); + } else { + button.classList.remove('focus-visible'); + } + }; + + button.addEventListener('blur', handleBlur); + applyFocus(shouldFocus); + + return { + update: applyFocus, + destroy() { + button.removeEventListener('blur', handleBlur); + button.classList.remove('focus-visible'); + } + }; + }; + onMount(() => { expandTabs.set(preferences.getKey('tableHeaderExpanded', true)); @@ -448,11 +483,63 @@ show: !!currentRowId, value: buildRowUrl(currentRowId) }}> + {#snippet topEndActions()} + {@const rows = $databaseRowSheetOptions.rows ?? []} + {@const currentIndex = $databaseRowSheetOptions.rowIndex ?? -1} + {@const isFirstRow = currentIndex <= 0} + {@const isLastRow = currentIndex >= rows.length - 1} + + {#if !$isTabletViewport} + {@const shouldFocusPrev = !$databaseRowSheetOptions.autoFocus && !isFirstRow} + {@const shouldFocusNext = + !$databaseRowSheetOptions.autoFocus && isFirstRow && !isLastRow} + +
+ +
+ +
+ +
+ {/if} + {/snippet} + {#key currentRowId} + bind:rowId={$databaseRowSheetOptions.rowId} + autoFocus={$databaseRowSheetOptions.autoFocus} /> {/key} @@ -522,3 +609,10 @@ + + diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte index c4d8041b2e..afe51be679 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte @@ -18,6 +18,7 @@ footer = null, titleBadge = null, topAction = null, + topEndActions = null, ...restProps }: { show: boolean; @@ -48,7 +49,8 @@ } | undefined; children?: Snippet; - footer?: Snippet | null; + footer?: Snippet; + topEndActions?: Snippet; } & HTMLAttributes = $props(); let form: Form; @@ -88,6 +90,12 @@ {/if} {/if} + + {#if topEndActions} + + {@render topEndActions()} + + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte index 03b95bea6c..ab3879962d 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte @@ -10,19 +10,27 @@ import { invalidate } from '$app/navigation'; import { table, type Columns, PROHIBITED_ROW_KEYS } from '../store'; import ColumnItem from './columns/columnItem.svelte'; - import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store'; + import { + buildWildcardColumnsQuery, + isRelationship, + isRelationshipToMany, + isSpatialType + } from './store'; import { Layout, Skeleton } from '@appwrite.io/pink-svelte'; import { deepClone } from '$lib/helpers/object'; + import deepEqual from 'deep-equal'; const tableId = page.params.table; const databaseId = page.params.database; let { row = $bindable(), - rowId = $bindable(null) + rowId = $bindable(null), + autoFocus = true }: { row?: Models.Row | null; rowId?: string | null; + autoFocus?: boolean; } = $props(); let loading = $state(false); @@ -76,7 +84,9 @@ $effect(() => { if (row) { work = initWork(); - requestAnimationFrame(() => focusFirstInput()); + if (autoFocus) { + requestAnimationFrame(() => focusFirstInput()); + } } else { work = null; } @@ -90,6 +100,10 @@ const workColumn = $work?.[column.key]; const currentColumn = $doc?.[column.key]; + if (isSpatialType(column)) { + return deepEqual(workColumn, currentColumn); + } + if (column.array) { return !symmetricDifference(Array.from(workColumn), Array.from(currentColumn)).length; } 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 5611b447fe..5e3428bd47 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 @@ -112,6 +112,9 @@ spreadsheetRenderKey.set(hash(Date.now().toString())); } + // create index map for O(1) row lookups, reactive! + $: rowIndexMap = new Map($paginatedRows.items.map((row, index) => [row.$id, index])); + const tableId = page.params.table; const databaseId = page.params.database; const organizationId = data.organization.$id ?? data.project.teamId; @@ -546,11 +549,14 @@ } else if (type === 'row') { if (action === 'update') { databaseRowSheetOptions.update((opts) => { + const rowIndex = rowIndexMap.get(row.$id) ?? -1; return { ...opts, row, + rowIndex, show: true, - title: 'Update row' + title: 'Update row', + rows: $paginatedRows.items }; }); } @@ -800,9 +806,10 @@ expandKbdShortcut="Cmd+Enter" on:expandKbdShortcut={({ detail }) => { const focusedRowId = detail.rowId; - const focusedRow = $rows.rows.find((row) => row.$id === focusedRowId); + const focusedRow = $paginatedRows.items.find((row) => row.$id === focusedRowId); previouslyFocusedElement = document.activeElement; + $databaseRowSheetOptions.autoFocus = false; onSelectSheetOption('update', null, 'row', focusedRow); }}> @@ -929,6 +936,7 @@ hide(); previouslyFocusedElement = document.activeElement; + $databaseRowSheetOptions.autoFocus = false; onSelectSheetOption( 'update', null, @@ -979,8 +987,10 @@ - onSelectSheetOption(option, null, 'row', row)} + onSelect={(option) => { + $databaseRowSheetOptions.autoFocus = true; + onSelectSheetOption(option, null, 'row', row); + }} onVisibilityChanged={(visible) => { canShowDatetimePopover = !visible; }}> @@ -1107,6 +1117,7 @@ rowColumn ); } else { + $databaseRowSheetOptions.autoFocus = true; onSelectSheetOption('update', null, 'row', row); } }} /> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts index 10db635a9b..6c6a284890 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts @@ -70,12 +70,18 @@ export const databaseRowSheetOptions = writable< DatabaseSheetOptions & { row: Models.Row; rowId?: string; + rows: Models.Row[]; + rowIndex?: number; + autoFocus?: boolean; } >({ title: null, show: false, row: null, - rowId: null // for loading from a given id + rowId: null, // for loading from a given id + rows: [], + rowIndex: -1, + autoFocus: true }); export const databaseRelatedRowSheetOptions = writable<