fix: related content loading and deletion.

This commit is contained in:
Darshan
2025-08-27 13:37:51 +05:30
parent 7b9eaa44b5
commit 8179f2ff5f
7 changed files with 237 additions and 91 deletions
@@ -397,7 +397,7 @@
}}>
<EditRelatedRow
bind:this={editRelatedRow}
rowId={$databaseRelatedRowSheetOptions.rowId}
rows={$databaseRelatedRowSheetOptions.rows}
tableId={$databaseRelatedRowSheetOptions.tableId} />
</SideSheet>
@@ -11,6 +11,7 @@
import Confirm from '$lib/components/confirm.svelte';
import { Layout } from '@appwrite.io/pink-svelte';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
let {
showDelete = $bindable(false),
@@ -70,6 +71,10 @@
trackError(e, Submit.ColumnDelete);
}
}
function getAsRelationship(column: string | Columns): Models.ColumnRelationship {
return column as Models.ColumnRelationship;
}
</script>
<Confirm
@@ -93,7 +98,7 @@
{#if requiresTwoWayConfirm}
<!-- not allowed on multi selections, safe to assume that this isn't a string! -->
{@const attribute = selectedColumn[0]}
{@const column = getAsRelationship(selectedColumns[0])}
<Layout.Stack direction="column" gap="xl">
<p>
This is a two way relationship and the corresponding relationship will also be
@@ -102,8 +107,8 @@
<p><b>This action is irreversible.</b></p>
<ul>
<InputChoice id="delete" label="Delete" showLabel={false} bind:value={checked}>
Delete relationship between <b data-private>{attribute.key}</b> to
<b data-private>{attribute.twoWayKey}</b>
Delete relationship between <b data-private>{column.key}</b> to
<b data-private>{column.twoWayKey}</b>
</InputChoice>
</ul>
</Layout.Stack>
@@ -5,33 +5,44 @@
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Column from './column.svelte';
import type { Columns } from '../../store';
import { writable } from 'svelte/store';
let {
column,
formValues = $bindable({}),
label,
editing = false,
fromSpreadsheet = false
fromSpreadsheet = false,
onUpdateFormValues = null
}: {
column: Columns;
formValues: object;
label: string;
editing?: boolean;
fromSpreadsheet?: boolean;
onUpdateFormValues?: (formValues: object) => void;
} = $props();
let formStore = writable(formValues);
function removeArrayItem(key: string, index: number) {
formValues = {
...formValues,
[key]: formValues[key].filter((_, i) => i !== index)
const next = {
...$formStore,
[key]: $formStore[key].filter((_, i) => i !== index)
};
formStore.set(next);
onUpdateFormValues?.(next);
}
function addArrayItem(key: string) {
formValues = {
...formValues,
[key]: [...formValues[key], null]
const next = {
...$formStore,
[key]: [...($formStore[key] ?? []), null]
};
formStore.set(next);
onUpdateFormValues?.(next);
}
function getColumnType(column: Columns) {
@@ -52,10 +63,12 @@
}
return `${capitalize(column.type)}${column.array ? '[]' : ''}`;
}
formStore.subscribe((values) => onUpdateFormValues?.(values));
</script>
{#if column.array}
{#if formValues[column.key]?.length === 0}
{#if $formStore[column.key]?.length === 0}
{#if fromSpreadsheet}
<Column
array
@@ -65,7 +78,7 @@
id={column.key}
limited={fromSpreadsheet}
optionalText={getColumnType(column)}
bind:value={formValues[column.key]}
bind:value={$formStore[column.key]}
on:click />
{:else}
<Layout.Stack direction="row" alignContent="space-between">
@@ -90,11 +103,11 @@
id={column.key}
limited={fromSpreadsheet}
optionalText={getColumnType(column)}
bind:value={formValues[column.key]}
bind:value={$formStore[column.key]}
on:click />
{:else}
<Layout.Stack>
{#each [...(formValues[column.key]?.keys() ?? [])] as index}
{#each [...($formStore[column.key]?.keys() ?? [])] as index}
<Layout.Stack direction="row" alignItems="flex-end" gap="xs">
<Column
{column}
@@ -102,7 +115,7 @@
id={`${column.key}-${index}`}
optionalText={index === 0 ? getColumnType(column) : undefined}
label={index === 0 ? label : ''}
bind:value={formValues[column.key][index]} />
bind:value={$formStore[column.key][index]} />
<Button text icon on:click={() => removeArrayItem(column.key, index)}>
<span class="icon-x" aria-hidden="true"></span>
</Button>
@@ -124,6 +137,6 @@
id={column.key}
limited={fromSpreadsheet}
optionalText={getColumnType(column)}
bind:value={formValues[column.key]}
bind:value={$formStore[column.key]}
on:click />
{/if}
@@ -34,22 +34,16 @@
let offset = 0;
onMount(async () => {
if (value && typeof value === 'object') {
row = value as Models.Row;
singleRel = row?.$id;
}
if (value && isRelationshipToMany(column)) {
// TODO: test this
relatedList = (value as string[]).slice();
}
if (editing && row?.[column.key]) {
if (row[column.key]?.length) {
relatedList =
row[column.key]?.map((d: Record<string, unknown>) => {
return d?.$id;
}) ?? [];
if (isRelationshipToMany(column)) {
if (Array.isArray(value)) {
relatedList = value.map((item: object | string) => {
return typeof item === 'string' ? item : item['$id'];
});
}
} else {
if (value && typeof value === 'object') {
row = value as Models.Row;
singleRel = row?.$id;
}
}
});
@@ -4,33 +4,39 @@
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 { get, 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 './columns/columnItem.svelte';
import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store';
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
import { Divider, Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
import { deepClone } from '$lib/helpers/object';
const databaseId = page.params.database;
let {
rowId,
rows,
tableId
}: {
rowId: string;
rows: string | Models.Row[];
tableId: string;
} = $props();
let loading = $state(false);
let fetchedRow = $state<Models.Row | null>(null);
let fetchedRows = $state<Models.Row[]>([]);
let relatedTable = $state<Models.Table | null>(null);
let work = $state<Writable<Models.Row> | null>(null);
let disabledState = $state(calculateAndCompareDisabledState());
let workData = $state<Map<string, Writable<Models.Row>>>(new Map());
let columnFormWrapper = $state<HTMLElement | null>(null);
function isSingleStore() {
return typeof rows === 'string';
}
async function loadRelatedRow() {
loading = true;
@@ -42,25 +48,46 @@
tableId: tableId
}));
fetchedRow = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.getRow({
databaseId,
tableId: tableId,
rowId: rowId,
queries: buildWildcardColumnsQuery(relatedTable)
if (isSingleStore()) {
const fetchedRow = await sdk
.forProject(page.params.region, page.params.project)
.tablesDB.getRow({
databaseId,
tableId: tableId,
rowId: rows as string,
queries: buildWildcardColumnsQuery(relatedTable)
});
fetchedRows = [fetchedRow];
} else {
const rowIds = (rows as Models.Row[]).map((row) => row.$id);
const fetchPromises = rowIds.map(async (rowId) => {
return sdk.forProject(page.params.region, page.params.project).tablesDB.getRow({
databaseId,
tableId: tableId,
rowId: rowId,
queries: buildWildcardColumnsQuery(relatedTable)
});
});
const filteredKeys = Object.keys(fetchedRow).filter((key) => {
return !PROHIBITED_ROW_KEYS.includes(key);
fetchedRows = await Promise.all(fetchPromises);
}
const newWorkData = new Map();
fetchedRows.forEach((row) => {
const filteredKeys = Object.keys(row).filter((key) => {
return !PROHIBITED_ROW_KEYS.includes(key);
});
const workingData = filteredKeys.reduce((obj, key) => {
obj[key] = row[key];
return obj;
}, {});
newWorkData.set(row.$id, writable(deepClone(workingData as Models.Row)));
});
const workingData = filteredKeys.reduce((obj, key) => {
obj[key] = fetchedRow[key];
return obj;
}, {});
work = writable(deepClone(workingData as Models.Row));
workData = newWorkData;
} catch (error) {
addNotification({
message: error.message,
@@ -72,14 +99,6 @@
}
}
$effect(() => {
if (rowId && tableId) {
loadRelatedRow().then(() => {
focusFirstInput();
});
}
});
function compareColumns(column: Columns, $work: Models.Row, originalRow: Models.Row) {
if (!column) {
return false;
@@ -94,6 +113,10 @@
if (isRelationship(column)) {
if (isRelationshipToMany(column as Models.ColumnRelationship)) {
if (!Array.isArray(workColumn) || !Array.isArray(currentColumn)) {
return workColumn === currentColumn;
}
const workIds = workColumn.map((doc: string | Record<string, unknown>) =>
typeof doc === 'string' ? doc : doc.$id
);
@@ -114,22 +137,77 @@
return workColumn === currentColumn;
}
export async function update() {
try {
await sdk.forProject(page.params.region, page.params.project).tablesDB.updateRow({
databaseId,
tableId: relatedTable.$id,
rowId: fetchedRow.$id,
data: $work,
permissions: $work.$permissions
function calculateAndCompareDisabledState() {
if (!relatedTable?.columns?.length || !fetchedRows.length) return true;
if (isSingleStore()) {
const rowId = fetchedRows[0].$id;
const row = fetchedRows.find((r) => r.$id === rowId);
const work = workData.get(rowId);
if (!row || !work) return true;
const workValue = get(work);
return relatedTable.columns.every((column) => compareColumns(column, workValue, row));
} else {
return fetchedRows.every((row) => {
const work = workData.get(row.$id);
if (!work) return true;
const workValue = get(work);
return relatedTable.columns.every((column) =>
compareColumns(column, workValue, row)
);
});
}
}
export async function update(rowId?: string) {
try {
if (rowId) {
const work = workData.get(rowId);
const workValue = get(work);
await sdk.forProject(page.params.region, page.params.project).tablesDB.updateRow({
databaseId,
tableId: relatedTable.$id,
rowId: rowId,
data: workValue,
permissions: workValue.$permissions
});
addNotification({
message: 'Related row has been updated',
type: 'success'
});
} else {
const updatePromises = fetchedRows.map(async (row) => {
const work = workData.get(row.$id);
if (!work) return;
const workValue = get(work);
return sdk
.forProject(page.params.region, page.params.project)
.tablesDB.updateRow({
databaseId,
tableId: relatedTable.$id,
rowId: row.$id,
data: workValue,
permissions: workValue.$permissions
});
});
await Promise.all(updatePromises);
addNotification({
message: 'Related row has been updated',
type: 'success'
});
}
invalidate(Dependencies.ROW);
trackEvent(Submit.RowUpdate);
addNotification({
message: 'Related row has been updated',
type: 'success'
});
} catch (error) {
addNotification({
message: error.message,
@@ -140,9 +218,7 @@
}
export function isDisabled(): boolean {
if (!work || !relatedTable?.columns?.length || !fetchedRow) return true;
return relatedTable.columns.every((column) => compareColumns(column, $work, fetchedRow));
return disabledState;
}
function focusFirstInput() {
@@ -152,19 +228,76 @@
firstInput?.focus({ preventScroll: true });
}
function getStore(id: string) {
const rowInstance = workData.get(id);
return get(rowInstance);
}
function handleFormUpdate(rowId: string) {
return (updatedFormValues: object) => {
const workStore = workData.get(rowId);
if (workStore) {
workStore.set(updatedFormValues as Models.Row);
disabledState = calculateAndCompareDisabledState();
}
};
}
$effect(() => {
if (rows && tableId) {
loadRelatedRow().then(() => {
focusFirstInput();
});
}
});
</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}
{:else if relatedTable?.columns?.length && fetchedRows.length}
<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>
{#if fetchedRows.length === 1}
{@const workStore = getStore(fetchedRows[0].$id)}
{#if workStore}
<Layout.Stack direction="column" gap="l">
{#each relatedTable.columns as column}
{@const label = column.key}
<ColumnItem
{column}
{label}
editing
formValues={workStore}
onUpdateFormValues={handleFormUpdate(fetchedRows[0].$id)} />
{/each}
</Layout.Stack>
{/if}
{:else}
<Layout.Stack direction="column" gap="m" class="column-item-stack">
<Typography.Text variant="l-400">{relatedTable.name}</Typography.Text>
<Layout.Stack direction="column" gap="xxl" class="column-item-stack">
{#each fetchedRows as row, index (row.$id)}
{@const workStore = getStore(row.$id)}
{#if workStore}
{#each relatedTable.columns as column}
{@const label = column.key}
<ColumnItem
{column}
{label}
editing
formValues={workStore}
onUpdateFormValues={handleFormUpdate(row.$id)} />
{/each}
{/if}
{#if index < fetchedRows.length - 1}
<Divider />
{/if}
{/each}
</Layout.Stack>
</Layout.Stack>
{/if}
</div>
{/if}
@@ -852,11 +852,11 @@
<Link.Button
variant="muted"
on:click={() => {
$databaseRelatedRowSheetOptions.show = true;
$databaseRelatedRowSheetOptions.tableId =
columnId;
$databaseRelatedRowSheetOptions.rowId =
row[columnId]?.['$tableId'];
$databaseRelatedRowSheetOptions.rows =
row[columnId]?.['$id'];
$databaseRelatedRowSheetOptions.show = true;
}}>
{displayValue}
</Link.Button>
@@ -877,9 +877,9 @@
badge={itemsNum ?? 0}
on:click={() => {
$databaseRelatedRowSheetOptions.show = true;
$databaseRelatedRowSheetOptions.rows =
row[columnId];
$databaseRelatedRowSheetOptions.tableId = columnId;
$databaseRelatedRowSheetOptions.rowId =
row[columnId]?.['$id'];
}}>
Items
</Button.Button>
@@ -66,13 +66,13 @@ export const databaseRowSheetOptions = writable<
export const databaseRelatedRowSheetOptions = writable<
DatabaseSheetOptions & {
rowId: string;
rows: string | Models.Row[];
tableId: string;
}
>({
title: 'Update related row',
show: false,
rowId: null,
rows: null,
tableId: null
});
@@ -177,6 +177,7 @@ export const paginatedRows = createSparsePagedDataStore<Models.DefaultRow>(SPREA
export const PROHIBITED_ROW_KEYS = [
'$id',
'$sequence',
'$collection',
'$tableId',
'$databaseId',