mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #2486 from appwrite/revamp-designs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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?.()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
+65
@@ -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>
|
||||
+1519
-313
File diff suppressed because it is too large
Load Diff
+8
@@ -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) {
|
||||
|
||||
+22
@@ -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>
|
||||
+3
-26
@@ -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)
|
||||
|
||||
+15
-3
@@ -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>
|
||||
|
||||
+33
-5
@@ -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}
|
||||
|
||||
+18
-7
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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">
|
||||
|
||||
+98
-42
@@ -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}
|
||||
|
||||
+3
-8
@@ -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"
|
||||
|
||||
+7
-2
@@ -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} />
|
||||
|
||||
+7
-2
@@ -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} />
|
||||
|
||||
+7
-2
@@ -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} />
|
||||
|
||||
+8
-2
@@ -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} />
|
||||
|
||||
+11
-2
@@ -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} />
|
||||
|
||||
+12
-4
@@ -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} />
|
||||
|
||||
+7
-2
@@ -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} />
|
||||
|
||||
+12
-3
@@ -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];
|
||||
|
||||
+13
-2
@@ -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) {
|
||||
|
||||
+13
-2
@@ -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}
|
||||
|
||||
+9
-5
@@ -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}
|
||||
|
||||
+8
-6
@@ -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>
|
||||
|
||||
|
||||
+18
-7
@@ -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);
|
||||
|
||||
+8
-3
@@ -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} />
|
||||
|
||||
+44
-2
@@ -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>
|
||||
|
||||
+101
-23
@@ -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}
|
||||
|
||||
+238
-161
@@ -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>
|
||||
|
||||
+48
@@ -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>
|
||||
+8
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user