Merge pull request #2486 from appwrite/revamp-designs

This commit is contained in:
Darshan
2025-11-12 15:11:01 +05:30
committed by GitHub
34 changed files with 2401 additions and 646 deletions
+16 -6
View File
@@ -20,21 +20,23 @@
};
type ButtonProps = {
isButton: true;
isButton: boolean;
href?: never;
};
type AnchorProps = {
href: string;
isButton?: never;
isButton?: boolean;
external?: boolean;
};
let classes = '';
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps;
export let isDashed = false;
export let isButton = false;
export let isDashed: boolean = false;
export let isButton: boolean = false;
export let href: string = null;
let classes = '';
export let external: boolean = false;
export { classes as class };
export let style = '';
export let padding: $$Props['padding'] = 'm';
@@ -45,7 +47,15 @@
</script>
{#if href}
<Card.Link class={resolvedClasses} {href} {style} {padding} {radius} {variant} on:click>
<Card.Link
{href}
{style}
{padding}
{radius}
{variant}
on:click
class={resolvedClasses}
{...external ? { target: '_blank' } : {}}>
<Layout.Stack gap="xl">
<slot />
</Layout.Stack>
+9 -2
View File
@@ -14,6 +14,7 @@
onDeletePoint: (index: number) => void;
onChangePoint: (pointIndex: number, coordIndex: number, newValue: number) => void;
addLineButton?: Snippet;
disabled?: boolean;
};
let {
@@ -24,7 +25,8 @@
onAddPoint,
onDeletePoint,
onChangePoint,
addLineButton
addLineButton,
disabled
}: Props = $props();
function isDeleteDisabled(index: number) {
@@ -40,6 +42,7 @@
<Layout.Stack>
{#each values as value, index}
<InputPoint
{disabled}
{nullable}
values={value}
deletePoints
@@ -52,7 +55,11 @@
{#if values}
<Layout.Stack direction="row" gap="s" alignItems="center">
<Button size="xs" compact on:click={() => onAddPoint(-1)} disabled={nullable}>
<Button
size="xs"
compact
on:click={() => onAddPoint(-1)}
disabled={nullable || disabled}>
<Icon icon={IconPlus} size="s" /> Add coordinate
</Button>
{@render addLineButton?.()}
+5 -2
View File
@@ -10,6 +10,7 @@
deletePoints?: boolean;
onDeletePoint?: () => void;
disableDelete?: boolean;
disabled?: boolean;
onChangePoint: (index: number, newValue: number) => void;
}
@@ -21,7 +22,8 @@
deletePoints = false,
disableDelete = false,
onDeletePoint,
onChangePoint
onChangePoint,
disabled
}: Props = $props();
</script>
@@ -38,6 +40,7 @@
placeholder="Enter value"
step={0.0001}
value={values[index]}
{disabled}
on:change={(e) => onChangePoint(index, Number.parseFloat(`${e.detail}`))} />
{/each}
{/if}
@@ -45,7 +48,7 @@
<Button
size="s"
secondary
disabled={nullable || disableDelete}
disabled={nullable || disableDelete || disabled}
on:click={() => onDeletePoint?.()}>
<Icon icon={IconX} size="s" />
</Button>
+4 -1
View File
@@ -17,6 +17,7 @@
coordIndex: number,
newValue: number
) => void;
disabled?: boolean;
};
let {
@@ -26,7 +27,8 @@
onAddPoint,
onAddLine,
onDeletePoint,
onChangePoint
onChangePoint,
disabled
}: Props = $props();
</script>
@@ -34,6 +36,7 @@
{#each values as value, index}
<Layout.Stack gap="xs">
<InputLine
{disabled}
values={value}
onAddPoint={() => onAddPoint(index)}
{nullable}
@@ -0,0 +1,65 @@
<script lang="ts">
import { page } from '$app/state';
import { resolve } from '$app/paths';
import { goto } from '$app/navigation';
import Input from './input.svelte';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { tableColumnSuggestions } from './store';
let {
show = $bindable(false)
}: {
show?: boolean;
} = $props();
const isOnRowsPage = $derived(page.route?.id?.endsWith('table-[table]'));
function resetSuggestionsStore() {
show = false;
$tableColumnSuggestions.table = null;
$tableColumnSuggestions.context = null;
$tableColumnSuggestions.force = false;
$tableColumnSuggestions.enabled = false;
$tableColumnSuggestions.thinking = false;
}
async function triggerColumnSuggestions() {
// set table info. first!
$tableColumnSuggestions.table = {
id: page.params.table,
name: page.data.table?.name ?? 'Table'
};
if (!isOnRowsPage) {
await goto(
resolve(
'/(console)/project-[region]-[project]/databases/database-[database]/table-[table]',
{
region: page.params.region,
project: page.params.project,
database: page.params.database,
table: page.params.table
}
)
);
}
$tableColumnSuggestions.force = true;
$tableColumnSuggestions.enabled = true;
show = false;
}
</script>
<Modal bind:show title="Suggest columns" onSubmit={triggerColumnSuggestions}>
<Input isModal />
<svelte:fragment slot="footer">
<Button text on:click={resetSuggestionsStore}>Cancel</Button>
<Button submit>Generate columns</Button>
</svelte:fragment>
</Modal>
@@ -71,8 +71,16 @@
border: 1.25px solid rgba(253, 54, 110, 0.12);
padding: 5px 0;
min-width: 40px;
width: 40px !important;
height: 40px !important;
& svg {
width: 30px;
height: 30px;
flex-shrink: 0;
aspect-ratio: 1/1;
}
}
:global(.ai-icon-holder.notification) {
@@ -0,0 +1,22 @@
<script lang="ts">
import { Layout } from '@appwrite.io/pink-svelte';
</script>
<Layout.Stack
inline
alignItems="center"
justifyContent="center"
style="width: 18px !important; height: 20px !important;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.00049 2C5.55277 2 6.00049 2.44772 6.00049 3V4H7.00049C7.55277 4 8.00049 4.44772 8.00049 5C8.00049 5.55228 7.55277 6 7.00049 6H6.00049V7C6.00049 7.55228 5.55277 8 5.00049 8C4.4482 8 4.00049 7.55228 4.00049 7V6H3.00049C2.4482 6 2.00049 5.55228 2.00049 5C2.00049 4.44772 2.4482 4 3.00049 4H4.00049V3C4.00049 2.44772 4.4482 2 5.00049 2ZM5.00049 12C5.55277 12 6.00049 12.4477 6.00049 13V14H7.00049C7.55277 14 8.00049 14.4477 8.00049 15C8.00049 15.5523 7.55277 16 7.00049 16H6.00049V17C6.00049 17.5523 5.55277 18 5.00049 18C4.4482 18 4.00049 17.5523 4.00049 17V16H3.00049C2.4482 16 2.00049 15.5523 2.00049 15C2.00049 14.4477 2.4482 14 3.00049 14H4.00049V13C4.00049 12.4477 4.4482 12 5.00049 12Z"
fill="#97979B" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.0004 2C12.4542 2 12.851 2.30548 12.9671 2.74411L14.1464 7.19893L17.5002 9.13381C17.8097 9.3124 18.0004 9.64262 18.0004 10C18.0004 10.3574 17.8097 10.6876 17.5002 10.8662L14.1464 12.8011L12.9671 17.2559C12.851 17.6945 12.4542 18 12.0004 18C11.5467 18 11.1498 17.6945 11.0337 17.2559L9.85451 12.8011L6.50076 10.8662C6.19121 10.6876 6.00049 10.3574 6.00049 10C6.00049 9.64262 6.19121 9.31241 6.50076 9.13382L9.85451 7.19893L11.0337 2.74411C11.1498 2.30548 11.5467 2 12.0004 2Z"
fill="#97979B" />
</svg>
</Layout.Stack>
@@ -8,7 +8,7 @@
mockSuggestions,
type SuggestedIndexSchema
} from './store';
import { Modal, Confirm } from '$lib/components';
import { Modal } from '$lib/components';
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { IndexType, type Models } from '@appwrite.io/console';
@@ -32,7 +32,6 @@
let creatingIndexes = $state(false);
let loadingSuggestions = $state(false);
let indexes = $state<SuggestedIndexSchema[]>([]);
let confirmDismiss = $state(false);
let columnOptions: Array<{
value: string;
label: string;
@@ -195,7 +194,6 @@
function dismissIndexes() {
indexes = [];
confirmDismiss = false;
$showIndexesSuggestions = false;
}
@@ -354,13 +352,7 @@
text
size="s"
disabled={loadingSuggestions || creatingIndexes}
on:click={() => {
if (indexes.length > 0 && !creatingIndexes) {
confirmDismiss = true;
} else {
$showIndexesSuggestions = false;
}
}}>Cancel</Button>
on:click={() => dismissIndexes()}>Cancel</Button>
<Button
size="s"
@@ -389,13 +381,7 @@
}}
cancel={{
disabled: loadingSuggestions || creatingIndexes,
onClick: () => {
if (indexes.length > 0 && !creatingIndexes) {
confirmDismiss = true;
} else {
$showIndexesSuggestions = false;
}
}
onClick: () => dismissIndexes()
}}>
{#if modalError}
<Alert.Inline status="error" title={modalError} />
@@ -540,15 +526,6 @@
{/if}
{/snippet}
<Confirm
confirmDeletion
action="Dismiss"
title="Dismiss indexes"
bind:open={confirmDismiss}
onSubmit={dismissIndexes}>
Are you sure you want to dismiss these suggested indexes? This action cannot be undone.
</Confirm>
<style lang="scss">
// Custom logic to hide the Sheet's
// `X` close button (not configurable via props)
@@ -7,6 +7,12 @@
import { Button, InputTextarea } from '$lib/elements/forms';
import { Card, Layout, Selector, Typography } from '@appwrite.io/pink-svelte';
const {
isModal = false
}: {
isModal?: boolean;
} = $props();
onMount(() => {
if (featureActive) {
$tableColumnSuggestions.enabled = true;
@@ -23,7 +29,9 @@
const subtitle = $derived.by(() => {
return featureActive
? 'Enable AI to suggest useful columns based on your table name'
? isModal
? 'Use AI to suggest useful columns'
: 'Enable AI to suggest useful columns based on your table name'
: 'Sign up for Cloud to generate columns based on your table name';
});
</script>
@@ -42,7 +50,7 @@
</Typography.Text>
</Layout.Stack>
{#if featureActive}
{#if featureActive && !isModal}
<div class="suggestions-switch">
<Selector.Switch
id="suggestions"
@@ -62,7 +70,7 @@
<!-- just being safe with extra guard! -->
{#if $tableColumnSuggestions.enabled && featureActive}
<div transition:slide={{ duration: 200 }}>
<div class="context-input" transition:slide={{ duration: 200 }}>
<InputTextarea
id="context"
rows={3}
@@ -78,4 +86,8 @@
.suggestions-switch :global(button):not(:disabled) {
cursor: pointer;
}
.context-input :global(.input) {
background: var(--bgcolor-neutral-primary);
}
</style>
@@ -1,21 +1,29 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Popover } from '@appwrite.io/pink-svelte';
import { Popover, Tooltip } from '@appwrite.io/pink-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import SideSheet from '../table-[table]/layout/sidesheet.svelte';
let {
children,
tooltipChildren,
mobileFooterChildren,
toggleOnTapClick = true,
onShowStateChanged = null,
enabled = true
enabled = true,
onChildrenClick,
triggerOpen,
headerTooltipText
}: {
children: Snippet<[toggle: (event: Event) => void]>;
tooltipChildren: Snippet<[toggle: (event: Event) => void]>;
mobileFooterChildren?: Snippet<[toggle: (event: Event) => void]>;
toggleOnTapClick?: boolean;
onShowStateChanged?: (showing: boolean) => void;
enabled?: boolean;
onChildrenClick?: () => void;
triggerOpen?: () => boolean;
headerTooltipText?: string;
} = $props();
let showSheet = $state(false);
@@ -25,6 +33,12 @@
showSheet = false;
}
});
$effect(() => {
if ($isSmallViewport && triggerOpen && triggerOpen()) {
showSheet = true;
}
});
</script>
<Popover let:toggle let:showing portal padding="none" placement="bottom-start">
@@ -36,9 +50,19 @@
{@render children(() => (showSheet = false))}
</button>
{:else}
<button style:cursor={enabled ? 'pointer' : undefined}>
{@render children(toggle)}
</button>
<div style:display="grid">
<Tooltip maxWidth="225px" portal disabled={!headerTooltipText || showing} delay={100}>
<button
onclick={() => enabled && onChildrenClick?.()}
style:cursor={enabled ? 'pointer' : undefined}>
{@render children(toggle)}
</button>
<svelte:fragment slot="tooltip">
{headerTooltipText}
</svelte:fragment>
</Tooltip>
</div>
{/if}
<div let:toggle slot="tooltip" style:width="480px" style:padding="16px">
@@ -56,6 +80,10 @@
showSheet = false;
}
}}>
{#snippet footer()}
{@render mobileFooterChildren?.(() => (showSheet = false))}
{/snippet}
{@render tooltipChildren(() => (showSheet = false))}
</SideSheet>
{/if}
@@ -3,6 +3,7 @@ import { IndexType } from '@appwrite.io/console';
import { columnOptions } from '../table-[table]/columns/store';
export type TableColumnSuggestions = {
force: boolean;
enabled: boolean;
thinking: boolean;
context?: string | undefined;
@@ -18,11 +19,15 @@ export type SuggestedColumnSchema = {
key: string;
type: string;
required: boolean;
array?: boolean;
default?: string | number | boolean | number[] | number[][] | number[][][] | null;
size?: number;
min?: number;
max?: number;
format?: string | null;
encrypt?: boolean | null;
elements?: string[];
isPlaceholder?: boolean;
};
export enum IndexOrder {
@@ -43,11 +48,14 @@ export const tableColumnSuggestions = writable<TableColumnSuggestions>({
enabled: false,
context: null,
thinking: false,
table: null
table: null,
force: false
});
export const showIndexesSuggestions = writable<boolean>(false);
export const showColumnsSuggestionsModal = writable<boolean>(false);
export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
total: 7,
columns: [
@@ -68,7 +76,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
formatOptions: null
},
{
name: 'publishedYear',
name: 'year',
type: 'integer',
size: null,
format: null,
@@ -79,7 +87,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
}
},
{
name: 'genre',
name: 'category',
type: 'string',
size: 64,
format: null,
@@ -88,7 +96,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
default: null
},
{
name: 'isbn',
name: 'code',
type: 'string',
size: 13,
required: false,
@@ -96,7 +104,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
default: null
},
{
name: 'language',
name: 'spokenLanguage',
type: 'string',
size: 32,
format: null,
@@ -105,7 +113,7 @@ export const mockSuggestions: { total: number; columns: ColumnInput[] } = {
default: null
},
{
name: 'pageCount',
name: 'count',
type: 'integer',
required: false,
min: 1,
@@ -123,6 +131,7 @@ export type ColumnInput = {
min?: number;
max?: number;
format?: string;
elements?: string[];
formatOptions?: {
min?: number;
max?: number;
@@ -134,6 +143,7 @@ export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): Sugges
key: col.name,
type: col.type,
required: col.required ?? false,
array: false,
default: col.default ?? null,
size: col.type === 'string' ? (col.size ?? undefined) : undefined,
min:
@@ -144,7 +154,8 @@ export function mapSuggestedColumns<T extends ColumnInput>(columns: T[]): Sugges
col.type === 'integer' || col.type === 'double'
? (col.max ?? col.formatOptions?.max ?? undefined)
: undefined,
format: col.format ?? null
format: col.format ?? null,
elements: col.elements ?? undefined
}));
}
@@ -74,6 +74,8 @@
import { isTabletViewport } from '$lib/stores/viewport';
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
import ColumnsSuggestions from '../(suggestions)/columns.svelte';
import { showColumnsSuggestionsModal } from '../(suggestions)';
let editRow: EditRow;
let editRelatedRow: EditRelatedRow;
@@ -608,6 +610,8 @@
</svelte:fragment>
</Dialog>
<ColumnsSuggestions bind:show={$showColumnsSuggestionsModal} />
<IndexesSuggestions />
<style lang="scss">
@@ -6,7 +6,7 @@
import { Container } from '$lib/layout';
import { preferences } from '$lib/stores/preferences';
import { canWriteTables, canWriteRows } from '$lib/stores/roles';
import { Icon, Layout, Divider, Tooltip } from '@appwrite.io/pink-svelte';
import { Icon, Layout, Divider, Tooltip, Typography, Link } from '@appwrite.io/pink-svelte';
import type { PageData } from './$types';
import {
table,
@@ -27,9 +27,11 @@
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { isSmallViewport } from '$lib/stores/viewport';
import {
IconBookOpen,
IconChevronDown,
IconChevronUp,
IconPlus,
IconViewBoards,
IconRefresh
} from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
@@ -37,9 +39,15 @@
import CreateRow from './rows/create.svelte';
import { onDestroy } from 'svelte';
import { isCloud } from '$lib/system';
import { Empty as SuggestionsEmptySheet, tableColumnSuggestions } from '../(suggestions)';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import {
Empty as SuggestionsEmptySheet,
tableColumnSuggestions,
showColumnsSuggestionsModal
} from '../(suggestions)';
import EmptySheetCards from './layout/emptySheetCards.svelte';
import IconAI from '../(suggestions)/icon/aiForButton.svelte';
export let data: PageData;
@@ -160,7 +168,11 @@
alignItems="center"
justifyContent="flex-end"
style="padding-right: 40px;">
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Layout.Stack
gap="s"
direction="row"
alignItems="center"
justifyContent="flex-end">
<Button
secondary
event={Click.DatabaseImportCsv}
@@ -188,8 +200,8 @@
preferences.setKey('tableHeaderExpanded', $expandTabs);
}}>
<Icon
icon={!$expandTabs ? IconChevronDown : IconChevronUp}
size="s" />
size="s"
icon={!$expandTabs ? IconChevronDown : IconChevronUp} />
</Button>
<Tooltip disabled={isRefreshing || !data.rows.total} placement="top">
@@ -231,7 +243,7 @@
</Container>
<div class="databases-spreadsheet">
{#if hasColumns && hasValidColumns}
{#if hasColumns && hasValidColumns && $tableColumnSuggestions.force !== true}
{#if data.rows.total}
<Divider />
<SpreadSheet {data} bind:showRowCreateSheet={$showRowCreateSheet} />
@@ -239,58 +251,102 @@
<EmptySheet
mode="rows-filtered"
title="There are no rows that match your filters"
customColumns={createTableColumns($table.columns, selected)}
actions={{
primary: {
text: 'Clear filters',
onClick: () => {
customColumns={createTableColumns($table.columns, selected)}>
{#snippet actions()}
<Button
size="s"
secondary
on:click={() => {
queries.clearAll();
queries.apply();
trackEvent(Submit.FilterClear, {
source: 'database_tables'
});
}
}
}} />
}}>
Clear filters
</Button>
{/snippet}
</EmptySheet>
{:else}
<EmptySheet
mode="rows"
customColumns={createTableColumns($table.columns, selected)}
showActions={$canWriteRows}
actions={{
primary: {
text: 'Create rows',
onClick: () => {
customColumns={createTableColumns($table.columns, selected)}>
{#snippet actions()}
<EmptySheetCards
icon={IconPlus}
title="Create rows"
subtitle="Create rows manually"
onClick={() => {
$showRowCreateSheet.show = true;
}
},
random: {
onClick: () => {
}} />
<EmptySheetCards
icon={IconViewBoards}
title="Generate sample data"
subtitle="Generate data for testing"
onClick={() => {
$randomDataModalState.show = true;
}
}
}} />
}} />
{/snippet}
</EmptySheet>
{/if}
{:else if isCloud && canShowSuggestionsSheet}
<SuggestionsEmptySheet />
<SuggestionsEmptySheet userColumns={$tableColumns} userDataRows={data.rows.rows} />
{:else}
<EmptySheet
mode="rows"
title="You have no columns yet"
showActions={$canWriteTables}
actions={{
primary: {
text: 'Create column',
onClick: async () => {
<EmptySheet mode="rows" showActions={$canWriteTables} title="You have no columns yet">
{#snippet subtitle()}
{#if !isCloud}
<!-- shown on self-hosted -->
<Typography.Text align="center">
Need a hand? Learn more in the
<Link.Anchor
target="_blank"
href="https://appwrite.io/docs/products/databases">
docs.
</Link.Anchor>
</Typography.Text>
{/if}
{/snippet}
{#snippet actions()}
{#if isCloud}
<!-- shown on cloud -->
<EmptySheetCards
icon={IconAI}
title="Suggest columns"
subtitle="Use AI to generate columns"
onClick={() => {
$showColumnsSuggestionsModal = true;
}} />
{/if}
<EmptySheetCards
icon={IconPlus}
title="Create column"
subtitle="Create columns manually"
onClick={() => {
$showCreateColumnSheet.show = true;
}
},
random: {
onClick: () => {
}} />
<EmptySheetCards
icon={IconViewBoards}
title="Generate sample data"
subtitle="Generate data for testing"
onClick={() => {
$randomDataModalState.show = true;
}
}
}} />
}} />
{#if isCloud}
<!-- shown on cloud because self-hosted shows a link above -->
<EmptySheetCards
icon={IconBookOpen}
title="Documentation"
subtitle="Read the Appwrite docs"
href="https://appwrite.io/docs/products/databases" />
{/if}
{/snippet}
</EmptySheet>
{/if}
</div>
{/key}
@@ -382,8 +382,9 @@
{column.key}{column.array ? '[]' : undefined}
{/if}
</Typography.Text>
{#if isString(column) && column.encrypt}
<Tooltip>
<Tooltip portal>
<Icon
size="s"
icon={IconLockClosed}
@@ -391,13 +392,7 @@
<div slot="tooltip">Encrypted</div>
</Tooltip>
{/if}
</Layout.Stack>
<Layout.Stack
gap="s"
inline
direction="row"
alignItems="center"
style="flex:0 0 auto; white-space:nowrap;">
{#if column.status !== 'available'}
<Badge
size="s"
@@ -42,6 +42,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnBoolean> = {
required: false,
array: false,
@@ -77,7 +78,7 @@
id="default"
label="Default value"
placeholder="Select a value"
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
options={[
{ label: 'NULL', value: null },
{ label: 'True', value: true },
@@ -85,4 +86,8 @@
]}
bind:value={data.default} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -46,6 +46,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnDatetime>;
let savedDefault = data.default;
@@ -77,7 +78,11 @@
id="default"
label="Default value"
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -42,6 +42,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnEmail>;
let savedDefault = data.default;
@@ -74,7 +75,11 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -46,6 +46,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnEnum>;
let savedDefault = data.default;
@@ -67,6 +68,7 @@
array: false,
...data
});
$: listen(data);
$: handleDefaultState($required || $array);
@@ -103,9 +105,13 @@
<InputSelect
id="default"
label="Default value"
disabled={data.array || data.required}
disabled={data.array || data.required || disabled}
placeholder="Select a value"
{options}
bind:value={data.default} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -47,6 +47,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnFloat> = {
required: false,
min: 0,
@@ -86,15 +87,19 @@
placeholder="Enter size"
bind:value={data.min}
step={0.1}
{disabled}
required={editing} />
<InputNumber
id="max"
label="Max"
placeholder="Enter size"
bind:value={data.max}
step={0.1}
{disabled}
required={editing} />
</Layout.Stack>
<InputNumber
id="default"
label="Default value"
@@ -102,8 +107,12 @@
min={data.min}
max={data.max}
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array}
step={0.1} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -47,7 +47,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnInteger> = {
required: false,
min: 0,
@@ -84,16 +84,20 @@
<InputNumber
id="min"
label="Min"
{disabled}
placeholder="Enter size"
bind:value={data.min}
required={editing} />
<InputNumber
id="max"
label="Max"
{disabled}
placeholder="Enter size"
bind:value={data.max}
required={editing} />
</Layout.Stack>
<InputNumber
id="default"
label="Default value"
@@ -101,7 +105,11 @@
min={data.min}
max={data.max}
bind:value={data.default}
disabled={data.required || data.array}
nullable={!data.required && !data.array} />
disabled={data.required || data.array || disabled}
nullable={(!data.required && !data.array) || disabled} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -41,6 +41,7 @@
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnIp>;
let savedDefault = data.default;
@@ -73,7 +74,11 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -42,11 +42,16 @@
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnLine>;
editing?: boolean;
disabled?: boolean;
data?: Partial<Models.ColumnLine>;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let {
data = { required: false, default: null },
editing = false,
disabled = false
}: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
@@ -106,6 +111,7 @@
size="s"
id="required"
label="Required"
{disabled}
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
@@ -116,6 +122,7 @@
size="s"
id="default"
label="Default value"
{disabled}
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
@@ -134,11 +141,13 @@
<Typography.Caption variant="400">Optional</Typography.Caption>
</Layout.Stack>
{/if}
<InputLine
{disabled}
values={defaultChecked ? data.default : null}
onAddPoint={() => pushCoordinate()}
onDeletePoint={deleteCoordinate}
onChangePoint={(pointIndex: number, coordIndex: number, newValue: number) => {
onChangePoint={(pointIndex, coordIndex, newValue) => {
if (data.default) {
data.default[pointIndex][coordIndex] = newValue;
data.default = [...data.default];
@@ -42,11 +42,19 @@
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPoint>;
editing?: boolean;
disabled?: boolean;
data?: Partial<Models.ColumnPoint>;
}
let { data = { required: false, default: null }, editing }: Props = $props();
let {
data = {
default: null,
required: false
},
editing = false,
disabled = false
}: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
@@ -96,6 +104,7 @@
size="s"
id="required"
label="Required"
{disabled}
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
@@ -106,6 +115,7 @@
size="s"
id="default"
label="Default value"
{disabled}
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
@@ -126,6 +136,7 @@
{/if}
<InputPoint
{disabled}
values={defaultChecked ? data.default : null}
onChangePoint={(index, newValue) => {
if (data.default) {
@@ -42,11 +42,19 @@
import { onMount } from 'svelte';
interface Props {
data?: Partial<Models.ColumnPolygon>;
editing?: boolean;
disabled?: boolean;
data?: Partial<Models.ColumnPolygon>;
}
let { data = { required: false, default: null }, editing = false }: Props = $props();
let {
data = {
default: null,
required: false
},
editing = false,
disabled = false
}: Props = $props();
let savedDefault = $state(data.default);
let defaultChecked = $state(!!data.default);
@@ -117,6 +125,7 @@
size="s"
id="required"
label="Required"
{disabled}
bind:checked={$required}
on:change={(e) => {
if (e.detail) defaultChecked = false;
@@ -128,6 +137,7 @@
size="s"
id="default"
label="Default value"
{disabled}
bind:checked={defaultChecked}
on:change={(e) => {
if (e.detail) {
@@ -148,6 +158,7 @@
{/if}
<InputPolygon
{disabled}
values={defaultChecked ? data.default : null}
onAddLine={pushLine}
onAddPoint={pushCoordinate}
@@ -69,6 +69,7 @@
// Props
export let editing = false;
export let disabled = false;
export let data: Models.ColumnRelationship;
// Constants
@@ -158,7 +159,7 @@
bind:group={way}
name="one"
value="one"
disabled={editing}
disabled={editing || disabled}
icon={IconArrowSmRight}>
One Relation column within this table
</Card.Selector>
@@ -167,7 +168,7 @@
bind:group={way}
name="two"
value="two"
disabled={editing}
disabled={editing || disabled}
icon={IconSwitchHorizontal}>
One Relation column within this table and another within the related table
</Card.Selector>
@@ -180,7 +181,7 @@
placeholder="Select a table"
bind:value={data.relatedTable}
on:change={updateKeyName}
disabled={editing}
disabled={editing || disabled}
options={tables?.map((n) => ({ value: n.$id, label: `${n.name} (${n.$id})` })) ?? []} />
{#if data?.relatedTable}
@@ -190,7 +191,8 @@
placeholder="Enter key"
bind:value={data.key}
helper="Allowed characters: a-z, A-Z, 0-9, -, ."
required />
required
{disabled} />
{#if way === 'two'}
<InputText
@@ -199,6 +201,7 @@
placeholder="Enter key"
bind:value={data.twoWayKey}
required
{disabled}
helper="Allowed characters: a-z, A-Z, 0-9, -, . Once created, column key cannot be
adjusted to maintain data integrity."
readonly={editing} />
@@ -211,7 +214,7 @@
required
placeholder="Select a relation"
options={relationshipType}
disabled={editing} />
disabled={editing || disabled} />
<div class="u-flex u-flex-vertical u-gap-16">
<Box>
@@ -251,6 +254,7 @@
label="On deleting a row"
bind:value={data.onDelete}
required
{disabled}
placeholder="Select a deletion method"
options={deleteOptions} />
{/if}
@@ -4,22 +4,24 @@
let {
required = $bindable(false),
array = $bindable(false),
editing = false
editing = false,
disabled = false
}: {
required: boolean;
array: boolean;
editing: boolean;
editing?: boolean;
disabled?: boolean;
} = $props();
</script>
<Tooltip disabled={!array} maxWidth="275px" placement="bottom-start">
<Tooltip disabled={!array || disabled} maxWidth="275px" placement="bottom-start">
<div style:width="fit-content">
<Selector.Checkbox
size="s"
id="required"
label="Required"
bind:checked={required}
disabled={array}
disabled={array || disabled}
description="Indicate whether this column is required." />
</div>
@@ -28,14 +30,14 @@
</svelte:fragment>
</Tooltip>
<Tooltip disabled={!(required || editing)} maxWidth="275px" placement="bottom-start">
<Tooltip disabled={!(required || editing) || disabled} maxWidth="275px" placement="bottom-start">
<div style:width="fit-content">
<Selector.Checkbox
size="s"
id="array"
label="Array"
bind:checked={array}
disabled={required || editing}
disabled={required || editing || disabled}
description="Indicate whether this column is an array. Defaults to an empty array." />
</div>
@@ -56,6 +56,8 @@
};
export let editing = false;
export let disabled = false;
export let autoIncreaseSize = false;
let savedDefault = data.default;
@@ -83,12 +85,17 @@
// Check plan on cloud, always allow on self-hosted
$: supportsStringEncryption = isCloud ? $currentPlan?.databasesAllowEncrypt : true;
$: if (autoIncreaseSize && data.encrypt && data.size < 150) {
data.size = 150;
}
</script>
<InputNumber
id="size"
label="Size"
required
{disabled}
placeholder="Enter size"
bind:value={data.size}
min={supportsStringEncryption && data.encrypt ? 150 : undefined}
@@ -103,30 +110,34 @@
placeholder="Enter string"
maxlength={data.size}
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
<Layout.Stack gap="xs" direction="column">
<div
class="popover-holder"
class:cursor-not-allowed={editing}
class:disabled-checkbox={!supportsStringEncryption || editing}>
class:cursor-not-allowed={editing || disabled}
class:disabled-checkbox={!supportsStringEncryption || editing || disabled}>
<Layout.Stack inline gap="s" alignItems="flex-start" direction="row">
<Popover let:toggle placement="bottom-start">
<Selector.Checkbox
size="s"
id="encrypt"
bind:checked={data.encrypt}
disabled={!supportsStringEncryption || editing} />
disabled={!supportsStringEncryption || editing || disabled} />
<Layout.Stack gap="xxs" direction="column">
<button
type="button"
disabled={editing}
disabled={editing || disabled}
class:cursor-pointer={!editing}
class:cursor-not-allowed={editing}
class:cursor-not-allowed={editing || disabled}
on:click={(e) => {
if (!supportsStringEncryption) {
toggle(e);
@@ -41,8 +41,9 @@
import { createConservative } from '$lib/helpers/stores';
import RequiredArrayCheckboxes from './requiredArrayCheckboxes.svelte';
export let data: Partial<Models.ColumnUrl>;
export let editing = false;
export let disabled = false;
export let data: Partial<Models.ColumnUrl>;
let savedDefault = data.default;
@@ -74,7 +75,11 @@
label="Default value"
placeholder="Enter value"
bind:value={data.default}
disabled={data.required || data.array}
disabled={data.required || data.array || disabled}
nullable={!data.required && !data.array} />
<RequiredArrayCheckboxes {editing} bind:array={data.array} bind:required={data.required} />
<RequiredArrayCheckboxes
{editing}
{disabled}
bind:array={data.array}
bind:required={data.required} />
@@ -1,9 +1,8 @@
<script lang="ts">
import { page } from '$app/state';
import { type Columns, type ColumnDirection } from './store';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Layout } from '@appwrite.io/pink-svelte';
import { Alert, Layout, Link } from '@appwrite.io/pink-svelte';
import { InputSelect, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
@@ -12,6 +11,12 @@
import { preferences } from '$lib/stores/preferences';
import { onMount } from 'svelte';
import { showColumnsSuggestionsModal } from '../(suggestions)/store';
import IconAINotification from '../(suggestions)/icon/aiNotification.svelte';
import { type Columns, type ColumnDirection, showCreateColumnSheet } from './store';
import { isCloud } from '$lib/system';
import { slide } from 'svelte/transition';
let {
direction = null,
column = null,
@@ -35,6 +40,8 @@
const tableId = page.params.table;
const databaseId = page.params.database;
let showSuggestionsAlert = $state(true);
let key: string = $state(column?.key ?? null);
let data: Partial<Columns> = $state({
required: column?.required ?? false,
@@ -180,6 +187,22 @@
</script>
<Layout.Stack gap="xl">
{#if isCloud && showSuggestionsAlert}
<div class="custom-inline-alert" transition:slide>
<Alert.Inline dismissible on:dismiss={() => (showSuggestionsAlert = false)}>
<svelte:fragment slot="icon">
<IconAINotification />
</svelte:fragment>
Need help? Let AI <Link.Button
on:click={() => {
$showCreateColumnSheet.show = false;
$showColumnsSuggestionsModal = true;
}}>suggest columns</Link.Button> based on your data
</Alert.Inline>
</div>
{/if}
<Layout.Stack direction="row">
<InputText
id="key"
@@ -209,3 +232,22 @@
<ColumnComponent bind:data onclose={() => ($option = null)} />
{/if}
</Layout.Stack>
<style lang="scss">
.custom-inline-alert {
& :global(article) {
border-radius: var(--border-radius-medium);
padding: var(--space-4, 8px);
background: var(--bgcolor-neutral-primary);
border: var(--border-width-s) solid var(--border-neutral);
}
& :global(div:first-child > :nth-child(2)) {
align-self: center;
}
& :global(.ai-icon-holder.notification) {
height: 36px !important;
}
}
</style>
@@ -23,6 +23,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import {
IconBookOpen,
IconDotsHorizontal,
IconEye,
IconPlus,
@@ -37,6 +38,10 @@
import { showCreateColumnSheet } from '../store';
import { isSmallViewport } from '$lib/stores/viewport';
import { page } from '$app/state';
import { showIndexesSuggestions, showColumnsSuggestionsModal } from '../../(suggestions)';
import IconAI from '../../(suggestions)/icon/aiForButton.svelte';
import EmptySheetCards from '../layout/emptySheetCards.svelte';
import { isCloud } from '$lib/system';
import { realtime } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
@@ -66,14 +71,14 @@
const spreadsheetColumns = $derived([
{
id: 'key',
width: getColumnWidth('key', $isSmallViewport ? 250 : 200),
minimumWidth: $isSmallViewport ? 250 : 200,
width: getColumnWidth('key', 250),
minimumWidth: 250,
resizable: true
},
{
id: 'type',
width: getColumnWidth('type', 120),
minimumWidth: 120,
width: getColumnWidth('type', 200),
minimumWidth: 200,
resizable: true
},
{
@@ -296,27 +301,100 @@
</Spreadsheet.Root>
</SpreadsheetContainer>
{:else}
<EmptySheet
mode="indexes"
actions={{
primary: {
onClick: () => (showCreateIndex = true),
disabled: !$table?.columns?.length
}
}} />
<EmptySheet mode="indexes" showActions={$canWriteTables}>
{#snippet subtitle()}
{#if isCloud}
<Typography.Text align="center">
Need a hand? Learn more in the
<Link.Anchor
target="_blank"
href="https://appwrite.io/docs/products/databases/tables#indexes">
docs.
</Link.Anchor>
</Typography.Text>
{/if}
{/snippet}
{#snippet actions()}
{#if isCloud}
<EmptySheetCards
icon={IconAI}
title="Suggest indexes"
disabled={!$table?.columns?.length}
subtitle="Use AI to generate indexes"
onClick={() => {
showIndexesSuggestions.update(() => true);
}} />
{/if}
<EmptySheetCards
icon={IconPlus}
title="Create index"
disabled={!$table?.columns?.length}
subtitle="Create indexes manually"
onClick={() => {
showCreateIndex = true;
}} />
{#if !isCloud}
<EmptySheetCards
icon={IconBookOpen}
title="Documentation"
subtitle="Read the Appwrite docs"
href="https://appwrite.io/docs/products/databases/tables#indexes" />
{/if}
{/snippet}
</EmptySheet>
{/if}
{:else}
<EmptySheet
mode="indexes"
title="You have no columns yet"
actions={{
primary: {
text: 'Create columns',
onClick: async () => {
$showCreateColumnSheet.show = true;
}
}
}} />
<EmptySheet mode="indexes" title="You have no columns yet" showActions={$canWriteTables}>
{#snippet subtitle()}
{#if isCloud}
<Typography.Text align="center">
Need a hand? Learn more in the
<Link.Anchor
target="_blank"
href="https://appwrite.io/docs/products/databases/tables#columns">
docs.
</Link.Anchor>
</Typography.Text>
{/if}
{/snippet}
{#snippet actions()}
{#if isCloud}
<EmptySheetCards
icon={IconAI}
title="Suggest columns"
subtitle="Use AI to generate columns"
onClick={() => {
$showColumnsSuggestionsModal = true;
}} />
<EmptySheetCards
icon={IconPlus}
title="Create column"
subtitle="Create columns manually"
onClick={() => {
$showCreateColumnSheet.show = true;
}} />
{:else}
<EmptySheetCards
icon={IconPlus}
title="Create column"
subtitle="Create columns manually"
onClick={() => {
$showCreateColumnSheet.show = true;
}} />
<EmptySheetCards
icon={IconBookOpen}
title="Documentation"
subtitle="Read the Appwrite docs"
href="https://appwrite.io/docs/products/databases/tables#columns" />
{/if}
{/snippet}
</EmptySheet>
{/if}
{#if selectedIndexes.length > 0}
@@ -19,43 +19,57 @@
expandTabs
} from '../store';
import SpreadsheetContainer from './spreadsheet.svelte';
import { onDestroy, onMount } from 'svelte';
import { onDestroy, onMount, type Snippet } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
import { columnOptions } from '../columns/store';
type Mode = 'rows' | 'rows-filtered' | 'indexes';
interface Action {
text?: string;
disabled?: boolean;
onClick?: () => void;
}
const {
mode,
showActions = true,
customColumns = [],
title,
actions
subtitle,
actions,
showActions
} = $props<{
mode: Mode;
showActions?: boolean;
customColumns?: Column[];
title?: string;
actions?: {
primary?: Action;
random?: Action;
};
subtitle?: Snippet;
actions?: Snippet;
showActions?: boolean;
}>();
let spreadsheetContainer: HTMLElement;
let headerElement: HTMLElement | null = null;
let resizeObserver: ResizeObserver;
let overlayOffsetHandler: ResizeObserver;
let overlayLeftOffset = $state('0px');
let overlayTopOffset = $state('auto');
let dynamicOverlayHeight = $state('60.5vh');
const baseColProps = { draggable: false, resizable: false };
const updateOverlayLeftOffset = () => {
if (spreadsheetContainer) {
const containerRect = spreadsheetContainer.getBoundingClientRect();
overlayLeftOffset = `${containerRect.left}px`;
}
// calculate vertical top position
if (!headerElement || !headerElement.isConnected) {
headerElement = spreadsheetContainer?.querySelector('[role="rowheader"]');
}
if (headerElement) {
const headerRect = headerElement.getBoundingClientRect();
overlayTopOffset = `${headerRect.bottom}px`;
}
};
const updateOverlayHeight = () => {
if (!spreadsheetContainer) return;
@@ -82,6 +96,9 @@
if (spreadsheetContainer) {
resizeObserver = new ResizeObserver(debouncedUpdateOverlayHeight);
resizeObserver.observe(spreadsheetContainer);
overlayOffsetHandler = new ResizeObserver(updateOverlayLeftOffset);
overlayOffsetHandler.observe(spreadsheetContainer);
}
});
@@ -89,74 +106,141 @@
if (resizeObserver) {
resizeObserver.disconnect();
}
if (overlayOffsetHandler) {
overlayOffsetHandler.disconnect();
}
});
const getCustomColumns = (): Column[] =>
customColumns.map((col: Column) => ({
...col,
width: 180,
hide: false,
icon: columnOptions.find((colOpt) => colOpt.type === col?.type)?.icon,
...baseColProps
}));
const getRowColumns = (): Column[] => [
{
id: '$id',
title: '$id',
type: 'string',
width: 180,
icon: IconFingerPrint,
...baseColProps
},
...getCustomColumns(),
{
id: '$createdAt',
title: '$createdAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
...baseColProps
},
{
id: '$updatedAt',
title: '$updatedAt',
type: 'datetime',
width: 180,
icon: IconCalendar,
...baseColProps
},
{
id: 'actions',
title: '',
type: 'string',
icon: IconPlus,
width: customColumns.length ? 555 : 832,
...baseColProps
},
{
id: 'empty',
title: '',
type: 'string',
...baseColProps
}
];
const getRowColumns = (): Column[] => {
const minColumnWidth = 180;
const fixedWidths = { id: 180, actions: 40 };
const hasCustomColumns = customColumns.length > 0;
const getIndexesColumns = (): Column[] =>
[
const customColumnsData = getCustomColumns();
// Calculate column widths based on whether we have custom columns
let columnWidths = {
id: fixedWidths.id,
createdAt: fixedWidths.id,
updatedAt: fixedWidths.id,
custom: minColumnWidth,
actions: hasCustomColumns ? fixedWidths.actions : 1387
};
if (hasCustomColumns) {
const equalWidthColumns = [
...customColumnsData,
{ id: '$createdAt' },
{ id: '$updatedAt' }
];
const totalBaseWidth =
fixedWidths.id + fixedWidths.actions + equalWidthColumns.length * minColumnWidth;
const viewportWidth =
spreadsheetContainer?.clientWidth ||
(typeof window !== 'undefined' ? window.innerWidth : totalBaseWidth);
const excessSpace = Math.max(0, viewportWidth - totalBaseWidth);
const extraPerColumn =
equalWidthColumns.length > 0 ? excessSpace / equalWidthColumns.length : 0;
const distributedWidth = minColumnWidth + extraPerColumn;
columnWidths.createdAt = distributedWidth;
columnWidths.updatedAt = distributedWidth;
columnWidths.custom = distributedWidth;
}
const columns: Column[] = [
{
id: '$id',
title: '$id',
type: 'string',
width: columnWidths.id,
icon: IconFingerPrint,
...baseColProps
}
];
if (hasCustomColumns) {
columns.push(
...customColumnsData.map((col) => ({
...col,
width: columnWidths.custom
}))
);
}
columns.push(
{
id: '$createdAt',
title: '$createdAt',
type: 'datetime',
width: columnWidths.createdAt,
icon: IconCalendar,
...baseColProps
},
{
id: '$updatedAt',
title: '$updatedAt',
type: 'datetime',
width: columnWidths.updatedAt,
icon: IconCalendar,
...baseColProps
},
{
id: 'actions',
title: '',
type: 'string',
icon: IconPlus,
isAction: hasCustomColumns,
width: columnWidths.actions,
...baseColProps
}
);
if (!hasCustomColumns) {
columns.push({
id: 'empty',
title: '',
type: 'string',
...baseColProps
});
}
return columns;
};
const getIndexesColumns = (): Column[] => {
const columns = [
{ id: 'key', title: 'Key', icon: null, isPrimary: false },
{ id: 'type', title: 'Type', icon: null, isPrimary: false },
{ id: 'columns', title: 'Columns', icon: null, isPrimary: false },
{
{ id: 'columns', title: 'Columns', icon: null, isPrimary: false }
] as Column[];
if (!$isSmallViewport) {
columns.push({
id: 'empty',
title: '',
width: 40,
isAction: true,
isPrimary: false
}
] as Column[];
} as Column);
}
const spreadsheetColumns = $derived(mode === 'rows' ? getRowColumns() : getIndexesColumns());
return columns;
};
const spreadsheetColumns = $derived(mode === 'indexes' ? getIndexesColumns() : getRowColumns());
const emptyCells = $derived(
($isSmallViewport ? 14 : $isTabletViewport ? 17 : 24) + (!$expandTabs ? 2 : 0)
@@ -164,9 +248,10 @@
</script>
<div
class="databases-spreadsheet spreadsheet-container-outer"
data-mode={mode}
bind:this={spreadsheetContainer}>
bind:this={spreadsheetContainer}
class:custom-columns={customColumns.length > 0}
class="databases-spreadsheet spreadsheet-container-outer">
<SpreadsheetContainer>
<Spreadsheet.Root
{emptyCells}
@@ -179,20 +264,23 @@
}}>
<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') {
$showCreateColumnSheet.show = true;
$showCreateColumnSheet.title = 'Create column';
$showCreateColumnSheet.columns = $tableColumns;
$showCreateColumnSheet.columnsOrder = $columnsOrder;
}
}}>
{#if column.isAction}
<Spreadsheet.Header.Cell column="actions" {root}>
<Button.Button
icon
variant="extra-compact"
onclick={() => {
if (mode === 'rows') {
$showCreateColumnSheet.show = true;
$showCreateColumnSheet.title = 'Create column';
$showCreateColumnSheet.columns = $tableColumns;
$showCreateColumnSheet.columnsOrder = $columnsOrder;
}
}}>
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
</Spreadsheet.Header.Cell>
{:else}
<Spreadsheet.Header.Cell
{root}
column={column.id}
@@ -215,7 +303,7 @@
</Layout.Stack>
{/if}
</Spreadsheet.Header.Cell>
</div>
{/if}
{/each}
</svelte:fragment>
@@ -236,48 +324,36 @@
{#if !$spreadsheetLoading}
<div
class="spreadsheet-fade-bottom"
class:custom-columns={customColumns.length > 0}
data-collapsed-tabs={!$expandTabs}
style:--overlay-left={overlayLeftOffset}
style:--overlay-top={overlayTopOffset}
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>
<Layout.Stack
gap="xl"
alignItems="center"
alignContent="center"
style="width: 653px; max-width: {$isSmallViewport ? '353px' : undefined}">
<Layout.Stack gap="xs" alignItems="center" alignContent="center">
<Typography.Title>{title ?? `You have no ${mode} yet`}</Typography.Title>
{#if showActions}
<Layout.Stack
inline
gap="s"
alignItems="center"
direction={$isSmallViewport ? 'column' : 'row'}>
{#if mode !== 'rows-filtered'}
<Button.Button
icon
size="s"
variant="secondary"
disabled={actions?.primary?.disabled}
onclick={actions?.primary?.onClick}>
<Icon icon={IconPlus} size="s" />
{actions?.primary?.text ?? `Create ${mode}`}
</Button.Button>
{@render subtitle?.()}
</Layout.Stack>
{#if mode === 'rows'}
<Button.Button
size="s"
variant="secondary"
disabled={actions?.random?.disabled}
onclick={actions?.random?.onClick}>
{actions?.random?.text ?? `Generate sample data`}
</Button.Button>
{#if showActions && actions}
{@const inline = mode === 'rows-filtered'}
<div class="controlled-width">
<Layout.Stack {inline}>
{#if inline}
{@render actions?.()}
{:else}
<Layout.Grid columns={2} columnsXS={1}>
{@render actions?.()}
</Layout.Grid>
{/if}
{:else}
<Button.Button
size="s"
variant="secondary"
disabled={actions?.primary?.disabled}
onclick={actions?.primary?.onClick}>
{actions?.primary?.text}
</Button.Button>
{/if}
</Layout.Stack>
</Layout.Stack>
</div>
{/if}
</Layout.Stack>
</div>
@@ -291,6 +367,31 @@
position: fixed;
overflow: hidden;
& :global(.spreadsheet-container) {
overflow-x: auto;
overflow-y: auto;
}
& :global([data-select='true']) {
opacity: 0.85;
pointer-events: none;
}
&.custom-columns {
width: unset;
}
&:not(.custom-columns) :global(.spreadsheet-container) {
overflow-x: hidden;
overflow-y: hidden;
}
/* alternative selector for header selection */
& :global(.sticky-header [data-select='true']) {
opacity: 1;
pointer-events: none;
}
&[data-mode='rows'] {
& :global([role='rowheader'] :nth-last-child(2) [role='presentation']) {
display: none;
@@ -298,6 +399,10 @@
}
&[data-mode='indexes'] {
& :global([role='cell']:last-child [role='presentation']) {
display: none;
}
& :global([role='rowheader'] [role='cell']:nth-last-child(1)) {
pointer-events: none;
@@ -306,22 +411,14 @@
}
}
}
& :global(.spreadsheet-container) {
overflow-x: hidden;
overflow-y: hidden;
}
& :global([data-select='true']) {
opacity: 0.85;
pointer-events: none;
}
}
.spreadsheet-fade-bottom {
right: 0;
bottom: 0;
width: 100%;
position: fixed;
top: var(--overlay-top, auto);
left: var(--overlay-left, 0px);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
@@ -330,17 +427,21 @@
);
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
transition: none !important;
height: var(--dynamic-overlay-height, 70.5vh);
@media (max-width: 1024px) {
height: var(--dynamic-overlay-height, 63.35vh);
&.custom-columns {
pointer-events: none;
}
}
@media (min-width: 1024px) {
height: var(--dynamic-overlay-height, 70.35vh);
.controlled-width {
width: 100%;
@media (min-width: 1440px) {
width: 538px;
max-width: 538px;
}
}
@@ -354,36 +455,12 @@
}
.empty-actions {
left: 50%;
bottom: 35%;
position: fixed;
@media (max-width: 768px) and (max-height: 768px) {
left: unset;
bottom: 12.5% !important;
}
@media (max-width: 768px) and (max-height: 1024px) {
left: unset;
bottom: 15% !important;
}
@media (max-width: 1024px) and (max-height: 1024px) {
left: unset;
bottom: 15%;
}
margin-bottom: 10%;
pointer-events: auto;
@media (max-width: 1024px) {
left: unset;
bottom: 30%;
}
@media (min-width: 1280px) {
bottom: 37.5%;
}
@media (min-width: 1440px) {
bottom: 40%;
// experiment
margin-bottom: 15%;
}
}
</style>
@@ -0,0 +1,48 @@
<script lang="ts">
import { Card } from '$lib/components';
import type { ComponentType } from 'svelte';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
let {
icon,
title,
subtitle,
onClick,
href,
disabled
}: {
icon?: ComponentType;
title: string;
subtitle?: string;
onClick?: () => Promise<void> | void;
href?: string;
disabled?: boolean;
} = $props();
</script>
<Card
{href}
{disabled}
external
radius="m"
padding="xs"
variant="primary"
isButton={!href}
on:click={() => onClick?.()}>
<Layout.Stack direction="row" gap="m">
{#if icon}
<Icon {icon} size="m" color="--fgcolor-neutral-tertiary" />
{/if}
<Layout.Stack direction="column" gap="none">
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{title}
</Typography.Text>
{#if subtitle}
<Typography.Text color="--fgcolor-neutral-secondary">
{subtitle}
</Typography.Text>
{/if}
</Layout.Stack>
</Layout.Stack>
</Card>
@@ -898,6 +898,7 @@
id={row?.$id}
virtualItem={item}
select={rowSelection}
hoverEffect
showSelectOnHover
valueWithoutHover={row.$sequence}>
{#each $tableColumns as { id: columnId, isEditable } (columnId)}
@@ -1159,7 +1160,8 @@
gap="xs"
direction="row"
alignItems="center"
alignContent="center">
alignContent="center"
class="footer-input-select-wrapper">
<span style:white-space="nowrap"> Page </span>
<InputSelect
@@ -1302,6 +1304,11 @@
transform: translateX(-50%);
}
:global(.footer-input-select-wrapper button.input) {
height: 30px;
background-color: var(--bgcolor-neutral-primary);
}
// very weird because the library already has this!
:global(.virtual-row:has([data-editing-mode='true'])) {
z-index: 1 !important;