mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+20
-1
@@ -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"
|
||||
|
||||
+174
@@ -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}
|
||||
+11
-16
@@ -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() {
|
||||
|
||||
+119
-93
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -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);
|
||||
}
|
||||
|
||||
+45
-38
@@ -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" />
|
||||
|
||||
+4
-2
@@ -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)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+9
-9
@@ -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
|
||||
|
||||
+50
-46
@@ -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} |{/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}
|
||||
|
||||
+21
@@ -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'
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user