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 b73f1f239..625530016 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 @@ -397,7 +397,7 @@ }}> diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte index 190c7ba81..8eab71181 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/columns/deleteColumn.svelte @@ -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; + } - {@const attribute = selectedColumn[0]} + {@const column = getAsRelationship(selectedColumns[0])}

This is a two way relationship and the corresponding relationship will also be @@ -102,8 +107,8 @@

This action is irreversible.

    - Delete relationship between {attribute.key} to - {attribute.twoWayKey} + Delete relationship between {column.key} to + {column.twoWayKey}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnForm.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnForm.svelte index e8a9ebd61..0bb7fcf44 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnForm.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnForm.svelte @@ -16,7 +16,11 @@ {#each columns as column} {@const label = column.key} - + (formValues = values)} /> {/each} {#if customId !== undefined} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnItem.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnItem.svelte index 8b897d0d3..c1377b917 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnItem.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/columnItem.svelte @@ -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)); {#if column.array} - {#if formValues[column.key]?.length === 0} + {#if $formStore[column.key]?.length === 0} {#if fromSpreadsheet} {:else} @@ -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} - {#each [...(formValues[column.key]?.keys() ?? [])] as index} + {#each [...($formStore[column.key]?.keys() ?? [])] as index} + bind:value={$formStore[column.key][index]} /> @@ -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} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte index 8e01a16f9..910e37e68 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte @@ -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) => { - 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; } } }); diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte index aec5e8ad4..8a28b2744 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/create.svelte @@ -119,6 +119,12 @@ createMore = false; } }); + + $effect(() => { + if ($createRow) { + console.log($createRow.row); + } + }); {#if $createRow} 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 c82c61672..b5e154ebf 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 @@ -118,6 +118,10 @@ firstInput?.focus({ preventScroll: true }); } + + function updateRowData(values: object) { + work.set(values as Models.Row); + } {#if $table.columns?.length && work} @@ -125,7 +129,12 @@ {#each $table.columns as column} {@const label = column.key} - + {/each} diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte index e8c604002..7a877a3b4 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/editRelated.svelte @@ -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 { type Models } from '@appwrite.io/console'; + import { get, type Writable, writable } from 'svelte/store'; + import { type Models, Query } 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(null); + let fetchedRows = $state([]); let relatedTable = $state(null); - let work = $state | null>(null); + let disabledState = $state(calculateAndCompareDisabledState()); + + let workData = $state>>(new Map()); let columnFormWrapper = $state(null); + function isSingleStore() { + return typeof rows === 'string'; + } + async function loadRelatedRow() { loading = true; @@ -42,25 +48,108 @@ 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 { + let fetchedTables = []; + const processedRows = []; + const existingRows = rows as Models.Row[]; + + const uniqueTableIds = [...new Set(existingRows.map((row) => row.$tableId))]; + const missingTableIds = uniqueTableIds.filter((tableId) => { + return !page.data.tables?.find((table: Models.Table) => table.$id === tableId); }); - const filteredKeys = Object.keys(fetchedRow).filter((key) => { - return !PROHIBITED_ROW_KEYS.includes(key); + if (missingTableIds.length > 0) { + const tablesResponse = await sdk + .forProject(page.params.region, page.params.project) + .tablesDB.listTables({ + databaseId, + queries: [ + Query.equal('$id', missingTableIds), + Query.limit(missingTableIds.length) + ] + }); + fetchedTables = tablesResponse.tables; + } + + const allTables = [...(page.data.tables || []), ...fetchedTables]; + let rowsMissingData = []; + + for (const row of existingRows) { + const rowTable = allTables.find((table) => table.$id === row.$tableId); + + const hasAllColumns = rowTable.columns.every( + (column: Columns) => column.key in row + ); + + if (!hasAllColumns) { + rowsMissingData.push({ row, rowTable }); + } else { + processedRows.push(row); + } + } + + if (rowsMissingData.length > 0) { + const rowsByTable = new Map(); + + for (const { row, rowTable } of rowsMissingData) { + if (!rowsByTable.has(row.$tableId)) { + rowsByTable.set(row.$tableId, { rows: [], rowTable }); + } + + rowsByTable.get(row.$tableId).rows.push(row); + } + + const fetchPromises = Array.from(rowsByTable.entries()).map( + async ([tableId, { rows, rowTable }]) => { + const rowIds: string[] = rows.map((row: Models.Row) => row.$id); + const response = await sdk + .forProject(page.params.region, page.params.project) + .tablesDB.listRows({ + databaseId, + tableId, + queries: [ + Query.equal('$id', rowIds), + Query.limit(rowIds.length), + ...buildWildcardColumnsQuery(rowTable) + ] + }); + return response.rows; + } + ); + + const allCompleteRows = await Promise.all(fetchPromises); + processedRows.push(...allCompleteRows.flat()); + } + + fetchedRows = processedRows; + } + + 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 +161,6 @@ } } - $effect(() => { - if (rowId && tableId) { - loadRelatedRow().then(() => { - focusFirstInput(); - }); - } - }); - function compareColumns(column: Columns, $work: Models.Row, originalRow: Models.Row) { if (!column) { return false; @@ -94,6 +175,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) => typeof doc === 'string' ? doc : doc.$id ); @@ -114,22 +199,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 +280,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 +290,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(); + }); + } + }); {#if loading}
-{:else if relatedTable?.columns?.length && work} +{:else if relatedTable?.columns?.length && fetchedRows.length}
- - {#each relatedTable.columns as column} - {@const label = column.key} - - {/each} - + {#if fetchedRows.length === 1} + {@const workStore = getStore(fetchedRows[0].$id)} + {#if workStore} + + {#each relatedTable.columns as column} + {@const label = column.key} + + {/each} + + {/if} + {:else} + + {relatedTable.name} + + {#each fetchedRows as row, index (row.$id)} + {@const workStore = getStore(row.$id)} + {#if workStore} + {#each relatedTable.columns as column} + {@const label = column.key} + + {/each} + {/if} + + {#if index < fetchedRows.length - 1} + + {/if} + {/each} + + + {/if}
{/if} 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 bfce84f0b..9373e4fd3 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 @@ -852,11 +852,11 @@ { - $databaseRelatedRowSheetOptions.show = true; $databaseRelatedRowSheetOptions.tableId = - columnId; - $databaseRelatedRowSheetOptions.rowId = + row[columnId]?.['$tableId']; + $databaseRelatedRowSheetOptions.rows = row[columnId]?.['$id']; + $databaseRelatedRowSheetOptions.show = true; }}> {displayValue} @@ -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 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 25e579753..024d94478 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 @@ -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(SPREA export const PROHIBITED_ROW_KEYS = [ '$id', + '$sequence', '$collection', '$tableId', '$databaseId',