mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/spreadsheet' into dat-606
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
|
||||
Generated
+5
-5
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b(svelte@5.25.3)
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75(svelte@5.25.3)
|
||||
'@faker-js/faker':
|
||||
specifier: ^9.9.0
|
||||
version: 9.9.0
|
||||
@@ -287,8 +287,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b}
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75}
|
||||
version: 2.0.0-RC.2
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -3831,7 +3831,7 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@0fed23b(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@7a52c75(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
ui = 'legacy',
|
||||
allowNoColumns = false,
|
||||
showAnyway = false,
|
||||
children
|
||||
children,
|
||||
onPreferencesUpdated = null
|
||||
}: {
|
||||
columns: Writable<Column[]>;
|
||||
isCustomTable?: boolean;
|
||||
@@ -28,6 +29,7 @@
|
||||
ui?: 'legacy' | 'new';
|
||||
showAnyway?: boolean;
|
||||
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
|
||||
onPreferencesUpdated?: () => void;
|
||||
} = $props();
|
||||
|
||||
let search = $state('');
|
||||
@@ -60,6 +62,7 @@
|
||||
const shownColumns = $columns.filter((n) => n.hide === true).map((n) => n.id);
|
||||
|
||||
if (isCustomTable) {
|
||||
onPreferencesUpdated?.();
|
||||
preferences.setCustomTableColumns(page.params.table, shownColumns);
|
||||
} else {
|
||||
preferences.setColumns(shownColumns);
|
||||
@@ -211,10 +214,8 @@
|
||||
>Deselect all</Button>
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<div style:padding-inline="0.6rem">
|
||||
<Typography.Text>
|
||||
No column named "{search}" was found
|
||||
</Typography.Text>
|
||||
<div style:padding-inline="0.6rem" style:padding-block-end="0.5rem">
|
||||
<Typography.Text>No results found</Typography.Text>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -7,29 +7,72 @@
|
||||
import ViewToggle from './viewToggle.svelte';
|
||||
import ColumnSelector from './columnSelector.svelte';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { page } from '$app/state';
|
||||
import { hash } from '$lib/helpers/string';
|
||||
|
||||
export let onlyIcon = false;
|
||||
export let ui: 'legacy' | 'new' = 'legacy';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let view: View;
|
||||
export let isCustomTable = false;
|
||||
export let hideView = false;
|
||||
export let hideColumns = false;
|
||||
export let allowNoColumns = false;
|
||||
export let showAnyway = false;
|
||||
interface Props {
|
||||
onlyIcon?: boolean;
|
||||
ui?: 'legacy' | 'new';
|
||||
columns: Writable<Column[]>;
|
||||
view: View;
|
||||
isCustomTable?: boolean;
|
||||
hideView?: boolean;
|
||||
hideColumns?: boolean;
|
||||
allowNoColumns?: boolean;
|
||||
showAnyway?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
onlyIcon = false,
|
||||
ui = 'legacy',
|
||||
columns,
|
||||
view = $bindable(),
|
||||
isCustomTable = false,
|
||||
hideView = false,
|
||||
hideColumns = false,
|
||||
allowNoColumns = false,
|
||||
showAnyway = false
|
||||
}: Props = $props();
|
||||
|
||||
let showCountBadge = $state(false);
|
||||
const preferenceKey = $derived(getPreferenceKey());
|
||||
|
||||
function getPreferenceKey(): string {
|
||||
const tableId = page.params.table;
|
||||
const organizationId = page.data.organization?.$id ?? page.data.project?.teamId;
|
||||
return hash([organizationId, tableId].filter(Boolean).join('#'));
|
||||
}
|
||||
|
||||
function updateBadgeState() {
|
||||
if (!preferences.getKey(preferenceKey, false)) {
|
||||
showCountBadge = true;
|
||||
preferences.setKey(preferenceKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
showCountBadge = !onlyIcon || !!preferences.getKey(preferenceKey, false);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !hideColumns && view === View.Table}
|
||||
<ColumnSelector {columns} {isCustomTable} {allowNoColumns} {ui} {showAnyway}>
|
||||
<ColumnSelector
|
||||
{ui}
|
||||
{columns}
|
||||
{showAnyway}
|
||||
{isCustomTable}
|
||||
{allowNoColumns}
|
||||
onPreferencesUpdated={updateBadgeState}>
|
||||
{#snippet children(toggle, selectedColumnsNumber)}
|
||||
<Button.Button
|
||||
size="s"
|
||||
icon={onlyIcon}
|
||||
onclick={toggle}
|
||||
variant="secondary"
|
||||
class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
|
||||
disabled={!$columns.length && showAnyway}
|
||||
badge={selectedColumnsNumber.toString()}
|
||||
on:click={toggle}>
|
||||
class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
|
||||
badge={showCountBadge ? selectedColumnsNumber.toString() : undefined}>
|
||||
<Icon slot="start" icon={IconViewBoards} />
|
||||
</Button.Button>
|
||||
{/snippet}
|
||||
|
||||
@@ -149,7 +149,7 @@ function generateSingleValue(column: Columns): string | number | boolean | null
|
||||
return faker.internet.url();
|
||||
}
|
||||
}
|
||||
return ''
|
||||
return '';
|
||||
} else {
|
||||
const stringAttr = column as Models.ColumnString;
|
||||
const maxLength = Math.min(stringAttr.size ?? 255, 1000);
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
export let limit = 0;
|
||||
export let offset = 0;
|
||||
export let logs: Models.LogList;
|
||||
export let insideSideSheet = false;
|
||||
export let databasesScreen = false;
|
||||
export let useCreateLinkForPagination = true;
|
||||
|
||||
const columns: PinkColumn[] = [
|
||||
{ id: 'user' },
|
||||
{ id: 'event' },
|
||||
{ id: 'location' },
|
||||
{ id: 'ip' },
|
||||
{ id: 'date' }
|
||||
{ id: 'user', ...(insideSideSheet ? { width: 140 } : {}) },
|
||||
{ id: 'event', ...(insideSideSheet ? { width: 125 } : {}) },
|
||||
{ id: 'location', ...(insideSideSheet ? { width: 100 } : {}) },
|
||||
{ id: 'ip', ...(insideSideSheet ? { width: { min: 150 } } : {}) },
|
||||
{ id: 'date', ...(insideSideSheet ? { width: { min: 200 } } : {}) }
|
||||
];
|
||||
</script>
|
||||
|
||||
<!-- TODO: only the databases page -->
|
||||
<Container expanded={databasesScreen} slotSpacing={databasesScreen} {databasesScreen}>
|
||||
{#if logs.total}
|
||||
<div>
|
||||
|
||||
@@ -15,20 +15,13 @@
|
||||
} = $props();
|
||||
|
||||
const buttonSize = $derived(collapsed ? 'xs' : 's');
|
||||
|
||||
const expandedSize = $derived(
|
||||
$isSmallViewport ? 'var(--font-size-m)' : 'var(--font-size-xxxl)'
|
||||
);
|
||||
|
||||
// const currentFontSize = $derived(collapsed ? 'var(--font-size-l)' : expandedSize);
|
||||
const currentFontSize = $derived(collapsed ? 'var(--font-size-l)' : 'var(--font-size-xxxl)');
|
||||
|
||||
const currentLineHeight = $derived(collapsed ? '130%' : '140%');
|
||||
const currentLetterSpacing = $derived(collapsed ? '0' : '-0.144px');
|
||||
const currentFontSize = $derived(collapsed ? 'var(--font-size-l)' : 'var(--font-size-xxxl)');
|
||||
</script>
|
||||
|
||||
<Layout.Stack justifyContent="center" alignItems="center" direction="row" gap="xs" inline>
|
||||
{#if href}
|
||||
{#if href && !$isSmallViewport}
|
||||
<span style:position="relative">
|
||||
<Button.Anchor size={buttonSize} icon variant="text" {href} aria-label="page back">
|
||||
<Icon icon={IconChevronLeft} />
|
||||
|
||||
@@ -46,6 +46,9 @@ type ConsolePreferencesStore = {
|
||||
columnWidths?: {
|
||||
[key: string]: TeamPreferences['widths'];
|
||||
};
|
||||
miscellaneous?: {
|
||||
[key: string]: string | number | boolean;
|
||||
};
|
||||
} & { hideAiDisclaimer?: boolean };
|
||||
|
||||
async function updateConsolePreferences(store: ConsolePreferencesStore): Promise<void> {
|
||||
@@ -191,8 +194,6 @@ function createPreferences() {
|
||||
n.tables ??= {};
|
||||
|
||||
n.tables[tableId] = Array.from(new Set(columns));
|
||||
// let's not double save
|
||||
// n.collections[tableId] = Array.from(new Set(columns));
|
||||
return n;
|
||||
}),
|
||||
|
||||
@@ -238,8 +239,8 @@ function createPreferences() {
|
||||
});
|
||||
},
|
||||
|
||||
getColumnOrder(collectionId: string): TeamPreferences['order'] {
|
||||
return teamPreferences?.columnOrder?.[collectionId] ?? [];
|
||||
getColumnOrder(tableId: string): TeamPreferences['order'] {
|
||||
return teamPreferences?.columnOrder?.[tableId] ?? [];
|
||||
},
|
||||
|
||||
async saveColumnOrder(
|
||||
@@ -299,6 +300,21 @@ function createPreferences() {
|
||||
});
|
||||
},
|
||||
|
||||
getKey<T>(key: string, _default: T): T {
|
||||
return (preferences?.miscellaneous?.[key] ?? _default) as T;
|
||||
},
|
||||
|
||||
async setKey(key: string, value: string | number | boolean) {
|
||||
await updateAndSync((n) => {
|
||||
if (!n?.miscellaneous) {
|
||||
n ??= {};
|
||||
n.miscellaneous ??= {};
|
||||
}
|
||||
|
||||
n.miscellaneous[key] = value;
|
||||
return n;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+23
-20
@@ -4,7 +4,7 @@
|
||||
import { showCreate } from './store';
|
||||
import type { PageData } from './$types';
|
||||
import { showSubNavigation } from '$lib/stores/layout';
|
||||
import { Icon, Sidebar, Navbar, Layout, Link } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Sidebar, Navbar, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconDatabase,
|
||||
@@ -55,14 +55,14 @@
|
||||
</a>
|
||||
<div class="collection-content">
|
||||
{#if tables?.total}
|
||||
<ul class="drop-list u-margin-inline-start-8 u-margin-block-start-8">
|
||||
<ul class="drop-list u-margin-inline-start-8 u-margin-block-start-4">
|
||||
{#each sortedTables as table, index}
|
||||
{@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`}
|
||||
{@const isSelected = tableId === table.$id}
|
||||
{@const isFirst = index === 0}
|
||||
{@const isLast = index === sortedTables.length - 1}
|
||||
|
||||
<Layout.Stack gap="m" direction="row" alignItems="center">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<li
|
||||
class:is-last={isLast}
|
||||
class:is-first={isFirst}
|
||||
@@ -99,7 +99,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Layout.Stack alignItems="center" direction="row" style="gap: 3px;">
|
||||
<Layout.Stack
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
style="gap: 3px; margin-block-start: 8px;">
|
||||
<Icon icon={IconPlus} size="s" />
|
||||
<Button
|
||||
compact
|
||||
@@ -120,18 +123,18 @@
|
||||
<Icon icon={IconDatabase} size="s" color="--neutral-300" />
|
||||
<Link.Anchor
|
||||
href={`${base}/project-${region}-${project}/databases/database-${databaseId}`}
|
||||
variant="quiet-muted">{data.database.name}</Link.Anchor>
|
||||
variant="quiet-muted">
|
||||
{data.database.name}
|
||||
</Link.Anchor>
|
||||
|
||||
<span style:margin-left="8px">/</span>
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
aria-label="Open collections"
|
||||
onclick={() => {
|
||||
openBottomSheet = !openBottomSheet;
|
||||
}}>
|
||||
<span class="orgName">{selectedTable?.name}</span>
|
||||
|
||||
<Button text extraCompact on:click={() => (openBottomSheet = !openBottomSheet)}>
|
||||
<Typography.Text variant="m-400">
|
||||
{selectedTable?.name}
|
||||
</Typography.Text>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Navbar.Base>
|
||||
@@ -166,17 +169,16 @@
|
||||
|
||||
<style lang="scss">
|
||||
.list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-name {
|
||||
margin-block-end: 8px;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.collection-content {
|
||||
@@ -212,6 +214,7 @@
|
||||
|
||||
.drop-list {
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
padding-left: 4px;
|
||||
position: relative;
|
||||
font-size: var(--font-size-sm);
|
||||
@@ -228,12 +231,12 @@
|
||||
|
||||
// first item
|
||||
&:has(li.is-first)::before {
|
||||
top: 0.9rem;
|
||||
top: 0.8rem;
|
||||
}
|
||||
|
||||
// last item
|
||||
&:has(li.is-last)::before {
|
||||
bottom: 0.9rem;
|
||||
bottom: 0.85rem;
|
||||
}
|
||||
|
||||
// the only item
|
||||
|
||||
+1
-3
@@ -72,8 +72,6 @@
|
||||
const tableId = page.params.table;
|
||||
|
||||
onMount(() => {
|
||||
columnsOrder.set(preferences.getColumnOrder(tableId));
|
||||
columnsWidth.set(preferences.getColumnWidths(tableId));
|
||||
expandTabs.set(preferences.isTableHeaderExpanded(tableId));
|
||||
|
||||
return realtime
|
||||
@@ -384,7 +382,7 @@
|
||||
externalColumnKey={$showCreateIndexSheet.column} />
|
||||
</SideSheet>
|
||||
|
||||
<SideSheet title="Row activity" bind:show={$rowActivitySheet.show} closeOnBlur>
|
||||
<SideSheet spaced title="Row activity" bind:show={$rowActivitySheet.show} closeOnBlur>
|
||||
<RowActivity />
|
||||
</SideSheet>
|
||||
|
||||
|
||||
+33
-3
@@ -102,6 +102,7 @@
|
||||
let showDelete = $state(false);
|
||||
let selectedColumns = $state([]);
|
||||
let selectedColumn: Columns = $state(null);
|
||||
let columnIndexMap: Record<string, boolean> = $state({});
|
||||
|
||||
let columnsOrder = $state([]);
|
||||
const tableId = page.params.table;
|
||||
@@ -161,6 +162,18 @@
|
||||
tick().then(() => spreadsheetContainer?.resizeSheet());
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!$showCreateIndexSheet.show && $showCreateIndexSheet.column) {
|
||||
const columnKey = $showCreateIndexSheet.column;
|
||||
|
||||
const isActuallyIndexed = $indexes.some((index) => index.columns.includes(columnKey));
|
||||
|
||||
if (!isActuallyIndexed) {
|
||||
columnIndexMap[columnKey] = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container
|
||||
@@ -275,17 +288,34 @@
|
||||
</Layout.Stack>
|
||||
{@const minMaxSize = getMinMaxSizeForColumn(column)}
|
||||
{#if minMaxSize}
|
||||
<Typography.Caption variant="400">
|
||||
<Typography.Caption
|
||||
variant="400"
|
||||
color="--fgcolor-neutral-tertiary">
|
||||
{minMaxSize}
|
||||
</Typography.Caption>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="indexed" {root} isEditable={false}>
|
||||
{@const isIndexed = $indexes.some((index) =>
|
||||
{@const isActuallyIndexed = $indexes.some((index) =>
|
||||
index.columns.includes(column.key)
|
||||
)}
|
||||
<Selector.Checkbox size="s" checked={isIndexed} disabled />
|
||||
|
||||
{@const checked = isActuallyIndexed || !!columnIndexMap[column.key]}
|
||||
|
||||
<Selector.Checkbox
|
||||
size="s"
|
||||
{checked}
|
||||
disabled={isActuallyIndexed}
|
||||
on:change={(e) => {
|
||||
if (!isActuallyIndexed) {
|
||||
if (e.detail) {
|
||||
columnIndexMap[column.key] = true;
|
||||
$showCreateIndexSheet.show = true;
|
||||
$showCreateIndexSheet.column = column.key;
|
||||
}
|
||||
}
|
||||
}} />
|
||||
</Spreadsheet.Cell>
|
||||
<Spreadsheet.Cell column="default" {root} isEditable={false}>
|
||||
{@const _default =
|
||||
|
||||
+33
-11
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { 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 { Dependencies } from '$lib/constants';
|
||||
import { Button, InputNumber, InputSelect, InputText } from '$lib/elements/forms';
|
||||
@@ -63,6 +64,11 @@
|
||||
|
||||
const addColumnDisabled = $derived(!columnList.at(-1)?.value || !columnList.at(-1)?.order);
|
||||
|
||||
const isOnIndexesPage = $derived(page.route.id?.endsWith('/indexes'));
|
||||
const navigatorPathToIndexes = $derived(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${$table?.$id}/indexes`
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (showCreateIndex) {
|
||||
initialize();
|
||||
@@ -80,15 +86,15 @@
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk.forProject(page.params.region, page.params.project).grids.createIndex(
|
||||
await sdk.forProject(page.params.region, page.params.project).grids.createIndex({
|
||||
databaseId,
|
||||
$table.$id,
|
||||
tableId: $table.$id,
|
||||
key,
|
||||
selectedType,
|
||||
columnList.map((a) => a.value),
|
||||
columnList.map((a) => a.order),
|
||||
columnList.map((a) => (a.length ? Number(a.length) : null))
|
||||
);
|
||||
type: selectedType,
|
||||
columns: columnList.map((a) => a.value),
|
||||
orders: columnList.map((a) => a.order),
|
||||
lengths: columnList.map((a) => (a.length ? Number(a.length) : null))
|
||||
});
|
||||
|
||||
await Promise.allSettled([
|
||||
invalidate(Dependencies.TABLE),
|
||||
@@ -96,8 +102,16 @@
|
||||
]);
|
||||
|
||||
addNotification({
|
||||
message: 'Creating index',
|
||||
type: 'success'
|
||||
message: 'Index is being created',
|
||||
type: 'success',
|
||||
buttons: !isOnIndexesPage
|
||||
? [
|
||||
{
|
||||
name: 'View index',
|
||||
method: () => goto(navigatorPathToIndexes)
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
trackEvent(Submit.IndexCreate);
|
||||
showCreateIndex = false;
|
||||
@@ -179,9 +193,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div style:margin-top="2.05rem">
|
||||
<div style:margin-top="27.6px" class="x-button-holder">
|
||||
<Button
|
||||
icon
|
||||
size="s"
|
||||
secondary
|
||||
disabled={columnList.length <= 1}
|
||||
on:click={() => {
|
||||
@@ -200,3 +215,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
.x-button-holder :global(button) {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+5
-2
@@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
/** adjust height to fill remaining viewport space */
|
||||
export function resizeSheet(): void {
|
||||
export function resizeSheet(fromResize: boolean = false): void {
|
||||
if (!spreadsheetWrapper) return;
|
||||
|
||||
clearTimeout(resizeTimeout);
|
||||
@@ -36,7 +36,7 @@
|
||||
base = window.innerHeight - rect.top;
|
||||
|
||||
let headerHeightDiff = 0;
|
||||
if (observeExpand && !isFirstMount) {
|
||||
if (observeExpand && !isFirstMount && !fromResize) {
|
||||
/**
|
||||
* 16px from padding top
|
||||
* 08px from padding bottom
|
||||
@@ -60,6 +60,7 @@
|
||||
|
||||
export function restoreGridSheetScroll(): void {
|
||||
if (initSpreadsheetGridContainer() && spreadsheetGridContainer.scrollWidth > 0) {
|
||||
spreadsheetGridContainer.scrollTop = 0;
|
||||
spreadsheetGridContainer.scrollLeft = $scrollStore;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +82,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={() => resizeSheet(true)} />
|
||||
|
||||
<div bind:this={spreadsheetWrapper} class="spreadsheet-wrapper" style:height={spreadsheetHeight}>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
+11
-5
@@ -6,7 +6,7 @@
|
||||
import { rowActivitySheet } from './store';
|
||||
import { pageToOffset } from '$lib/helpers/load';
|
||||
import { type Models, Query } from '@appwrite.io/console';
|
||||
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
|
||||
import { Skeleton } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let limit = 25; /* default */
|
||||
let offset = $state(0);
|
||||
@@ -47,16 +47,22 @@
|
||||
<Activity
|
||||
{limit}
|
||||
{offset}
|
||||
insideSideSheet
|
||||
on:page={loadRowLogs}
|
||||
logs={rowActivityLogs}
|
||||
databasesScreen
|
||||
useCreateLinkForPagination={false} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.row-activity-wrapper :global(.console-container) {
|
||||
margin-inline: unset;
|
||||
padding-block-start: 1.25rem !important;
|
||||
.row-activity-wrapper {
|
||||
& :global(.console-container) {
|
||||
margin-inline: unset;
|
||||
padding-inline-end: unset;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+95
-103
@@ -96,7 +96,6 @@
|
||||
|
||||
const minimumWidth = 168;
|
||||
const emptyCellsLimit = $isSmallViewport ? 12 : 18;
|
||||
const selected = preferences.getCustomTableColumns(page.params.table);
|
||||
const SYSTEM_KEYS = new Set([
|
||||
'$tableId',
|
||||
'$databaseId'
|
||||
@@ -120,6 +119,8 @@
|
||||
|
||||
onMount(async () => {
|
||||
displayNames = preferences.getDisplayNames();
|
||||
columnsOrder.set(preferences.getColumnOrder(tableId));
|
||||
columnsWidth.set(preferences.getColumnWidths(tableId));
|
||||
|
||||
makeTableColumns();
|
||||
sortState.set(data.currentSort as SortState);
|
||||
@@ -133,11 +134,13 @@
|
||||
onDestroy(() => ($showCreateAttributeSheet.show = false));
|
||||
|
||||
function makeTableColumns() {
|
||||
const selectedColumnsToHide = preferences.getCustomTableColumns(tableId);
|
||||
|
||||
const baseColumns = $table.columns.map((col) => ({
|
||||
id: col.key,
|
||||
title: col.key,
|
||||
type: col.type as ColumnType,
|
||||
hide: !!selected?.includes(col.key),
|
||||
hide: !!selectedColumnsToHide?.includes(col.key),
|
||||
array: col?.array,
|
||||
width: getColumnWidth(col.key, { min: minimumWidth }),
|
||||
minimumWidth: minimumWidth,
|
||||
@@ -158,7 +161,7 @@
|
||||
icon: IconFingerPrint,
|
||||
isEditable: false,
|
||||
isPrimary: false,
|
||||
hide: !!selected?.includes('$id')
|
||||
hide: !!selectedColumnsToHide?.includes('$id')
|
||||
},
|
||||
{
|
||||
id: '$createdAt',
|
||||
@@ -169,7 +172,7 @@
|
||||
type: 'datetime',
|
||||
icon: IconCalendar,
|
||||
isEditable: false,
|
||||
hide: !!selected?.includes('$createdAt')
|
||||
hide: !!selectedColumnsToHide?.includes('$createdAt')
|
||||
},
|
||||
{
|
||||
id: '$updatedAt',
|
||||
@@ -180,17 +183,18 @@
|
||||
type: 'datetime',
|
||||
icon: IconCalendar,
|
||||
isEditable: false,
|
||||
hide: !!selected?.includes('$updatedAt')
|
||||
hide: !!selectedColumnsToHide?.includes('$updatedAt')
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
title: '',
|
||||
width: getColumnWidth('actions', 40),
|
||||
width: 40,
|
||||
isAction: true,
|
||||
draggable: false,
|
||||
type: 'string',
|
||||
resizable: false,
|
||||
isEditable: false
|
||||
isEditable: false,
|
||||
hide: false
|
||||
}
|
||||
];
|
||||
|
||||
@@ -198,32 +202,15 @@
|
||||
staticColumns[0],
|
||||
...baseColumns,
|
||||
staticColumns[1],
|
||||
staticColumns[2],
|
||||
staticColumns[3]
|
||||
staticColumns[2]
|
||||
];
|
||||
|
||||
const visibleNonAction = groupedColumns.filter((c) => !c.hide && !c.isAction);
|
||||
if (visibleNonAction.length === 1) {
|
||||
const only = visibleNonAction[0];
|
||||
if (typeof only.width === 'number') {
|
||||
only.width = {
|
||||
min: only.width
|
||||
};
|
||||
}
|
||||
}
|
||||
const actionsColumn = staticColumns[3];
|
||||
|
||||
tableColumns.set(
|
||||
reorderItems(
|
||||
[
|
||||
staticColumns[0],
|
||||
...baseColumns,
|
||||
staticColumns[1],
|
||||
staticColumns[2],
|
||||
staticColumns[3]
|
||||
],
|
||||
$columnsOrder
|
||||
)
|
||||
);
|
||||
const reorderedNonActions = reorderItems(groupedColumns, $columnsOrder);
|
||||
const finalColumns = [...reorderedNonActions, actionsColumn];
|
||||
|
||||
tableColumns.set(finalColumns);
|
||||
}
|
||||
|
||||
function getColumnWidth(
|
||||
@@ -264,18 +251,14 @@
|
||||
}
|
||||
|
||||
function formatArray(array: unknown[]) {
|
||||
if (array.length === 0) return '[ ]';
|
||||
if (array.length === 0) return 'Empty';
|
||||
|
||||
let formattedFields: string[] = [];
|
||||
for (const item of array) {
|
||||
if (typeof item === 'string') {
|
||||
formattedFields.push(`"${item}"`);
|
||||
} else {
|
||||
formattedFields.push(`${item}`);
|
||||
}
|
||||
formattedFields.push(`${item}`);
|
||||
}
|
||||
|
||||
return `[${formattedFields.join(', ')}]`;
|
||||
return `${formattedFields.join(', ')}`;
|
||||
}
|
||||
|
||||
function formatColumn(column: unknown) {
|
||||
@@ -342,9 +325,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
// trigger ui update with randomized key!
|
||||
spreadsheetRenderKey.set(`sorted#${Date.now()}`);
|
||||
|
||||
// save > navigate > restore!
|
||||
spreadsheetContainer.saveGridSheetScroll();
|
||||
await goto(`${url.pathname}${url.search}`);
|
||||
@@ -411,9 +391,11 @@
|
||||
showColumnDelete = false;
|
||||
|
||||
try {
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.grids.deleteColumn(databaseId, tableId, $databaseColumnSheetOptions.column.key);
|
||||
await sdk.forProject(page.params.region, page.params.project).grids.deleteColumn({
|
||||
databaseId,
|
||||
tableId,
|
||||
key: $databaseColumnSheetOptions.column.key
|
||||
});
|
||||
|
||||
if ($columnsOrder.includes($databaseColumnSheetOptions.column.key)) {
|
||||
const updatedOrder = $columnsOrder.filter(
|
||||
@@ -532,9 +514,13 @@
|
||||
Object.entries(row).filter(([key]) => !SYSTEM_KEYS.has(key))
|
||||
);
|
||||
|
||||
await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.grids.updateRow(databaseId, tableId, row.$id, onlyData, row.$permissions);
|
||||
await sdk.forProject(page.params.region, page.params.project).grids.updateRow({
|
||||
databaseId,
|
||||
tableId,
|
||||
rowId: row.$id,
|
||||
data: onlyData,
|
||||
permissions: row.$permissions
|
||||
});
|
||||
|
||||
invalidate(Dependencies.ROW);
|
||||
trackEvent(Submit.RowUpdate);
|
||||
@@ -586,6 +572,7 @@
|
||||
}
|
||||
|
||||
async function handleGoToPage(targetPageNum: number): Promise<void> {
|
||||
jumpToPageReactive = 0;
|
||||
if (targetPageNum < 1 || targetPageNum > totalPages) return;
|
||||
|
||||
if (!$paginatedRows.hasPage(targetPageNum)) {
|
||||
@@ -742,8 +729,8 @@
|
||||
{#if row === null}
|
||||
<Spreadsheet.Row.Base
|
||||
{root}
|
||||
virtualItem={item}
|
||||
{index}
|
||||
virtualItem={item}
|
||||
id={`loading-${index}`}
|
||||
select={rowSelection}>
|
||||
{#each $tableColumns as col}
|
||||
@@ -760,7 +747,9 @@
|
||||
{index}
|
||||
id={row?.$id}
|
||||
virtualItem={item}
|
||||
select={rowSelection}>
|
||||
select={rowSelection}
|
||||
showSelectOnHover
|
||||
valueWithoutHover={row.$sequence}>
|
||||
{#each $tableColumns as { id: columnId, isEditable } (columnId)}
|
||||
{@const rowColumn = $columns.find((col) => col.key === columnId)}
|
||||
<Spreadsheet.Cell {root} {isEditable} column={columnId}>
|
||||
@@ -838,6 +827,7 @@
|
||||
{:else}
|
||||
{@const value = row[columnId]}
|
||||
{@const formatted = formatColumn(row[columnId])}
|
||||
{@const isEmptyArray = formatted === 'Empty'}
|
||||
{@const isDatetimeAttribute = rowColumn.type === 'datetime'}
|
||||
{@const isEncryptedAttribute =
|
||||
isString(rowColumn) && rowColumn.encrypt}
|
||||
@@ -868,6 +858,8 @@
|
||||
</Tooltip>
|
||||
{:else if formatted === 'null'}
|
||||
<Badge variant="secondary" content="NULL" size="xs" />
|
||||
{:else if isEmptyArray}
|
||||
<Badge variant="secondary" content={formatted} size="xs" />
|
||||
{:else}
|
||||
<Typography.Text truncate>
|
||||
{formatted}
|
||||
@@ -878,7 +870,7 @@
|
||||
<svelte:fragment slot="cell-editor">
|
||||
<EditRowCell
|
||||
column={rowColumn}
|
||||
row={paginatedRows.items[index]}
|
||||
row={paginatedRows.getItemAtVirtualIndex(index)}
|
||||
onRowStructureUpdate={updateRowContents}
|
||||
on:change={(row) => paginatedRows.update(index, row.detail)}
|
||||
on:revert={(row) =>
|
||||
@@ -985,65 +977,65 @@
|
||||
title={selectedRows.length === 1 ? 'Delete Row' : 'Delete Rows'}>
|
||||
{@const isSingle = selectedRowForDelete !== null}
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{#if isSingle}
|
||||
Are you sure you want to delete this document from <b>{$table.name}</b>?
|
||||
Are you sure you want to delete this row from <b>{$table.name}</b>?
|
||||
{:else}
|
||||
Are you sure you want to delete <b>{selectedRows.length}</b>
|
||||
{selectedRows.length > 1 ? 'rows' : 'row'} from <b>{$table.name}</b>?
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if relatedColumns?.length}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'relation', width: 150 },
|
||||
{ id: 'setting', width: 150 },
|
||||
{ id: 'desc' }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="relation" {root}>Relation</Table.Header.Cell>
|
||||
<Table.Header.Cell column="setting" {root}>Setting</Table.Header.Cell>
|
||||
<Table.Header.Cell column="desc" {root} />
|
||||
</svelte:fragment>
|
||||
{#each relatedColumns as attr}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="relation" {root}>
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
{#if attr.twoWay}
|
||||
<span class="icon-switch-horizontal"></span>
|
||||
{:else}
|
||||
<span class="icon-arrow-sm-right"></span>
|
||||
{/if}
|
||||
<span data-private>{attr.key}</span>
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="setting" {root}>
|
||||
{attr.onDelete}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="desc" {root}>
|
||||
{Deletion[attr.onDelete]}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Alert>To change the selection edit the relationship settings.</Alert>
|
||||
<ul>
|
||||
<InputChoice
|
||||
id="delete"
|
||||
label="Delete"
|
||||
showLabel={false}
|
||||
bind:value={deleteConfirmationChecked}>
|
||||
Delete {isSingle ? 'document' : 'documents'} from
|
||||
<span data-private>{$table.name}</span>
|
||||
</InputChoice>
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if relatedColumns?.length}
|
||||
<Table.Root
|
||||
let:root
|
||||
columns={[
|
||||
{ id: 'relation', width: 150 },
|
||||
{ id: 'setting', width: 150 },
|
||||
{ id: 'desc' }
|
||||
]}>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="relation" {root}>Relation</Table.Header.Cell>
|
||||
<Table.Header.Cell column="setting" {root}>Setting</Table.Header.Cell>
|
||||
<Table.Header.Cell column="desc" {root} />
|
||||
</svelte:fragment>
|
||||
{#each relatedColumns as attr}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="relation" {root}>
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
{#if attr.twoWay}
|
||||
<span class="icon-switch-horizontal"></span>
|
||||
{:else}
|
||||
<span class="icon-arrow-sm-right"></span>
|
||||
{/if}
|
||||
<span data-private>{attr.key}</span>
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="setting" {root}>
|
||||
{attr.onDelete}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="desc" {root}>
|
||||
{Deletion[attr.onDelete]}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<Alert>To change the selection edit the relationship settings.</Alert>
|
||||
<ul>
|
||||
<InputChoice
|
||||
id="delete"
|
||||
label="Delete"
|
||||
showLabel={false}
|
||||
bind:value={deleteConfirmationChecked}>
|
||||
Delete {isSingle ? 'document' : 'documents'} from
|
||||
<span data-private>{$table.name}</span>
|
||||
</InputChoice>
|
||||
</ul>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
{/if}
|
||||
</Confirm>
|
||||
|
||||
<Confirm
|
||||
@@ -1087,7 +1079,7 @@
|
||||
}
|
||||
|
||||
/* TODO: not good! */
|
||||
& :global(input) {
|
||||
& :global(input[type='text']) {
|
||||
padding-inline: 8px !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user