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<