updates: to spreadsheet.

This commit is contained in:
Darshan
2025-07-15 18:21:02 +05:30
parent 585e918f58
commit 380b241a78
14 changed files with 774 additions and 776 deletions
+7 -7
View File
@@ -1,10 +1,10 @@
import { goto } from '$app/navigation';
import { derived, get, writable } from 'svelte/store';
import { page } from '$app/stores';
import deepEqual from 'deep-equal';
import type { Column, ColumnType } from '$lib/helpers/types';
import { Query } from '@appwrite.io/console';
import { toLocaleDateTime } from '$lib/helpers/date';
import { derived, get, writable } from 'svelte/store';
import type { SheetColumn, SheetColumnType } from '$lib/helpers/types';
export type TagValue = {
tag: string;
@@ -14,7 +14,7 @@ export type TagValue = {
export type Operator = {
toTag: (attribute: string, input?: string | number | string[], type?: string) => TagValue;
toQuery: (attribute: string, input?: string | number | string[]) => string;
types: ColumnType[];
types: SheetColumnType[];
hideInput?: boolean;
};
@@ -33,7 +33,7 @@ function initQueries(initialValue = new Map<TagValue, string>()) {
type AddFilterArgs = {
operator: Operator;
column: Column;
column: SheetColumn;
value: string | number | string[];
};
@@ -91,14 +91,14 @@ export const tags = derived(queries, ($queries) => Array.from($queries.keys()));
/* eslint @typescript-eslint/no-explicit-any: 'off' */
export function addFilter(
columns: Column[],
columns: SheetColumn[],
columnId: string,
operatorKey: string,
value: any, // We cast to any to not cause type errors in the input components
arrayValues: string[] = []
) {
const operator = operatorKey ? operators[operatorKey] : null;
const column = columns.find((c) => c.id === columnId) as Column;
const column = columns.find((c) => c.id === columnId) as SheetColumn;
if (!column || !operator) return;
if (column.array) {
queries.addFilter({ column, operator, value: arrayValues });
@@ -135,7 +135,7 @@ const operatorsDefault = new Map<
ValidOperators,
{
query: (attr: string, input: string | number | string[]) => string;
types: ColumnType[];
types: SheetColumnType[];
hideInput?: boolean;
}
>([
@@ -20,11 +20,12 @@
import { addNotification } from '$lib/stores/notifications';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { isSmallViewport } from '$lib/stores/viewport';
import { base } from '$app/paths';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import EmptySheet from './emptySheet.svelte';
import EmptySheet from './layout/emptySheet.svelte';
import CreateRecord from './createRecord.svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
export let data: PageData;
@@ -135,7 +136,7 @@
{#if hasAttributes && hasValidAttributes}
{#if data.documents.total}
<Divider />
<SpreadSheet {data} />
<SpreadSheet {data} bind:showRecordsCreateSheet />
{:else if $hasPageQueries}
<EmptySearch hidePages>
<div class="common-section">
@@ -159,10 +160,30 @@
</div>
</EmptySearch>
{:else}
<EmptySheet on:record={() => (showRecordsCreateSheet = true)} />
<EmptySheet
mode="records"
cta={{
primary: {
onClick: () => {
showRecordsCreateSheet = true;
}
}
}} />
{/if}
{:else}
<EmptySheet on:record={() => (showRecordsCreateSheet = true)} />
<EmptySheet
mode="records"
title="You have no columns yet"
cta={{
primary: {
text: 'Create column',
onClick: async () => {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/attributes`
);
}
}
}} />
{/if}
</div>
{/key}
@@ -186,6 +207,4 @@
}} />
{/if}
{#if showRecordsCreateSheet}
<CreateRecord collection={$collection} bind:showSheet={showRecordsCreateSheet} />
{/if}
<CreateRecord collection={$collection} bind:showSheet={showRecordsCreateSheet} />
@@ -8,7 +8,8 @@ import { queries, queryParamToMap } from '$lib/components/filters';
export const load: PageLoad = async ({ params, depends, url, route }) => {
depends(Dependencies.DOCUMENTS);
const page = getPage(url);
const limit = getLimit(url, route, 18 /*PAGE_LIMIT*/);
// TODO: apply pagination
const limit = getLimit(url, route, 96 /*PAGE_LIMIT*/);
const view = getView(url, route, View.Grid);
const offset = pageToOffset(page, limit);
const query = getQuery(url);
@@ -1,7 +1,4 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { canWriteCollections } from '$lib/stores/roles';
@@ -12,7 +9,8 @@
Layout,
Link,
Popover,
Table,
Selector,
Spreadsheet,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
@@ -20,10 +18,10 @@
import { isRelationship, isString } from '../document-[document]/attributes/store';
import FailedModal from '../failedModal.svelte';
import CreateIndex from '../indexes/createIndex.svelte';
import { attributes, type Attributes, isCsvImportInProgress } from '../store';
import { attributes, type Attributes, indexes, isCsvImportInProgress } from '../store';
import CreateAttributeDropdown from './createAttributeDropdown.svelte';
import Delete from './deleteAttribute.svelte';
import Edit from './edit.svelte';
import EditAttribute from './edit.svelte';
import { attributeOptions, type Option } from './store';
import {
IconArrowSmRight,
@@ -40,19 +38,23 @@
import type { ComponentProps } from 'svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
import CsvDisabled from '../csvDisabled.svelte';
const databaseId = page.params.database;
import { isSmallViewport } from '$lib/stores/viewport';
import SideSheet from '../layout/sidesheet.svelte';
import SpreadsheetContainer from '../layout/spreadsheet.svelte';
import EmptySheet from '../layout/emptySheet.svelte';
let showDropdown = [];
let selectedOption: Option['name'] = null;
let selectedAttribute: Attributes = null;
let showCreate = false;
let showDelete = false;
let showEdit = false;
let showCreateIndex = false;
let showFailed = false;
let error = '';
let showEdit = false;
let editAttribute: EditAttribute;
const attributeFormatIcon = {
ip: IconLocationMarker,
url: IconLink,
@@ -72,181 +74,197 @@
return undefined;
}
}
const emptyCellsLimit = $isSmallViewport ? 14 : 16;
$: emptyCellsCount =
$attributes.length >= emptyCellsLimit ? 0 : emptyCellsLimit - $attributes.length;
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Attributes</Typography.Title>
<Container expanded style="background: var(--bgcolor-neutral-primary)">
<Layout.Stack direction="row" justifyContent="flex-end">
<!-- TODO: filters? -->
{#if $canWriteCollections}
<CreateAttributeDropdown bind:selectedOption bind:showCreate />
{/if}
</Layout.Stack>
</Container>
<div class="databases-spreadsheet">
{#if $attributes.length}
<Table.Root
let:root
columns={[
{ id: 'key' },
{ id: 'type' },
{ id: 'default' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="default" {root}>Default value</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each $attributes as attribute, index}
{@const option = attributeOptions.find((option) => option.type === attribute.type)}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>
<Layout.Stack direction="row" alignItems="center">
{#if isRelationship(attribute)}
<Icon
size="s"
icon={attribute?.twoWay
? IconSwitchHorizontal
: IconArrowSmRight} />
{:else if 'format' in attribute && attribute.format}
{@const icon = attributeFormatIcon[attribute?.format]}
<Icon {icon} size="s" />
{:else}
<Icon icon={option.icon} size="s" />
{/if}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack inline direction="row" alignItems="center" gap="xxs">
<span class="text u-trim-1" data-private>{attribute.key}</span>
{#if isString(attribute) && attribute.encrypt}
<Tooltip>
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<div slot="tooltip">Encrypted</div>
</Tooltip>
<SpreadsheetContainer>
<Spreadsheet.Root
let:root
height="100%"
emptyCells={emptyCellsCount}
columns={[
{ id: 'key', width: { min: 200 } },
{ id: 'indexed', width: { min: 150 } },
{ id: 'default', width: { min: 200 } },
{ id: 'actions', width: 40, isAction: true }
]}>
<svelte:fragment slot="header" let:root>
<Spreadsheet.Header.Cell column="key" {root}
>Column name</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="indexed" {root}
>Indexed</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="default" {root}
>Default value</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each $attributes as attribute, index}
{@const option = attributeOptions.find(
(option) => option.type === attribute.type
)}
<Spreadsheet.Row.Base {root}>
<Spreadsheet.Cell column="key" {root} isEditable={false}>
<Layout.Stack direction="row" alignItems="center">
{#if isRelationship(attribute)}
<Icon
size="s"
icon={attribute?.twoWay
? IconSwitchHorizontal
: IconArrowSmRight} />
{:else if 'format' in attribute && attribute.format}
{@const icon = attributeFormatIcon[attribute?.format]}
<Icon {icon} size="s" />
{:else}
<Icon icon={option.icon} size="s" />
{/if}
<Layout.Stack direction="row" alignItems="center" gap="s">
<Layout.Stack
inline
direction="row"
alignItems="center"
gap="xxs">
<span class="text u-trim-1" data-private
>{attribute.key}</span>
{#if isString(attribute) && attribute.encrypt}
<Tooltip>
<Icon
size="s"
icon={IconLockClosed}
color="--fgcolor-neutral-tertiary" />
<div slot="tooltip">Encrypted</div>
</Tooltip>
{/if}
</Layout.Stack>
{#if attribute.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{:else if attribute.required}
<Badge size="xs" variant="secondary" content="required" />
{/if}
</Layout.Stack>
{#if attribute.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={attribute.status}
type={getAttributeStatusBadge(attribute.status)} />
{#if attribute.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = attribute.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{:else if attribute.required}
<Badge size="xs" variant="secondary" content="required" />
{/if}
</Layout.Stack>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>
{#if 'format' in attribute && attribute.format}
<span class="u-capitalize">{attribute.format}</span>
{:else}
<p>
<span class="u-capitalize">{attribute.type}</span>
{#if isRelationship(attribute)}
<span>
with <a
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${attribute?.relatedCollection}`}
><b data-private>{attribute?.key}</b></a>
</span>
{/if}
</p>
{/if}
<span>
{attribute.array ? '[]' : ''}
</span>
</Table.Cell>
<Table.Cell column="default" {root}>
{attribute?.default !== null && attribute?.default !== undefined
? attribute?.default
: '-'}
</Table.Cell>
<Table.Cell column="actions" {root}>
{#if $isCsvImportInProgress}
<CsvDisabled>
<Button disabled text icon ariaLabel="more options">
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
</CsvDisabled>
{:else}
<Popover let:toggle padding="none" placement="bottom-end">
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip" let:toggle>
<ActionMenu.Item.Button
leadingIcon={IconPencil}
on:click={(event) => {
toggle(event);
showEdit = true;
selectedAttribute = attribute;
showDropdown[index] = false;
}}>
Update
</ActionMenu.Item.Button>
{#if !isRelationship(attribute)}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="indexed" {root}>
{@const isIndexed = $indexes.some((index) =>
index.attributes.includes(attribute.key)
)}
<Selector.Checkbox size="s" checked={isIndexed} disabled />
</Spreadsheet.Cell>
<Spreadsheet.Cell column="default" {root}>
{@const _default =
attribute?.default !== null && attribute?.default !== undefined
? attribute?.default
: null}
{#if _default === null}
<Badge variant="secondary" content="NULL" size="xs" />
{:else}
{_default}
{/if}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="actions" {root}>
{#if $isCsvImportInProgress}
<CsvDisabled>
<Button disabled text icon ariaLabel="more options">
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
</CsvDisabled>
{:else}
<!-- TODO: no portal, rather see if we can fix the cell -->
<Popover let:toggle padding="none" placement="bottom-end" portal>
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip" let:toggle>
<ActionMenu.Item.Button
leadingIcon={IconPlus}
leadingIcon={IconPencil}
on:click={(event) => {
toggle(event);
showEdit = true;
selectedAttribute = attribute;
showCreateIndex = true;
showDropdown[index] = false;
}}>
Create index
Update
</ActionMenu.Item.Button>
{/if}
{#if attribute.status !== 'processing'}
<ActionMenu.Item.Button
leadingIcon={IconTrash}
on:click={(event) => {
toggle(event);
showDelete = true;
showDropdown[index] = false;
selectedAttribute = attribute;
trackEvent(Click.DatabaseAttributeDelete);
}}>
Delete
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</Popover>
{/if}
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
{#if !isRelationship(attribute)}
<ActionMenu.Item.Button
leadingIcon={IconPlus}
on:click={(event) => {
toggle(event);
selectedAttribute = attribute;
showCreateIndex = true;
showDropdown[index] = false;
}}>
Create index
</ActionMenu.Item.Button>
{/if}
{#if attribute.status !== 'processing'}
<ActionMenu.Item.Button
status="danger"
leadingIcon={IconTrash}
on:click={(event) => {
toggle(event);
showDelete = true;
showDropdown[index] = false;
selectedAttribute = attribute;
trackEvent(Click.DatabaseAttributeDelete);
}}>
Delete
</ActionMenu.Item.Button>
{/if}
</ActionMenu.Root>
</Popover>
{/if}
</Spreadsheet.Cell>
</Spreadsheet.Row.Base>
{/each}
<svelte:fragment slot="footer">
<Layout.Stack
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
{$attributes.length} columns
</Typography.Text>
</Layout.Stack>
</svelte:fragment>
</Spreadsheet.Root>
</SpreadsheetContainer>
{:else}
<Empty single target="attribute">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown bind:selectedOption bind:showCreate let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
</Button>
</CreateAttributeDropdown>
{/if}
</svelte:fragment>
</Empty>
<!-- TODO: handle these cases -->
<!--{#if $canWriteCollections}-->
<EmptySheet mode="columns" />
{/if}
</Container>
</div>
{#if showCreate}
<Create bind:showCreate bind:selectedOption />
@@ -255,8 +273,17 @@
<Delete bind:showDelete {selectedAttribute} />
{/if}
{#if showEdit}
<Edit bind:showEdit {selectedAttribute} />
<SideSheet
title="Edit column"
bind:show={showEdit}
submit={{
text: 'Update',
onClick: async () => await editAttribute.submit()
}}>
<EditAttribute showEdit isModal={false} {selectedAttribute} bind:this={editAttribute} />
</SideSheet>
{/if}
{#if showCreateIndex}
<CreateIndex bind:showCreateIndex externalAttribute={selectedAttribute} />
{/if}
@@ -6,8 +6,8 @@
import CsvDisabled from '../csvDisabled.svelte';
import { isCsvImportInProgress } from '../store';
export let selectedOption: Option['name'] = null;
export let showCreate = false;
export let selectedOption: Option['name'] = null;
</script>
{#if $isCsvImportInProgress}
@@ -20,9 +20,9 @@
{:else}
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle}>
<Button on:click={toggle} event="create_attribute">
<Button secondary on:click={toggle} event="create_attribute">
<Icon icon={IconPlus} slot="start" size="s" />
Create attribute
Create column
</Button>
</slot>
<ActionMenu.Root slot="tooltip">
@@ -43,7 +43,7 @@
}
}) as Option;
async function submit() {
export async function submit() {
try {
await option.update(databaseId, collectionId, selectedAttribute, originalKey);
await invalidate(Dependencies.COLLECTION);
@@ -58,6 +58,7 @@
type: 'success',
message: `Attribute ${selectedAttribute.key} has been updated`
});
showEdit = false;
trackEvent(Submit.AttributeUpdate);
@@ -1,135 +0,0 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Wizard } from '$lib/layout';
import { Alert, Fieldset, Layout, Sheet, Typography } from '@appwrite.io/pink-svelte';
import Button from '$lib/elements/forms/button.svelte';
import Form from '$lib/elements/forms/form.svelte';
import { sdk } from '$lib/stores/sdk';
import { goto } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { writable } from 'svelte/store';
import AttributeForm from '../document-[document]/attributeForm.svelte';
import { Permissions } from '$lib/components/permissions';
import type { PageData } from './$types';
import type { Attributes } from '../store';
import { ID } from '@appwrite.io/console';
export let data: PageData;
let showExitModal = false;
let formComponent: Form;
let isSubmitting = writable(false);
type CreateDocument = {
id?: string;
document: object;
permissions: string[];
attributes: Attributes[];
};
function createDocumentWritable() {
const availableAttributes = (data.collection.attributes as unknown as Attributes[]).filter(
(a) => a.status === 'available'
);
const initial = {
id: null,
document: availableAttributes.reduce((acc, attr) => {
acc[attr.key] = attr.array ? [] : null;
return acc;
}, {}),
permissions: [],
attributes: availableAttributes
};
const store = writable<CreateDocument>({ ...initial });
return store;
}
const createDocument = createDocumentWritable();
async function create() {
try {
const { $id } = await sdk
.forProject(page.params.region, page.params.project)
.databases.createDocument(
page.params.database,
page.params.collection,
$createDocument.id ?? ID.unique(),
$createDocument.document,
$createDocument.permissions
);
addNotification({
message: 'Document has been created',
type: 'success'
});
trackEvent(Submit.DocumentCreate, {
customId: !!$createDocument.id
});
goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/document-${$id}`
);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.DocumentCreate);
}
}
</script>
<Sheet bind:open={showExitModal}>
<div slot="header">Create document</div>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack gap="xxl">
<Fieldset legend="Data">
<AttributeForm
bind:formValues={$createDocument.document}
attributes={$createDocument.attributes}
bind:customId={$createDocument.id} />
</Fieldset>
<Fieldset legend="Permissions">
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project goals.
</Typography.Text>
{#if data.collection.documentSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is enabled</svelte:fragment>
Users will be able to access this document if they have been granted
<b>either document or collection permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createDocument.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is disabled</svelte:fragment>
If you want to assign document permissions, navigate to Collection settings
and enable document security. Otherwise, only collection permissions will
be used.
</Alert.Inline>
{/if}
</Layout.Stack>
</Fieldset>
</Layout.Stack>
</Form>
<!-- <svelte:fragment slot="footer">-->
<!-- <Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>-->
<!-- <Button-->
<!-- fullWidthMobile-->
<!-- on:click={() => formComponent.triggerSubmit()}-->
<!-- disabled={$isSubmitting}>-->
<!-- Create-->
<!-- </Button>-->
<!-- </svelte:fragment>-->
</Sheet>
@@ -1,7 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
import Form from '$lib/elements/forms/form.svelte';
import Button from '$lib/elements/forms/button.svelte';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
@@ -10,15 +8,15 @@
import { Permissions } from '$lib/components/permissions';
import type { Attributes } from './store';
import { ID, type Models } from '@appwrite.io/console';
import { Alert, Divider, Layout, Sheet, Typography } from '@appwrite.io/pink-svelte';
import { Alert, Layout, Typography } from '@appwrite.io/pink-svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import SideSheet from './layout/sidesheet.svelte';
export let showSheet = false;
export let collection: Models.Collection;
let formComponent: Form;
let isSubmitting = writable(false);
let isSubmitting = false;
type CreateDocument = {
id?: string;
@@ -48,6 +46,8 @@
const createDocument = createDocumentWritable();
async function create() {
isSubmitting = true;
try {
await sdk
.forProject(page.params.region, page.params.project)
@@ -74,102 +74,51 @@
type: 'error'
});
trackError(error, Submit.DocumentCreate);
} finally {
isSubmitting = false;
}
}
</script>
<div class="sheet-container">
<Sheet bind:open={showSheet} closeOnBlur={false}>
<svelte:fragment slot="header">
<Layout.Stack>
Create document
<!-- TODO: add a ID badge-->
<SideSheet
spaced
title="Create record"
bind:show={showSheet}
closeOnBlur={false}
submit={{
text: 'Create',
disabled: isSubmitting,
onClick: () => create()
}}>
<Layout.Stack gap="xxl">
<AttributeForm
bind:formValues={$createDocument.document}
attributes={$createDocument.attributes}
bind:customId={$createDocument.id} />
<!-- TODO: add a ID badge-->
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best practice to
allow only the permissions you need to meet your project goals.
</Typography.Text>
{#if collection.documentSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title">Document security is enabled</svelte:fragment>
Users will be able to access this document if they have been granted
<b>either document or collection permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createDocument.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is disabled</svelte:fragment>
If you want to assign document permissions, navigate to Collection settings and
enable document security. Otherwise, only collection permissions will be used.
</Alert.Inline>
{/if}
</Layout.Stack>
</svelte:fragment>
<Form bind:this={formComponent} onSubmit={create} bind:isSubmitting>
<Layout.Stack direction="column" justifyContent="space-evenly">
<Layout.Stack gap="xxl">
<AttributeForm
bind:formValues={$createDocument.document}
attributes={$createDocument.attributes}
bind:customId={$createDocument.id} />
<Layout.Stack gap="xl">
<Typography.Text>
Choose which permission scopes to grant your application. It is best
practice to allow only the permissions you need to meet your project
goals.
</Typography.Text>
{#if collection.documentSecurity}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is enabled</svelte:fragment>
Users will be able to access this document if they have been granted
<b>either document or collection permissions</b>.
</Alert.Inline>
<Permissions bind:permissions={$createDocument.permissions} />
{:else}
<Alert.Inline status="info">
<svelte:fragment slot="title"
>Document security is disabled</svelte:fragment>
If you want to assign document permissions, navigate to Collection settings
and enable document security. Otherwise, only collection permissions
will be used.
</Alert.Inline>
{/if}
</Layout.Stack>
</Layout.Stack>
<div class="sheet-footer">
<Layout.Stack gap="l">
<Divider />
<div class="sheet-footer-actions">
<Layout.Stack gap="m" direction="row" justifyContent="flex-end">
<Button
size="s"
secondary
fullWidthMobile
on:click={() => (showSheet = false)}>Cancel</Button>
<Button
size="s"
fullWidthMobile
disabled={$isSubmitting}
on:click={() => formComponent.triggerSubmit()}>
Create
</Button>
</Layout.Stack>
</div>
</Layout.Stack>
</div>
</Layout.Stack>
</Form>
</Sheet>
</Layout.Stack>
</SideSheet>
</div>
<style>
.sheet-container {
top: 0;
position: absolute;
& :global(aside header) {
@media (max-width: 768px) {
margin-top: 6rem;
}
}
}
.sheet-footer {
left: 0;
right: 0;
bottom: 0;
position: absolute;
& .sheet-footer-actions {
padding: var(--space-8);
}
}
</style>
@@ -1,173 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import {
Button,
Icon,
Layout,
Spreadsheet,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
const staticColumns = [
{
id: '$id',
title: 'ID',
width: 200,
draggable: false,
type: 'string',
icon: IconFingerPrint
},
{
id: '$createdAt',
title: 'createdAt',
width: { min: 180 },
draggable: false,
type: 'datetime',
icon: IconCalendar
},
{
id: '$updatedAt',
title: 'updatedAt',
width: { min: 180 },
draggable: false,
type: 'datetime',
icon: IconCalendar
},
{
id: 'actions',
title: '',
width: 555,
draggable: false,
type: 'string',
icon: IconPlus
},
{
id: 'empty',
title: '',
width: 40,
isAction: true,
draggable: false,
type: 'string'
}
];
const dispatch = createEventDispatcher();
</script>
<div class="spreadsheet-container-outer">
<Spreadsheet.Root allowSelection emptyCells={20} height="fit-content" columns={staticColumns}>
<svelte:fragment slot="header" let:root>
{#each staticColumns as column (column.id)}
{#if column.isAction}
<Spreadsheet.Header.Cell column="actions" {root}>
<Button.Button icon variant="extra-compact" on:click={() => {}}>
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
</Spreadsheet.Header.Cell>
{:else}
<Spreadsheet.Header.Cell
{root}
column={column.id}
icon={column.icon ?? undefined}>
{column.title}
</Spreadsheet.Header.Cell>
{/if}
{/each}
</svelte:fragment>
</Spreadsheet.Root>
<div class="spreadsheet-fade-bottom">
<div class="empty-actions">
<Layout.Stack gap="xl">
<Typography.Title>You have no records yet</Typography.Title>
<Layout.Stack>
<Button.Button
icon
size="s"
variant="secondary"
on:click={() => dispatch('record')}>
<Icon icon={IconPlus} size="s" />
Create record
</Button.Button>
<Tooltip>
<Button.Button
size="s"
variant="secondary"
on:click={() => dispatch('random')}>
Generate random data
</Button.Button>
<span slot="tooltip"> Yet to be added </span>
</Tooltip>
</Layout.Stack>
</Layout.Stack>
</div>
</div>
</div>
<style lang="scss">
.spreadsheet-container-outer {
width: 100%;
overflow: hidden;
position: fixed;
@media (max-width: 768px) {
position: unset;
}
& :global(.spreadsheet-container) {
overflow-x: hidden;
overflow-y: hidden;
}
& :global([data-select='true']) {
opacity: 0.85;
pointer-events: none;
}
}
.spreadsheet-fade-bottom {
bottom: 0;
width: 100%;
height: 70vh;
position: fixed;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.855) 21%,
#ffffff 100%
);
z-index: 20;
display: flex;
justify-content: center;
@media (max-width: 768px) {
height: 50vh;
}
}
:global(.theme-dark) .spreadsheet-fade-bottom {
background: linear-gradient(
180deg,
rgba(25, 25, 28, 0.38) 13%,
rgba(25, 25, 28, 0.7) 21%,
rgba(25, 25, 28, 0.95) 38%,
var(--bgcolor-neutral-default, #19191c) 100%
);
}
.empty-actions {
bottom: 30%;
position: fixed;
@media (min-width: 1024px) {
left: 55%;
bottom: 40%;
}
}
</style>
@@ -1,5 +1,4 @@
<script lang="ts">
import { Empty } from '$lib/components';
import { Container } from '$lib/layout';
import { collection } from '../store';
import Delete from './deleteIndex.svelte';
@@ -8,7 +7,7 @@
import CreateAttribute from '../createAttribute.svelte';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import CreateAttributeDropdown from '../attributes/createAttributeDropdown.svelte';
// import CreateAttributeDropdown from '../attributes/createAttributeDropdown.svelte';
import type { Option } from '../attributes/store';
import FailedModal from '../failedModal.svelte';
import { canWriteCollections } from '$lib/stores/roles';
@@ -19,8 +18,7 @@
Layout,
Link,
Popover,
Table,
Typography
Spreadsheet
} from '@appwrite.io/pink-svelte';
import {
IconDotsHorizontal,
@@ -30,6 +28,11 @@
} from '@appwrite.io/pink-icons-svelte';
import type { ComponentProps } from 'svelte';
import { Click, trackEvent } from '$lib/actions/analytics';
import EmptySheet from '../layout/emptySheet.svelte';
import SpreadsheetContainer from '../layout/spreadsheet.svelte';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
export let data;
@@ -56,11 +59,11 @@
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Title>Indexes</Typography.Title>
<Container expanded style="background: var(--bgcolor-neutral-primary)">
<Layout.Stack direction="row" justifyContent="flex-end">
{#if $canWriteCollections}
<Button
secondary
event="create_index"
disabled={!$collection?.attributes?.length}
on:click={() => (showCreateIndex = true)}>
@@ -69,116 +72,119 @@
</Button>
{/if}
</Layout.Stack>
</Container>
<div class="databases-spreadsheet">
{#if data.collection?.attributes?.length}
{#if data.collection.indexes.length}
<Table.Root
let:root
columns={[
{ id: 'key' },
{ id: 'type' },
{ id: 'attributes' },
{ id: 'orders' },
{ id: 'lengths' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="key" {root}>Key</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="attributes" {root}>Attributes</Table.Header.Cell>
<Table.Header.Cell column="orders" {root}>Orders</Table.Header.Cell>
<Table.Header.Cell column="lengths" {root}>Lengths</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.collection.indexes as index}
<Table.Row.Base {root}>
<Table.Cell column="key" {root}>
<Layout.Stack direction="row" alignItems="center">
{index.key}
{#if index.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={index.status}
type={getAttributeStatusBadge(index.status)} />
{#if index.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = index.error;
showFailed = true;
}}>Details</Link.Button>
<SpreadsheetContainer>
<Spreadsheet.Root
let:root
columns={[
{ id: 'key' },
{ id: 'type' },
{ id: 'attributes' },
{ id: 'orders' },
{ id: 'lengths' },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Spreadsheet.Header.Cell column="key" {root}>Key</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="type" {root}>Type</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="attributes" {root}
>Attributes</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="orders" {root}
>Orders</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="lengths" {root}
>Lengths</Spreadsheet.Header.Cell>
<Spreadsheet.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.collection.indexes as index}
<Spreadsheet.Row.Base {root}>
<Spreadsheet.Cell column="key" {root}>
<Layout.Stack direction="row" alignItems="center">
{index.key}
{#if index.status !== 'available'}
<Badge
size="s"
variant="secondary"
content={index.status}
type={getAttributeStatusBadge(index.status)} />
{#if index.error}
<Link.Button
variant="muted"
on:click={(e) => {
e.preventDefault();
error = index.error;
showFailed = true;
}}>Details</Link.Button>
{/if}
{/if}
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>{index.type}</Table.Cell>
<Table.Cell column="attributes" {root}>
{index.attributes}
</Table.Cell>
<Table.Cell column="orders" {root}>
{index.orders}
</Table.Cell>
<Table.Cell column="lengths" {root}>
{index.lengths}
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle padding="none" placement="bottom-end">
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Button
leadingIcon={IconEye}
on:click={() => {
selectedIndex = index;
showOverview = true;
}}>Overview</ActionMenu.Item.Button>
<ActionMenu.Item.Button
leadingIcon={IconTrash}
on:click={() => {
selectedIndex = index;
showDelete = true;
trackEvent(Click.DatabaseIndexDelete);
}}>Delete</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
</Layout.Stack>
</Spreadsheet.Cell>
<Spreadsheet.Cell column="type" {root}>{index.type}</Spreadsheet.Cell>
<Spreadsheet.Cell column="attributes" {root}>
{index.attributes}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="orders" {root}>
{index.orders}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="lengths" {root}>
{index.lengths}
</Spreadsheet.Cell>
<Spreadsheet.Cell column="actions" {root}>
<Popover let:toggle padding="none" placement="bottom-end">
<Button text icon ariaLabel="more options" on:click={toggle}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Button
leadingIcon={IconEye}
on:click={() => {
selectedIndex = index;
showOverview = true;
}}>Overview</ActionMenu.Item.Button>
<ActionMenu.Item.Button
leadingIcon={IconTrash}
on:click={() => {
selectedIndex = index;
showDelete = true;
trackEvent(Click.DatabaseIndexDelete);
}}>Delete</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</Spreadsheet.Cell>
</Spreadsheet.Row.Base>
{/each}
</Spreadsheet.Root>
</SpreadsheetContainer>
{:else}
<Empty
allowCreate={$canWriteCollections}
single
href="https://appwrite.io/docs/products/databases/collections#indexes"
target="index"
on:click={() => (showCreateIndex = true)} />
<EmptySheet
mode="indexes"
cta={{
primary: {
onClick: () => (showCreateIndex = true),
disabled: !$collection?.attributes?.length
}
}} />
{/if}
{:else}
<Empty single target="attribute">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/collections#attributes"
text
event="empty_documentation"
ariaLabel={`create {target}`}>Documentation</Button>
{#if $canWriteCollections}
<CreateAttributeDropdown
bind:selectedOption={selectedAttribute}
bind:showCreate={showCreateAttribute}
let:toggle>
<Button secondary event="create_attribute" on:click={toggle}>
Create attribute
</Button>
</CreateAttributeDropdown>
{/if}
</svelte:fragment>
</Empty>
<!-- todo: show create attribute if one doesn't exist -->
<EmptySheet
mode="indexes"
title="You have no columns yet"
cta={{
primary: {
text: 'Create columns',
onClick: async () => {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${page.params.database}/collection-${page.params.collection}/attributes`
);
}
}
}} />
{/if}
</Container>
</div>
<Create bind:showCreateIndex />
@@ -0,0 +1,241 @@
<script lang="ts">
import {
Button,
Icon,
Layout,
Spreadsheet,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import type { SheetColumn } from '$lib/helpers/types';
import { IconCalendar, IconFingerPrint, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isSmallViewport } from '$lib/stores/viewport';
type Mode = 'records' | 'columns' | 'indexes';
type ColumnsMap = Record<Mode, SheetColumn[]>;
type CTA = {
text?: string;
disabled?: boolean;
onClick?: () => void;
};
export let mode: Mode;
export let title: string | undefined = undefined;
export let cta:
| {
primary?: CTA;
random?: CTA;
}
| undefined = undefined;
function makeColumns(...middle: SheetColumn[]): SheetColumn[] {
return [
{
id: '$id',
title: 'ID',
type: 'string',
width: 250,
icon: IconFingerPrint
},
...middle,
{
id: 'actions',
title: '',
width: 555,
draggable: false,
type: 'string',
icon: IconPlus
},
{
id: 'empty',
title: '',
width: 40,
isAction: true,
draggable: false,
type: 'string'
}
];
}
const columnsMap: ColumnsMap = {
records: makeColumns(
{
id: '$createdAt',
title: 'Created',
type: 'datetime',
width: { min: 180 },
draggable: false,
icon: IconCalendar
},
{
id: '$updatedAt',
title: 'Updated',
type: 'datetime',
width: { min: 180 },
draggable: false,
icon: IconCalendar
}
),
columns: makeColumns(
{
id: 'indexed',
title: 'Indexed',
type: 'boolean',
width: { min: 180 },
draggable: false,
isAction: false
},
{
id: 'default',
title: 'Default',
type: 'string',
width: { min: 180 },
draggable: false,
isAction: false
}
),
indexes: makeColumns(
{
id: 'type',
title: 'Type',
type: 'string',
width: { min: 150 },
draggable: false,
isAction: false
},
{
id: 'attributes',
title: 'Columns',
type: 'string',
width: { min: 200 },
draggable: false,
isAction: false
}
)
};
$: spreadsheetColumns = columnsMap[mode];
</script>
<div class="spreadsheet-container-outer">
<Spreadsheet.Root
allowSelection
emptyCells={20}
height="fit-content"
columns={spreadsheetColumns}>
<svelte:fragment slot="header" let:root>
{#each spreadsheetColumns as column (column.id)}
<Spreadsheet.Header.Cell {root} column={column.id} icon={column.icon ?? undefined}>
{#if column.isAction}
<Button.Button icon variant="extra-compact">
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
{:else}
{column.title}
{/if}
</Spreadsheet.Header.Cell>
{/each}
</svelte:fragment>
</Spreadsheet.Root>
<div class="spreadsheet-fade-bottom">
<div class="empty-actions">
<Layout.Stack gap="xl" alignItems="center">
<Typography.Title>{title ?? `You have no ${mode} yet`}</Typography.Title>
<Layout.Stack
inline
alignItems="center"
direction={$isSmallViewport ? 'column' : 'row'}
gap="s">
<Button.Button
icon
size="s"
variant="secondary"
disabled={cta?.primary?.disabled}
on:click={cta?.primary?.onClick}>
<Icon icon={IconPlus} size="s" />
{cta?.primary?.text ?? `Create ${mode}`}
</Button.Button>
{#if mode === 'records'}
<Tooltip>
<Button.Button
disabled={cta?.random?.disabled}
size="s"
variant="secondary"
on:click={cta?.random.onClick}>
{cta?.random?.text ?? `Generate random data`}
</Button.Button>
<span slot="tooltip">Yet to be added</span>
</Tooltip>
{/if}
</Layout.Stack>
</Layout.Stack>
</div>
</div>
</div>
<style lang="scss">
.spreadsheet-container-outer {
width: 100%;
overflow: hidden;
position: fixed;
@media (max-width: 768px) {
position: unset;
}
& :global(.spreadsheet-container) {
overflow-x: hidden;
overflow-y: hidden;
}
& :global([data-select='true']) {
opacity: 0.85;
pointer-events: none;
}
}
.spreadsheet-fade-bottom {
bottom: 0;
width: 100%;
height: 70vh;
position: fixed;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.855) 21%,
#ffffff 100%
);
z-index: 20;
display: flex;
justify-content: center;
@media (max-width: 768px) {
height: 50vh;
}
}
:global(.theme-dark) .spreadsheet-fade-bottom {
background: linear-gradient(
180deg,
rgba(25, 25, 28, 0.38) 13%,
rgba(25, 25, 28, 0.7) 21%,
rgba(25, 25, 28, 0.95) 38%,
var(--bgcolor-neutral-default, #19191c) 100%
);
}
.empty-actions {
bottom: 30%;
position: fixed;
@media (min-width: 1024px) {
left: 55%; /* TODO: if both sidebars open, use 55%, else 50% */
bottom: 40%;
}
}
</style>
@@ -0,0 +1,91 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Button } from '$lib/elements/forms';
import { Divider, Layout, Sheet, Typography } from '@appwrite.io/pink-svelte';
let {
show = $bindable(false),
title,
spaced = false,
closeOnBlur = false,
submit,
children
}: {
show: boolean;
title: string;
spaced?: boolean;
closeOnBlur?: boolean;
submit: {
text: string;
disabled?: boolean;
onClick: () => void | Promise<void>;
};
children: Snippet;
} = $props();
</script>
<div class="sheet-container" class:spaced>
<Sheet bind:open={show} {closeOnBlur}>
<div slot="header" style:width="100%">
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" gap="m" alignItems="center">
<Typography.Text variant="m-400">{title}</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</div>
<Layout.Stack direction="column" justifyContent="space-evenly">
<Layout.Stack gap="xl">
{@render children()}
</Layout.Stack>
<div class="sheet-footer">
<Layout.Stack gap="l">
<Divider />
<div class="sheet-footer-actions">
<Layout.Stack gap="m" direction="row" justifyContent="flex-end">
<Button size="s" secondary on:click={() => (show = false)}
>Cancel</Button>
<Button
size="s"
disabled={submit.disabled}
on:click={async () => {
await submit.onClick();
show = false;
}}>
{submit.text}
</Button>
</Layout.Stack>
</div>
</Layout.Stack>
</div>
</Layout.Stack>
</Sheet>
</div>
<style lang="scss">
.sheet-container {
top: 0;
position: absolute;
@media (max-width: 768px) {
&.spaced :global(aside header) {
margin-top: 6rem;
}
}
}
.sheet-footer {
left: 0;
right: 0;
bottom: 0;
position: absolute;
& .sheet-footer-actions {
padding-inline: var(--space-8);
padding-block-end: var(--space-6);
}
}
</style>
@@ -0,0 +1,24 @@
<script lang="ts">
import { onMount } from 'svelte';
let spreadsheetHeight = '75vh';
let spreadsheetWrapper: HTMLDivElement;
function resizeSheet() {
if (!spreadsheetWrapper) return;
const rect = spreadsheetWrapper.getBoundingClientRect();
spreadsheetHeight = window.innerHeight - rect.top + 'px';
}
onMount(resizeSheet);
</script>
<svelte:window on:resize={resizeSheet} />
<div
bind:this={spreadsheetWrapper}
style:height={spreadsheetHeight}
style:position="relative"
style:width="100%">
<slot />
</div>
@@ -31,9 +31,7 @@
FloatingActionBar,
Icon,
InteractiveText,
Typography,
Sheet,
Divider
Typography
} from '@appwrite.io/pink-svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import DualTimeView from '$lib/components/dualTimeView.svelte';
@@ -58,30 +56,23 @@
import { type Action } from './sheetOptions.svelte';
import EditAttribute from './attributes/edit.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import SideSheet from './layout/sidesheet.svelte';
import SpreadsheetContainer from './layout/spreadsheet.svelte';
export let data: PageData;
export let showRecordsCreateSheet: boolean;
const databaseId = page.params.database;
const collectionId = page.params.collection;
let displayNames = {};
let showRelationships = false;
let selectedRelationship: Models.AttributeRelationship = null;
let relationshipData: Partial<Models.Document>[];
let spreadsheetHeight = '75vh';
let spreadsheetWrapper: HTMLDivElement;
let selectedRelationship: Models.AttributeRelationship = null;
$: documents = data.documents;
function resizeSheet() {
if (!spreadsheetWrapper) return;
const rect = spreadsheetWrapper.getBoundingClientRect();
spreadsheetHeight = window.innerHeight - rect.top + 'px';
}
onMount(async () => {
resizeSheet();
displayNames = preferences.getDisplayNames();
});
@@ -316,7 +307,7 @@
if (type === 'update') {
$databaseSheetOptions.show = true;
$databaseSheetOptions.isEdit = true;
$databaseSheetOptions.title = 'update column';
$databaseSheetOptions.title = 'Update column';
}
if (type === 'column-left' || type === 'column-right') {
@@ -339,13 +330,7 @@
: emptyCellsLimit - data.documents.documents.length;
</script>
<svelte:window on:resize={resizeSheet} />
<div
bind:this={spreadsheetWrapper}
style:height={spreadsheetHeight}
style:position="relative"
style:width="100%">
<SpreadsheetContainer>
<Spreadsheet.Root
let:root
{loading}
@@ -361,7 +346,10 @@
{#each $columns as column (column.id)}
{#if column.isAction}
<Spreadsheet.Header.Cell column="actions" {root}>
<Button.Button icon variant="extra-compact" on:click={() => {}}>
<Button.Button
icon
variant="extra-compact"
on:click={() => (showRecordsCreateSheet = true)}>
<Icon icon={IconPlus} color="--fgcolor-neutral-primary" />
</Button.Button>
</Spreadsheet.Header.Cell>
@@ -553,7 +541,7 @@
</FloatingActionBar>
</div>
{/if}
</div>
</SpreadsheetContainer>
<RelationshipsModal bind:show={showRelationships} {selectedRelationship} data={relationshipData} />
@@ -624,49 +612,19 @@
</p>
</Confirm>
<div style="position: absolute; top: 0; ">
<Sheet bind:open={$databaseSheetOptions.show} closeOnBlur={false}>
<div slot="header" style:width="100%">
<Layout.Stack direction="row" justifyContent="space-between" alignItems="center">
<Layout.Stack direction="row" gap="m" alignItems="center">
<Typography.Text variant="m-400">{$databaseSheetOptions.title}</Typography.Text>
</Layout.Stack>
</Layout.Stack>
</div>
<Layout.Stack direction="column" justifyContent="space-evenly">
<Layout.Stack gap="xl">
<EditAttribute
isModal={false}
showEdit={$databaseSheetOptions.isEdit}
selectedAttribute={$databaseSheetOptions.column} />
</Layout.Stack>
<div class="sheet-footer">
<Layout.Stack gap="l">
<Divider />
<div class="sheet-footer-actions">
<Layout.Stack gap="m" direction="row" justifyContent="flex-end">
<Button.Button
size="s"
variant="secondary"
on:click={() => ($databaseSheetOptions.show = false)}
>Cancel</Button.Button>
<Button.Button
size="s"
disabled={$databaseSheetOptions.disableSubmit}
on:click={() => $databaseSheetOptions.submitAction()}>
Update
</Button.Button>
</Layout.Stack>
</div>
</Layout.Stack>
</div>
</Layout.Stack>
</Sheet>
</div>
<SideSheet
title={$databaseSheetOptions.title}
bind:show={$databaseSheetOptions.show}
submit={{
text: 'Update',
disabled: $databaseSheetOptions.disableSubmit,
onClick: () => $databaseSheetOptions.submitAction()
}}>
<EditAttribute
isModal={false}
showEdit={$databaseSheetOptions.isEdit}
selectedAttribute={$databaseSheetOptions.column} />
</SideSheet>
<style lang="scss">
.floating-action-bar {
@@ -676,15 +634,4 @@
position: absolute;
transform: translateX(-50%);
}
.sheet-footer {
left: 0;
right: 0;
bottom: 0;
position: absolute;
& .sheet-footer-actions {
padding: var(--space-8);
}
}
</style>