Merge pull request #2538 from appwrite/add-arrow-navs

This commit is contained in:
Darshan
2025-11-10 15:13:29 +05:30
committed by GitHub
5 changed files with 145 additions and 12 deletions
@@ -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}
<div use:autoFocusAction={shouldFocusPrev} class:nav-button-wrapper={shouldFocusPrev}>
<Button
icon
text
size="xs"
on:click={() => {
if (currentIndex > 0) {
databaseRowSheetOptions.update((opts) => ({
...opts,
row: rows[currentIndex - 1],
rowIndex: currentIndex - 1
}));
}
}}
disabled={isFirstRow}>
<Icon icon={IconChevronUp} />
</Button>
</div>
<div use:autoFocusAction={shouldFocusNext} class:nav-button-wrapper={shouldFocusNext}>
<Button
icon
text
size="xs"
on:click={() => {
if (currentIndex < rows.length - 1) {
databaseRowSheetOptions.update((opts) => ({
...opts,
row: rows[currentIndex + 1],
rowIndex: currentIndex + 1
}));
}
}}
disabled={isLastRow}>
<Icon icon={IconChevronDown} />
</Button>
</div>
{/if}
{/snippet}
{#key currentRowId}
<EditRow
bind:this={editRow}
bind:row={$databaseRowSheetOptions.row}
bind:rowId={$databaseRowSheetOptions.rowId} />
bind:rowId={$databaseRowSheetOptions.rowId}
autoFocus={$databaseRowSheetOptions.autoFocus} />
{/key}
</SideSheet>
@@ -522,3 +609,10 @@
</Dialog>
<IndexesSuggestions />
<style lang="scss">
// not the best solution but needed!
.nav-button-wrapper :global(button.focus-visible) {
outline: var(--border-width-l) solid var(--border-focus);
}
</style>
@@ -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<HTMLDivElement> = $props();
let form: Form;
@@ -88,6 +90,12 @@
{/if}
{/if}
</Layout.Stack>
{#if topEndActions}
<Layout.Stack direction="row" gap="xs" alignItems="center" inline>
{@render topEndActions()}
</Layout.Stack>
{/if}
</Layout.Stack>
</div>
@@ -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;
}
@@ -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);
}}>
<svelte:fragment slot="header" let:root>
@@ -929,6 +936,7 @@
hide();
previouslyFocusedElement =
document.activeElement;
$databaseRowSheetOptions.autoFocus = false;
onSelectSheetOption(
'update',
null,
@@ -979,8 +987,10 @@
<SheetOptions
type="row"
column={rowColumn}
onSelect={(option) =>
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);
}
}} />
@@ -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<