update: ID > $id

fix: displayNames logic <> preferences.
fix: edit related rows in the side sheet.
fix: dynamic height on empty sheet's overlay.
fix: filter checkbox and action item being a bit independent.
This commit is contained in:
Darshan
2025-08-18 16:10:49 +05:30
parent 5a1d39729f
commit bd6e156ca3
13 changed files with 488 additions and 230 deletions
+4 -1
View File
@@ -233,7 +233,10 @@
? false
: visibleRealColumns.length <= 1 && !column.hide}>
<Layout.Stack direction="row" gap="s">
<Selector.Checkbox size="s" checked={!column.hide} />
<Selector.Checkbox
size="s"
checked={!column.hide}
on:change={() => toggleColumn(column)} />
{column.title}
</Layout.Stack>
</ActionMenu.Item.Button>
+24 -18
View File
@@ -208,31 +208,37 @@ function createPreferences() {
loadTeamPrefs: loadTeamPreferences,
getDisplayNames: () => {
return preferences?.displayNames ?? {};
getDisplayNames: (tableId: string) => {
return teamPreferences?.displayNames?.[tableId];
},
setDisplayNames: async (tableId: string, names: TeamPreferences['names']) => {
await updateAndSync((n) => {
if (!n?.displayNames) {
n ??= {};
n.displayNames ??= {};
}
setDisplayNames: async (
orgId: string,
tableId: string,
displayNames: TeamPreferences['names']
) => {
if (!teamPreferences.displayNames) {
teamPreferences.displayNames = {};
}
n.displayNames[tableId] = names;
return n;
teamPreferences.displayNames[tableId] = displayNames;
await sdk.forConsole.teams.updatePrefs({
teamId: orgId,
prefs: teamPreferences
});
},
deleteDisplayNames: async (tableId: string) => {
await updateAndSync((n) => {
if (!n?.displayNames) {
n ??= {};
n.displayNames ??= {};
}
deleteDisplayNames: async (orgId: string, tableId: string) => {
if (!teamPreferences?.displayNames) {
return;
}
delete n.displayNames[tableId];
return n;
delete teamPreferences.displayNames[tableId];
await sdk.forConsole.teams.updatePrefs({
teamId: orgId,
prefs: teamPreferences
});
},
@@ -56,7 +56,8 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => {
});
}
preferences.loadTeamPrefs(project.teamId);
// should be awaited for `displayNames`!
await preferences.loadTeamPrefs(project.teamId);
if (isCloud && scopes.includes('billing.read')) {
loadFailedInvoices(project.teamId);
@@ -33,7 +33,8 @@
spreadsheetLoading,
rowActivitySheet,
spreadsheetRenderKey,
expandTabs
expandTabs,
databaseRelatedRowSheetOptions
} from './store';
import { addSubPanel, registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
import CreateColumn from './createColumn.svelte';
@@ -46,6 +47,7 @@
import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
import SideSheet from './layout/sidesheet.svelte';
import EditRow from './editRow.svelte';
import EditRelatedRow from './editRelatedRow.svelte';
import EditColumn from './columns/edit.svelte';
import RowActivity from './rowActivity.svelte';
import { Dialog, Layout, Typography } from '@appwrite.io/pink-svelte';
@@ -58,6 +60,8 @@
import { preferences } from '$lib/stores/preferences';
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
let createIndex: CreateIndex;
let createColumn: CreateColumn;
let selectedOption: Option['name'] = 'String';
@@ -357,6 +361,21 @@
<EditRow bind:row={$databaseRowSheetOptions.row} bind:this={editRow} />
</SideSheet>
<SideSheet
closeOnBlur
title={$databaseRelatedRowSheetOptions.title}
bind:show={$databaseRelatedRowSheetOptions.show}
submit={{
text: 'Update',
disabled: editRelatedRow?.isDisabled(),
onClick: async () => await editRelatedRow?.update()
}}>
<EditRelatedRow
bind:this={editRelatedRow}
rowId={$databaseRelatedRowSheetOptions.rowId}
tableId={$databaseRelatedRowSheetOptions.tableId} />
</SideSheet>
<SideSheet
closeOnBlur
title="Create index"
@@ -0,0 +1,174 @@
<script lang="ts">
import { symmetricDifference } from '$lib/helpers/array';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { page } from '$app/state';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { type Writable, writable } from 'svelte/store';
import { type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { type Columns, PROHIBITED_ROW_KEYS } from './store';
import ColumnItem from './row-[row]/columnItem.svelte';
import {
buildWildcardColumnsQuery,
isRelationship,
isRelationshipToMany
} from './row-[row]/columns/store';
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
import { deepClone } from '$lib/helpers/object';
const databaseId = page.params.database;
let {
rowId,
tableId
}: {
rowId: string;
tableId: string;
} = $props();
let loading = $state(false);
let fetchedRow = $state<Models.Row | null>(null);
let relatedTable = $state<Models.Table | null>(null);
let work = $state<Writable<Models.Row> | null>(null);
let columnFormWrapper = $state<HTMLElement | null>(null);
async function loadRelatedRow() {
loading = true;
try {
relatedTable =
page.data.tables?.[tableId] ??
(await sdk.forProject(page.params.region, page.params.project).grids.getTable({
databaseId,
tableId: tableId
}));
fetchedRow = await sdk
.forProject(page.params.region, page.params.project)
.grids.getRow({
databaseId,
tableId: tableId,
rowId: rowId,
queries: buildWildcardColumnsQuery(relatedTable)
});
const filteredKeys = Object.keys(fetchedRow).filter((key) => {
return !PROHIBITED_ROW_KEYS.includes(key);
});
const workingData = filteredKeys.reduce((obj, key) => {
obj[key] = fetchedRow[key];
return obj;
}, {});
work = writable(deepClone(workingData as Models.Row));
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.RowUpdate);
} finally {
loading = false;
}
}
$effect(() => {
if (rowId && tableId) {
loadRelatedRow().then(() => {
focusFirstInput();
});
}
});
function compareColumns(column: Columns, $work: Models.Row, originalRow: Models.Row) {
if (!column) {
return false;
}
const workColumn = $work?.[column.key];
const currentColumn = originalRow?.[column.key];
if (column.array) {
return !symmetricDifference(Array.from(workColumn), Array.from(currentColumn)).length;
}
if (isRelationship(column)) {
if (isRelationshipToMany(column as Models.ColumnRelationship)) {
const workIds = workColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
const relatedIds = currentColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
return !symmetricDifference(workIds, relatedIds).length;
} else {
const workId = typeof workColumn === 'string' ? workColumn : workColumn?.$id;
const relatedId =
typeof currentColumn === 'string' ? currentColumn : currentColumn?.$id;
return workId === relatedId;
}
}
return workColumn === currentColumn;
}
export async function update() {
try {
await sdk.forProject(page.params.region, page.params.project).grids.updateRow({
databaseId,
tableId: relatedTable.$id,
rowId: fetchedRow.$id,
data: $work,
permissions: $work.$permissions
});
invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
addNotification({
message: 'Related row has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.RowUpdate);
}
}
export function isDisabled(): boolean {
if (!work || !relatedTable?.columns?.length || !fetchedRow) return true;
return relatedTable.columns.every((column) => compareColumns(column, $work, fetchedRow));
}
function focusFirstInput() {
const firstInput = columnFormWrapper?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
'input:not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly])'
);
firstInput?.focus({ preventScroll: true });
}
</script>
{#if loading}
<div style:margin-block="" style:margin-inline-end="2.25rem">
<Skeleton variant="line" height={40} width="auto" />
</div>
{:else if relatedTable?.columns?.length && work}
<div bind:this={columnFormWrapper}>
<Layout.Stack direction="column" gap="l">
{#each relatedTable.columns as column}
{@const label = column.key}
<ColumnItem {column} bind:formValues={$work} {label} editing />
{/each}
</Layout.Stack>
</div>
{/if}
@@ -8,7 +8,7 @@
import type { Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
import { invalidate } from '$app/navigation';
import { table, type Columns } from './store';
import { table, type Columns, PROHIBITED_ROW_KEYS } from './store';
import ColumnItem from './row-[row]/columnItem.svelte';
import { isRelationship, isRelationshipToMany } from './row-[row]/columns/store';
import { Layout } from '@appwrite.io/pink-svelte';
@@ -27,17 +27,8 @@
let columnFormWrapper = $state<HTMLElement | null>(null);
function initWork() {
const prohibitedKeys = [
'$id',
'$collection',
'$tableId',
'$databaseId',
'$createdAt',
'$updatedAt'
];
const filteredKeys = Object.keys(row).filter((key) => {
return !prohibitedKeys.includes(key);
return !PROHIBITED_ROW_KEYS.includes(key);
});
const result = filteredKeys.reduce((obj, key) => {
@@ -55,7 +46,7 @@
}
});
function compareAttributes(column: Columns, $work: Models.Row, $doc: Models.Row) {
function compareColumns(column: Columns, $work: Models.Row, $doc: Models.Row) {
if (!column) {
return false;
}
@@ -91,9 +82,13 @@
export async function update() {
try {
await sdk
.forProject(page.params.region, page.params.project)
.grids.updateRow(databaseId, tableId, row.$id, $work, $work.$permissions);
await sdk.forProject(page.params.region, page.params.project).grids.updateRow({
databaseId,
tableId,
rowId: row.$id,
data: $work,
permissions: $work.$permissions
});
invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
@@ -113,7 +108,7 @@
export function isDisabled(): boolean {
if (!work || !$table?.columns?.length) return true;
return $table.columns.every((attribute) => compareAttributes(attribute, $work, row));
return $table.columns.every((column) => compareColumns(column, $work, row));
}
function focusFirstInput() {
@@ -7,13 +7,7 @@
Spreadsheet,
Typography
} from '@appwrite.io/pink-svelte';
import {
IconCalendar,
IconFingerPrint,
IconHashtag,
IconPlus
} from '@appwrite.io/pink-icons-svelte';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { SortButton } from '$lib/components';
import type { Column } from '$lib/helpers/types';
@@ -24,6 +18,8 @@
spreadsheetLoading,
expandTabs
} from '../store';
import { onMount, tick } from 'svelte';
import SpreadsheetContainer from './spreadsheet.svelte';
type Mode = 'rows' | 'indexes';
@@ -50,8 +46,39 @@
};
}>();
let spreadsheetContainer: HTMLElement;
let headerElement: HTMLElement | null = null;
let dynamicOverlayHeight = $state('70.5vh');
let spreadsheetRootContainer: SpreadsheetContainer;
const baseColProps = { draggable: false, resizable: false };
const updateOverlayHeight = () => {
tick().then(() => {
spreadsheetRootContainer?.resizeSheet(false, true);
});
if (!spreadsheetContainer) return;
if (!headerElement || !headerElement.isConnected) {
headerElement = spreadsheetContainer.querySelector('[role="rowheader"]');
}
if (headerElement) {
const headerRect = headerElement.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dynamicHeight = viewportHeight - headerRect.bottom;
dynamicOverlayHeight = `${dynamicHeight}px`;
if (!$expandTabs) {
dynamicOverlayHeight = `calc(${dynamicHeight}px - 89px)`;
}
}
};
onMount(updateOverlayHeight);
const getCustomColumns = (): Column[] =>
customColumns.map((col: Column) => ({
...col,
@@ -61,18 +88,9 @@
}));
const getRowColumns = (): Column[] => [
{
id: '$sequence',
title: 'Sequence',
type: 'string',
width: 150,
isPrimary: true,
icon: IconHashtag,
...baseColProps
},
{
id: '$id',
title: 'ID',
title: '$id',
type: 'string',
width: 180,
icon: IconFingerPrint,
@@ -81,7 +99,7 @@
...getCustomColumns(),
{
id: '$createdAt',
title: 'Created',
title: '$createdAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
@@ -89,7 +107,7 @@
},
{
id: '$updatedAt',
title: 'Updated',
title: '$updatedAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
@@ -127,82 +145,89 @@
const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
const emptyCells = $derived($isSmallViewport ? 14 : 17);
const fixedHeight = $derived($isSmallViewport ? '60.75vh' : '74.35vh');
const fixedHeight = $derived($expandTabs ? 'calc(100% - 89px)' : '100%');
const emptyCells = $derived(($isSmallViewport ? 14 : 17) + (!$expandTabs ? 2 : 0));
</script>
<div class="spreadsheet-container-outer" data-mode={mode}>
<Spreadsheet.Root
{emptyCells}
allowSelection
height={fixedHeight}
columns={spreadsheetColumns}
loading={$spreadsheetLoading}
bottomActionClick={() => {
/* @ignore: only for showing the `+` button on footer */
}}>
<svelte:fragment slot="header" let:root>
{#each spreadsheetColumns as column (column.id)}
{@const columnActionsById = column.id === 'actions'}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
style:cursor={columnActionsById ? 'pointer' : null}
onclick={() => {
if (columnActionsById && mode === 'rows') {
$showCreateAttributeSheet.show = true;
$showCreateAttributeSheet.title = 'Create column';
$showCreateAttributeSheet.columns = $tableColumns;
$showCreateAttributeSheet.columnsOrder = $columnsOrder;
}
}}>
<Spreadsheet.Header.Cell
{root}
column={column.id}
icon={column.icon ?? undefined}>
{#if column.isAction}
<Button.Button
icon
variant="extra-compact"
on:click={() => {
console.log('dank');
}}>
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
{:else if column.id === 'actions' || column.id === 'empty'}
{column.title}
{:else}
<Layout.Stack
gap="xs"
direction="row"
alignItems="center"
alignContent="center">
<svelte:window on:resize={updateOverlayHeight} />
<div class="spreadsheet-container-outer" data-mode={mode} bind:this={spreadsheetContainer}>
<SpreadsheetContainer bind:this={spreadsheetRootContainer}>
<Spreadsheet.Root
{emptyCells}
allowSelection
height={fixedHeight}
columns={spreadsheetColumns}
loading={$spreadsheetLoading}
bottomActionClick={() => {
/* @ignore: only for showing the `+` button on footer */
}}>
<svelte:fragment slot="header" let:root>
{#each spreadsheetColumns as column (column.id)}
{@const columnActionsById = column.id === 'actions'}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
style:cursor={columnActionsById ? 'pointer' : null}
onclick={() => {
if (columnActionsById && mode === 'rows') {
$showCreateAttributeSheet.show = true;
$showCreateAttributeSheet.title = 'Create column';
$showCreateAttributeSheet.columns = $tableColumns;
$showCreateAttributeSheet.columnsOrder = $columnsOrder;
}
}}>
<Spreadsheet.Header.Cell
{root}
column={column.id}
icon={column.icon ?? undefined}>
{#if column.isAction}
<Button.Button
icon
variant="extra-compact"
on:click={() => {
console.log('dank');
}}>
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
{:else if column.id === 'actions' || column.id === 'empty'}
{column.title}
{:else}
<Layout.Stack
gap="xs"
direction="row"
alignItems="center"
alignContent="center">
{column.title}
<SortButton disabled column={column.id} />
</Layout.Stack>
{/if}
</Spreadsheet.Header.Cell>
</div>
{/each}
</svelte:fragment>
<SortButton disabled column={column.id} />
</Layout.Stack>
{/if}
</Spreadsheet.Header.Cell>
</div>
{/each}
</svelte:fragment>
<svelte:fragment slot="footer">
{#if $spreadsheetLoading}
<Layout.Stack
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Skeleton variant="line" height={18} width={125} />
</Layout.Stack>
{/if}
</svelte:fragment>
</Spreadsheet.Root>
<svelte:fragment slot="footer">
{#if $spreadsheetLoading}
<Layout.Stack
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Skeleton variant="line" height={18} width={125} />
</Layout.Stack>
{/if}
</svelte:fragment>
</Spreadsheet.Root>
</SpreadsheetContainer>
{#if !$spreadsheetLoading}
<div class="spreadsheet-fade-bottom" data-collapsed-tabs={!$expandTabs}>
<div
class="spreadsheet-fade-bottom"
data-collapsed-tabs={!$expandTabs}
style:--dynamic-overlay-height={dynamicOverlayHeight}>
<div class="empty-actions">
<Layout.Stack gap="xl" alignItems="center">
<Typography.Title>{title ?? `You have no ${mode} yet`}</Typography.Title>
@@ -293,7 +318,6 @@
.spreadsheet-fade-bottom {
bottom: 0;
width: 100%;
height: 70.5vh;
position: fixed;
background: linear-gradient(
180deg,
@@ -302,20 +326,22 @@
#ffffff 100%
);
z-index: 20;
display: flex;
display: none;
justify-content: center;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: var(--dynamic-overlay-height, 70.5vh);
&[data-collapsed-tabs='true'] {
height: 79.1vh !important;
height: calc(var(--dynamic-overlay-height, 79.1vh) + 8.6vh);
}
@media (max-width: 1024px) {
height: 63.35vh;
height: var(--dynamic-overlay-height, 63.35vh);
}
@media (min-width: 1024px) {
height: 70.35vh;
height: var(--dynamic-overlay-height, 70.35vh);
}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount, onDestroy, type Snippet, tick } from 'svelte';
import { expandTabs, scrollStore } from '../store';
import { onMount, onDestroy, type Snippet, tick } from 'svelte';
let {
observeExpand = false,
@@ -26,7 +26,7 @@
}
/** adjust height to fill remaining viewport space */
export function resizeSheet(fromResize: boolean = false): void {
export function resizeSheet(fromResize: boolean = false, isEmptySheet: boolean = false): void {
if (!spreadsheetWrapper) return;
clearTimeout(resizeTimeout);
@@ -36,7 +36,7 @@
base = window.innerHeight - rect.top;
let headerHeightDiff = 0;
if (observeExpand && !isFirstMount && !fromResize) {
if (observeExpand && !fromResize) {
/**
* 16px from padding top
* 08px from padding bottom
@@ -44,10 +44,10 @@
* ————————————————
* 89px
*/
headerHeightDiff = $expandTabs ? -89 : 89;
headerHeightDiff = $expandTabs ? (isFirstMount ? 0 : -89) : 89;
}
spreadsheetHeight = `${base + headerHeightDiff}px`;
spreadsheetHeight = `${base + headerHeightDiff + (isEmptySheet ? 89 : 0)}px`;
isFirstMount = false;
}, 16);
}
@@ -23,7 +23,6 @@
let rowList: Models.RowList<Models.Row>;
let search: string = null;
let displayNames = ['$id'];
let relatedList: string[] = [];
let singleRel: string;
let showInput = false;
@@ -50,21 +49,21 @@
singleRel = $row[column.key]?.$id;
}
}
displayNames = preferences.getDisplayNames()?.[column?.relatedTable] ?? ['$id'];
if (!displayNames?.includes('$id')) {
displayNames.unshift('$id');
}
});
async function getRows(search: string = null) {
const queries = search
? [Query.select(['$id']), Query.startsWith('$id', search), Query.orderDesc('')]
: [Query.select(['$id'])];
// already includes the `$id`, dw!
const displayNames = preferences.getDisplayNames(column.relatedTable);
return await sdk
.forProject(page.params.region, page.params.project)
.grids.listRows(databaseId, column.relatedTable, queries);
const queries = search
? [Query.select(displayNames), Query.startsWith('$id', search), Query.orderDesc('')]
: [Query.select(displayNames)];
return await sdk.forProject(page.params.region, page.params.project).grids.listRows({
databaseId,
tableId: column.relatedTable,
queries
});
}
function getAvailableOptions(excludeIndex?: number) {
@@ -73,6 +72,7 @@
excludeIndex !== undefined
? relatedList.filter((_, idx) => idx !== excludeIndex)
: relatedList;
return !otherItems.includes(option.value);
});
}
@@ -119,7 +119,10 @@
$: options =
rowList?.rows?.map((row) => {
const names = displayNames.filter((name) => name !== '$id');
const names = preferences
.getDisplayNames(column?.relatedTable)
.filter((name) => name !== '$id');
const values = names
.map((name) => row?.[name])
// always supposed to be a string but just being a bit safe here
@@ -155,25 +158,26 @@
}
</script>
<!-- TODO: Maybe show a dialog for this for spreadsheet -->
{#if isRelationshipToMany(column)}
<Layout.Stack gap="xxl">
<Layout.Stack gap="m">
<Layout.Stack direction="row" alignContent="space-between">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{optionalText}
</Typography.Text>
</Layout.Stack>
{#if !limited}
<Layout.Stack direction="row" alignContent="space-between">
<Layout.Stack gap="xxs" direction="row" alignItems="center">
<Typography.Text variant="m-500">{label}</Typography.Text>
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
{optionalText}
</Typography.Text>
</Layout.Stack>
{#if showTopAddButton}
<Button secondary on:click={() => (showInput = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
{/if}
</Layout.Stack>
{#if showTopAddButton}
<Button secondary on:click={() => (showInput = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Add item
</Button>
{/if}
</Layout.Stack>
{/if}
<Layout.Stack gap="m">
<!-- Empty input for editing mode when no items exist -->
@@ -200,19 +204,22 @@
<InputSelect
{id}
required
autofocus={limited}
options={getAvailableOptions(actualIndex)}
bind:value={relatedList[actualIndex]}
placeholder={`Select ${column.key}`}
on:change={updateRelatedList} />
{#if relatedList[actualIndex]}
<div style:padding-block-start="0.5rem">
<Button
icon
extraCompact
on:click={() => removeItem(actualIndex)}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{#if !limited}
{#if relatedList[actualIndex]}
<div style:padding-block-start="0.5rem">
<Button
icon
extraCompact
on:click={() => removeItem(actualIndex)}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
{/if}
</Layout.Stack>
{/each}
@@ -263,7 +270,7 @@
{/if}
</Layout.Stack>
{#if showBottomAddButton}
{#if showBottomAddButton && !limited}
<Layout.Stack direction="row" alignContent="flex-start">
<Button extraCompact on:click={() => (showInput = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
@@ -10,6 +10,7 @@
import { Typography } from '@appwrite.io/pink-svelte';
import { table } from '../store';
import { preferences } from '$lib/stores/preferences';
import { organization } from '$lib/stores/organization.js';
export let showDelete = false;
@@ -19,8 +20,9 @@
async function clearPreferences() {
await Promise.all([
preferences.deleteDisplayNames(tableId),
preferences.deleteCustomTableColumns(tableId)
// TODO: remove other prefs like column order, widths, etc.
preferences.deleteCustomTableColumns(tableId),
preferences.deleteDisplayNames($organization.$id, tableId)
]);
}
@@ -12,16 +12,13 @@
import { page } from '$app/state';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
import { organization } from '$lib/stores/organization';
const tableId = page.params.table;
function getDisplayNames() {
return [...(preferences.getDisplayNames()?.[tableId] ?? [])].filter(
(name) => name !== '$id'
); // edge case with `$id`? got saved during tests!
}
const displayNames = $derived(preferences.getDisplayNames(tableId) ?? []);
let names: string[] = $state(getDisplayNames());
let names: string[] = $state(preferences.getDisplayNames(tableId) ?? []);
async function updateDisplayName() {
try {
@@ -29,8 +26,9 @@
// structuredClone doesn't work
const regularArray = [...names];
await preferences.setDisplayNames(tableId, regularArray);
names = getDisplayNames();
await preferences.setDisplayNames($organization.$id, tableId, regularArray);
names = displayNames;
await invalidate(Dependencies.TEAM);
addNotification({
@@ -68,7 +66,7 @@
);
const updateBtnDisabled = $derived(
!symmetricDifference(names, preferences.getDisplayNames()?.[tableId] ?? [])?.length ||
!symmetricDifference(names, preferences.getDisplayNames(tableId))?.length ||
(names?.length && !last(names))
);
@@ -97,12 +95,14 @@
{@const options = getOptions(index)}
{@const disabled =
(!!names[index] && names.length > index + 1) || hasExhaustedOptions}
<InputSelect
id={name}
{options}
{disabled}
bind:value={names[index]}
placeholder="Select column" />
<Button
icon
extraCompact
@@ -1,7 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { goto, invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Confirm, Id, SortButton } from '$lib/components';
import { Dependencies, SPREADSHEET_PAGE_LIMIT } from '$lib/constants';
@@ -32,9 +31,10 @@
rowActivitySheet,
paginatedRows,
paginatedRowsLoading,
spreadsheetRenderKey
spreadsheetRenderKey,
expandTabs,
databaseRelatedRowSheetOptions
} from './store';
import RelationshipsModal from './relationshipsModal.svelte';
import type { Column, ColumnType } from '$lib/helpers/types';
import {
Alert,
@@ -109,11 +109,6 @@
]); /* TODO: should be fixed at the sdk level! */
let selectedRows = [];
let displayNames = {};
let showRelationships = false;
let relationshipData: Partial<Models.Row>[];
let selectedRelationship: Models.ColumnRelationship = null;
let spreadsheetContainer: SpreadsheetContainer;
let currentPage = 1;
@@ -125,7 +120,6 @@
let selectedRowForDelete: Models.Row['$id'] | null = null;
onMount(async () => {
displayNames = preferences.getDisplayNames();
columnsOrder.set(preferences.getColumnOrder(tableId));
columnsWidth.set(preferences.getColumnWidths(tableId));
@@ -160,7 +154,7 @@
const staticColumns: Column[] = [
{
id: '$id',
title: 'ID',
title: '$id',
width: getColumnWidth('$id', 225),
minimumWidth: 225,
draggable: false,
@@ -172,7 +166,7 @@
},
{
id: '$createdAt',
title: 'createdAt',
title: '$createdAt',
width: getColumnWidth('$createdAt', { min: 200 }),
minimumWidth: 200,
draggable: true,
@@ -183,7 +177,7 @@
},
{
id: '$updatedAt',
title: 'updatedAt',
title: '$updatedAt',
width: getColumnWidth('$updatedAt', { min: 200 }),
minimumWidth: 200,
draggable: true,
@@ -604,6 +598,19 @@
}
}
function getDisplayNamesForTable(relatedTable: string | object | null): string[] {
if (!relatedTable) return ['$id'];
let tableId = null;
if (typeof relatedTable === 'string') {
tableId = relatedTable;
} else if (typeof relatedTable === 'object' && '$tableId' in relatedTable) {
tableId = relatedTable.$tableId;
}
return preferences.getDisplayNames(tableId) ?? ['$id'];
}
const saveColumnWidthsToPreferences = debounce(
(column: { columnId: string; newWidth: number; fixedWidth: number }) => {
preferences.saveColumnWidths(organizationId, tableId, {
@@ -636,7 +643,7 @@
$: emptyCellsCount =
$paginatedRows.virtualLength >= emptyCellsLimit
? 0
: emptyCellsLimit - $paginatedRows.virtualLength;
: emptyCellsLimit - $paginatedRows.virtualLength + (!$expandTabs ? 2 : 0);
$: canShowDatetimePopover = true;
@@ -785,32 +792,31 @@
{/snippet}
</SheetOptions>
{:else if isRelationship(rowColumn)}
{@const args = displayNames?.[rowColumn.relatedTable] ?? [
'$id'
]}
{@const args = getDisplayNamesForTable(row[columnId])}
{#if !isRelationshipToMany(rowColumn)}
{#if row[columnId]}
{@const related = row[columnId]}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
// TODO: open sheet maybes
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table/${rowColumn.relatedTable}/row/${related.$id}`
);
}}>
{#each args as arg, i}
{#if arg !== undefined}
{#if i}&nbsp;|{/if}
<span class="text" data-private
>{related?.[arg]}</span>
{/if}
{/each}
</Link.Button>
{@const displayValue = args
.map((arg) => row[columnId]?.[arg])
.filter(Boolean)
.join(' | ')}
{#if displayValue}
<Link.Button
variant="muted"
on:click={() => {
$databaseRelatedRowSheetOptions.show = true;
$databaseRelatedRowSheetOptions.tableId =
columnId;
$databaseRelatedRowSheetOptions.rowId =
row[columnId]?.['$id'];
}}>
{displayValue}
</Link.Button>
{:else}
<Badge variant="secondary" content="-" size="xs" />
{/if}
{:else}
<span class="text">n/a</span>
<Badge variant="secondary" content="NULL" size="xs" />
{/if}
{:else}
{@const itemsNum = row[columnId]?.length}
@@ -818,12 +824,11 @@
variant="extra-compact"
disabled={!itemsNum}
badge={itemsNum ?? 0}
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
relationshipData = row[columnId];
showRelationships = true;
selectedRelationship = rowColumn;
on:click={() => {
$databaseRelatedRowSheetOptions.show = true;
$databaseRelatedRowSheetOptions.tableId = columnId;
$databaseRelatedRowSheetOptions.rowId =
row[columnId]?.['$id'];
}}>
Items
</Button.Button>
@@ -941,7 +946,8 @@
{#if !$isSmallViewport}
<div style:margin-right="var(--space-6)">
<Button.Button
variant="extra-compact"
size="xs"
variant="secondary"
on:click={() => {
$randomDataModalState.show = true;
}}>Generate sample data</Button.Button>
@@ -976,8 +982,6 @@
{/if}
</SpreadsheetContainer>
<RelationshipsModal bind:show={showRelationships} {selectedRelationship} data={relationshipData} />
<Confirm
bind:open={showDelete}
onSubmit={handleDelete}
@@ -64,6 +64,18 @@ export const databaseRowSheetOptions = writable<
row: null
});
export const databaseRelatedRowSheetOptions = writable<
DatabaseSheetOptions & {
rowId: string;
tableId: string;
}
>({
title: 'Update related row',
show: false,
rowId: null,
tableId: null
});
export const showRecordsCreateSheet = writable({
show: false,
row: null
@@ -156,3 +168,12 @@ export const spreadsheetRenderKey = writable('initial');
export const paginatedRowsLoading = writable(false);
export const paginatedRows = createSparsePagedDataStore<Models.DefaultRow>(SPREADSHEET_PAGE_LIMIT);
export const PROHIBITED_ROW_KEYS = [
'$id',
'$collection',
'$tableId',
'$databaseId',
'$createdAt',
'$updatedAt'
];