mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge remote-tracking branch 'origin/main' into feat-csv-export
# Conflicts: # package.json # pnpm-lock.yaml # src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+page.svelte
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
name: Dockerize Profiles
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [feat-profiles]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches: [feat-profiles]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dockerize-profiles:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: appwrite/console-profiles
|
||||
tags: |
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=gh-${{ github.run_id}}
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+2
-2
@@ -24,9 +24,9 @@
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@d0c5a57",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
|
||||
Generated
+10
-10
@@ -18,14 +18,14 @@ importers:
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
'@appwrite.io/pink-icons-svelte':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0(svelte@5.25.3)
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a(svelte@5.25.3)
|
||||
'@appwrite.io/pink-legacy':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@appwrite.io/pink-svelte':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0(svelte@5.25.3)
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a(svelte@5.25.3)
|
||||
'@faker-js/faker':
|
||||
specifier: ^9.9.0
|
||||
version: 9.9.0
|
||||
@@ -269,8 +269,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0}
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a}
|
||||
version: 2.0.0-RC.1
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -284,8 +284,8 @@ packages:
|
||||
'@appwrite.io/pink-legacy@1.0.3':
|
||||
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0}
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a}
|
||||
version: 2.0.0-RC.2
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0
|
||||
@@ -3709,7 +3709,7 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c4d7bc0(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bd82d9a(svelte@5.25.3)':
|
||||
dependencies:
|
||||
svelte: 5.25.3
|
||||
|
||||
@@ -3722,7 +3722,7 @@ snapshots:
|
||||
'@appwrite.io/pink-icons': 1.0.0
|
||||
the-new-css-reset: 1.11.3
|
||||
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@c4d7bc0(svelte@5.25.3)':
|
||||
'@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@bd82d9a(svelte@5.25.3)':
|
||||
dependencies:
|
||||
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
|
||||
'@floating-ui/dom': 1.6.13
|
||||
|
||||
@@ -20,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>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { InputDateTime, InputSelect } from '$lib/elements/forms';
|
||||
import { isSameDay, isValidDate, toLocaleDate } from '$lib/helpers/date';
|
||||
import { InputDate, InputSelect } from '$lib/elements/forms';
|
||||
import { isSameDay, isValidDate, toLocaleDate, toLocaleDateISO } from '$lib/helpers/date';
|
||||
|
||||
function incrementToday(value: number, type: 'day' | 'month' | 'year'): string {
|
||||
const date = new Date();
|
||||
@@ -74,6 +74,10 @@
|
||||
export let resourceType: string | 'key' | 'token' | undefined = 'key';
|
||||
export let expiryOptions: 'default' | 'limited' | ExpirationOptions[] = 'default';
|
||||
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
let minDate: string = toLocaleDateISO(tomorrow.getTime());
|
||||
|
||||
const options = Array.isArray(expiryOptions)
|
||||
? expiryOptions
|
||||
: expiryOptions === 'default'
|
||||
@@ -129,6 +133,8 @@
|
||||
if (hasUserInteracted && !isSameDay(new Date(expirationSelect), new Date(value))) {
|
||||
value = expirationSelect === 'custom' ? expirationCustom : expirationSelect;
|
||||
}
|
||||
|
||||
value = toLocaleDateISO(new Date(value).getTime());
|
||||
}
|
||||
|
||||
$: helper =
|
||||
@@ -147,11 +153,11 @@
|
||||
on:change={() => (hasUserInteracted = true)} />
|
||||
|
||||
{#if expirationSelect === 'custom'}
|
||||
<InputDateTime
|
||||
<InputDate
|
||||
required
|
||||
type="date"
|
||||
id="expire"
|
||||
min={minDate}
|
||||
label={dateSelectorLabel}
|
||||
bind:value={expirationCustom}
|
||||
on:change={() => (hasUserInteracted = true)} />
|
||||
on:input={() => (hasUserInteracted = true)} />
|
||||
{/if}
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
repository.set(e);
|
||||
repositoryName = e.name;
|
||||
selectedRepository = e.id;
|
||||
connectRepo();
|
||||
}} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
{#each [...$groups] as [role, permission] (role)}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="role" {root}>
|
||||
<Row {role} onNotFound={() => deleteRole(role)} />
|
||||
<Row {role} />
|
||||
</Table.Cell>
|
||||
<Table.Cell column="create" {root}>
|
||||
<Selector.Checkbox
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
hideColumns?: boolean;
|
||||
allowNoColumns?: boolean;
|
||||
showAnyway?: boolean;
|
||||
disableButton?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -32,7 +33,8 @@
|
||||
hideView = false,
|
||||
hideColumns = false,
|
||||
allowNoColumns = false,
|
||||
showAnyway = false
|
||||
showAnyway = false,
|
||||
disableButton = false
|
||||
}: Props = $props();
|
||||
|
||||
let showCountBadge = $state(false);
|
||||
@@ -70,7 +72,7 @@
|
||||
icon={onlyIcon}
|
||||
onclick={toggle}
|
||||
variant="secondary"
|
||||
disabled={!$columns.length && showAnyway}
|
||||
disabled={(!$columns.length && showAnyway) || disableButton}
|
||||
class={onlyIcon && !$isSmallViewport ? 'width-fix' : undefined}
|
||||
badge={showCountBadge ? selectedColumnsNumber.toString() : undefined}>
|
||||
<Icon slot="start" icon={IconViewBoards} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,10 +1,51 @@
|
||||
<script>
|
||||
import { Container } from '$lib/layout';
|
||||
import { base } from '$app/paths';
|
||||
import { loading } from '$routes/store';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
loading.set(false);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<slot />
|
||||
</Container>
|
||||
<Layout.Stack
|
||||
height="100vh"
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style="background: var(--bgcolor-neutral-primary, #fff);">
|
||||
<section class="console-container">
|
||||
<slot />
|
||||
</section>
|
||||
<footer>
|
||||
<Typography.Eyebrow color="--fgcolor-neutral-secondary">POWERED BY</Typography.Eyebrow>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src="{base}/images/appwrite-logo-dark.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src="{base}/images/appwrite-logo-light.svg"
|
||||
width="120"
|
||||
height="22"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</footer>
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
footer {
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app';
|
||||
import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg';
|
||||
import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg';
|
||||
import { Vcs, Client } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { getApiEndpoint } from '$lib/stores/sdk';
|
||||
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
|
||||
export let data;
|
||||
const { data } = $props();
|
||||
|
||||
const endpoint = getApiEndpoint();
|
||||
const client = new Client();
|
||||
const vcs = new Vcs(client);
|
||||
|
||||
let installationId: string;
|
||||
let repositoryId: string;
|
||||
let providerPullRequestId: string;
|
||||
|
||||
let error = '';
|
||||
let success = '';
|
||||
let loading = false;
|
||||
let error = $state('');
|
||||
let success = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
repositoryId = data.repositoryId;
|
||||
installationId = data.installationId;
|
||||
providerPullRequestId = data.providerPullRequestId;
|
||||
|
||||
client.setEndpoint(endpoint).setProject(data.projectId).setMode('admin');
|
||||
});
|
||||
|
||||
@@ -39,9 +30,9 @@
|
||||
|
||||
try {
|
||||
await vcs.updateExternalDeployments({
|
||||
installationId,
|
||||
repositoryId,
|
||||
providerPullRequestId
|
||||
installationId: data.installationId,
|
||||
repositoryId: data.repositoryId,
|
||||
providerPullRequestId: data.providerPullRequestId
|
||||
});
|
||||
success = 'Deployment approved successfully! Build will start soon.';
|
||||
} catch (e) {
|
||||
@@ -52,49 +43,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="container" style="display: grid; place-items: center; min-height: 100vh;">
|
||||
<div class="u-flex u-flex-vertical u-cross-center" style="width: 100%">
|
||||
<div class="card" style="min-width: 600px; max-width: 100%;">
|
||||
<h1 class="heading-level-2">Authorize External Deployment</h1>
|
||||
<small style="margin-block-start: 8px;display: block;"
|
||||
>The deployment for pull request <code class="inline-code"
|
||||
>#{providerPullRequestId}</code> is awaiting approval. When authorized, deployments
|
||||
will be started.
|
||||
</small>
|
||||
|
||||
<div class="with-borders" style="margin-block-start: 1rem;display: block;">
|
||||
<button disabled={loading} on:click={approveDeployment} class="button" type="button"
|
||||
>Approve Deployment</button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p style="margin-block-start: 1rem" class="u-color-text-danger">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<p style="margin-block-start: 1rem" class="u-color-text-success">{success}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-gap-4 u-flex u-main-center u-cross-center" style="margin-block-start: 2rem;">
|
||||
<span class="text">Powered by</span>
|
||||
<a
|
||||
href="https://appwrite.io/"
|
||||
target="_blank"
|
||||
style="display: grid;place-items: center;">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="120"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="120"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Layout.Stack gap="l" alignItems="center" style="max-width: 500px;">
|
||||
{#if success}
|
||||
<Badge type="success" variant="secondary" content={success} />
|
||||
{:else if error}
|
||||
<Badge type="error" variant="secondary" content={error} />
|
||||
{/if}
|
||||
<Typography.Title size="l" align="center">
|
||||
The deployment for pull request #{data.providerPullRequestId}
|
||||
is awaiting approval. When authorized, deployments will be started.
|
||||
</Typography.Title>
|
||||
<Button on:click={approveDeployment} secondary>Approve Deployment</Button>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let organization = data.organization;
|
||||
|
||||
// Reactive statement to update organization when data changes
|
||||
$: organization = data.organization;
|
||||
|
||||
// why are these reactive?
|
||||
$: defaultPaymentMethod = data?.paymentMethods?.paymentMethods?.find(
|
||||
|
||||
+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>
|
||||
+1527
-312
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}
|
||||
|
||||
+22
-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,9 +131,11 @@ export type ColumnInput = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
format?: string;
|
||||
elements?: string[];
|
||||
formatOptions?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
elements?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -134,6 +144,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 +155,11 @@ 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.format === 'enum'
|
||||
? (col.elements ?? col.formatOptions?.elements ?? undefined)
|
||||
: 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">
|
||||
|
||||
+100
-44
@@ -8,7 +8,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,
|
||||
@@ -29,9 +29,11 @@
|
||||
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import {
|
||||
IconBookOpen,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconPlus,
|
||||
IconViewBoards,
|
||||
IconRefresh,
|
||||
IconUpload,
|
||||
IconDownload
|
||||
@@ -41,9 +43,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;
|
||||
|
||||
@@ -95,6 +103,8 @@
|
||||
$tableColumnSuggestions.table &&
|
||||
$tableColumnSuggestions.table.id === page.params.table;
|
||||
|
||||
$: disableButton = canShowSuggestionsSheet;
|
||||
|
||||
async function onSelect(file: Models.File, localFile = false) {
|
||||
$isCsvImportInProgress = true;
|
||||
|
||||
@@ -156,7 +166,8 @@
|
||||
columns={tableColumns}
|
||||
hideView
|
||||
showAnyway
|
||||
isCustomTable />
|
||||
isCustomTable
|
||||
{disableButton} />
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="tooltip">Columns</svelte:fragment>
|
||||
@@ -167,7 +178,7 @@
|
||||
onlyIcon
|
||||
query={data.query}
|
||||
columns={filterColumns}
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
analyticsSource="database_tables" />
|
||||
|
||||
<svelte:fragment slot="tooltip">Filters</svelte:fragment>
|
||||
@@ -183,7 +194,7 @@
|
||||
<Button
|
||||
secondary
|
||||
event="create_row"
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
on:click={() => ($showRowCreateSheet.show = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create row
|
||||
@@ -243,7 +254,8 @@
|
||||
secondary
|
||||
disabled={isRefreshing ||
|
||||
!data.rows.total ||
|
||||
!(hasColumns && hasValidColumns)}
|
||||
!(hasColumns && hasValidColumns) ||
|
||||
disableButton}
|
||||
class="small-button-dimensions"
|
||||
on:click={async () => {
|
||||
isRefreshing = true;
|
||||
@@ -265,7 +277,7 @@
|
||||
<Button
|
||||
secondary
|
||||
event="create_row"
|
||||
disabled={!(hasColumns && hasValidColumns)}
|
||||
disabled={!(hasColumns && hasValidColumns) || disableButton}
|
||||
on:click={() => ($showRowCreateSheet.show = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Create row
|
||||
@@ -275,7 +287,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} />
|
||||
@@ -283,58 +295,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;
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Icon, Layout, Tag, Typography, Button, Card } from '@appwrite.io/pink-svelte';
|
||||
import { Icon, Input, Layout, Popover, Tag, Typography, Card } from '@appwrite.io/pink-svelte';
|
||||
import { supportData, isSupportOnline } from './wizard/support/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Form, InputSelect, InputText, InputTextarea } from '$lib/elements/forms/index.js';
|
||||
|
||||
import {
|
||||
Form,
|
||||
InputSelect,
|
||||
InputText,
|
||||
InputTextarea,
|
||||
Button
|
||||
} from '$lib/elements/forms/index.js';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import {
|
||||
localeTimezoneName,
|
||||
@@ -18,29 +24,83 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { IconCheckCircle, IconXCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
import { IconCheckCircle, IconXCircle, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let projectOptions: Array<{ value: string; label: string }>;
|
||||
let projectOptions = $state<Array<{ value: string; label: string }>>([]);
|
||||
|
||||
// Category options with display names
|
||||
const categories = [
|
||||
{ value: 'general', label: 'General' },
|
||||
{ value: 'billing', label: 'Billing' },
|
||||
{ value: 'technical', label: 'Technical' }
|
||||
];
|
||||
|
||||
// Topic options based on category
|
||||
const topicsByCategory = {
|
||||
general: ['Security', 'Compliance', 'Performance'],
|
||||
billing: ['Invoices', 'Plans'],
|
||||
technical: [
|
||||
'Auth',
|
||||
'Databases',
|
||||
'Storage',
|
||||
'Functions',
|
||||
'Realtime',
|
||||
'Messaging',
|
||||
'Migrations',
|
||||
'Webhooks',
|
||||
'SDKs',
|
||||
'Console'
|
||||
]
|
||||
};
|
||||
|
||||
// Severity options
|
||||
const severityOptions = [
|
||||
{ value: 'critical', label: 'Critical' },
|
||||
{ value: 'high', label: 'High' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'question', label: 'Question' }
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const projectList = await sdk.forConsole.projects.list();
|
||||
// Filter projects by organization ID using server-side queries
|
||||
const projectList = await sdk.forConsole.projects.list({
|
||||
queries: $organization?.$id ? [Query.equal('teamId', $organization.$id)] : []
|
||||
});
|
||||
projectOptions = projectList.projects.map((project) => ({
|
||||
value: project.$id,
|
||||
label: project.name
|
||||
}));
|
||||
});
|
||||
|
||||
// Cleanup on component destroy
|
||||
onDestroy(() => {
|
||||
$supportData = {
|
||||
message: null,
|
||||
subject: null,
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
topic: undefined,
|
||||
severity: 'question',
|
||||
file: null
|
||||
};
|
||||
});
|
||||
|
||||
// Update topic options when category changes
|
||||
const topicOptions = $derived(
|
||||
($supportData.category ? topicsByCategory[$supportData.category] || [] : []).map(
|
||||
(topic) => ({
|
||||
value: topic.toLowerCase(),
|
||||
label: topic
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
async function handleSubmit() {
|
||||
// Create category-topic tag
|
||||
const categoryTopicTag = $supportData.topic
|
||||
? `${$supportData.category}-${$supportData.topic}`.toLowerCase()
|
||||
: $supportData.category.toLowerCase();
|
||||
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/support`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -51,13 +111,13 @@
|
||||
subject: $supportData.subject,
|
||||
firstName: ($user?.name || 'Unknown').slice(0, 40),
|
||||
message: $supportData.message,
|
||||
tags: ['cloud'],
|
||||
tags: [categoryTopicTag],
|
||||
customFields: [
|
||||
{ id: '41612', value: $supportData.category },
|
||||
{ id: '48493', value: $user?.name ?? '' },
|
||||
{ id: '48492', value: $organization?.$id ?? '' },
|
||||
{ id: '48491', value: $supportData?.project ?? '' },
|
||||
{ id: '48490', value: $user?.$id ?? '' }
|
||||
{ id: '56023', value: $supportData?.severity ?? '' },
|
||||
{ id: '56024', value: $organization?.billingPlan ?? '' }
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -84,7 +144,9 @@
|
||||
$supportData = {
|
||||
message: null,
|
||||
subject: null,
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
topic: undefined,
|
||||
severity: undefined,
|
||||
file: null,
|
||||
project: null
|
||||
};
|
||||
@@ -99,10 +161,43 @@
|
||||
endDay: 'Friday' as WeekDay
|
||||
};
|
||||
|
||||
$: supportTimings = `${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`;
|
||||
$: supportWeekDays = `${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`;
|
||||
const supportTimings = $derived(
|
||||
`${utcHourToLocaleHour(workTimings.start)} - ${utcHourToLocaleHour(workTimings.end)} ${localeTimezoneName()}`
|
||||
);
|
||||
const supportWeekDays = $derived(
|
||||
`${utcWeekDayToLocaleWeekDay(workTimings.startDay, workTimings.start)} - ${utcWeekDayToLocaleWeekDay(workTimings.endDay, workTimings.end)}`
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet severityPopover()}
|
||||
<Popover let:toggle>
|
||||
<Button extraCompact size="s" on:click={toggle}>
|
||||
<Icon size="s" icon={IconInfo} />
|
||||
</Button>
|
||||
<div slot="tooltip" style="max-width: 400px;">
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text>
|
||||
<b>Critical:</b> System is down or a critical component is non-functional, causing
|
||||
a complete stoppage of work or significant business impact.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>High:</b> Major functionality is impaired, but a workaround is available, or a
|
||||
critical component is significantly degraded.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Medium:</b> Minor functionality is impaired without significant business impact.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Low:</b> Issue has minor impact on business operations; workaround is not necessary.
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<b>Question:</b> Requests for information, general guidance, or feature requests.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</div>
|
||||
</Popover>
|
||||
{/snippet}
|
||||
|
||||
<Wizard title="Contact us" confirmExit={true}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Layout.Stack gap="xl">
|
||||
@@ -113,24 +208,48 @@
|
||||
</Layout.Stack>
|
||||
<Layout.Stack gap="s">
|
||||
<Typography.Text color="--fgcolor-neutral-secondary"
|
||||
>Choose a topic</Typography.Text>
|
||||
>Choose a category</Typography.Text>
|
||||
<Layout.Stack gap="s" direction="row">
|
||||
{#each ['general', 'billing', 'technical'] as category}
|
||||
{#each categories as category}
|
||||
<Tag
|
||||
on:click={() => {
|
||||
$supportData.category = category;
|
||||
if ($supportData.category !== category.value) {
|
||||
$supportData.topic = undefined;
|
||||
}
|
||||
$supportData.category = category.value;
|
||||
}}
|
||||
selected={$supportData.category === category}>{category}</Tag>
|
||||
selected={$supportData.category === category.value}
|
||||
>{category.label}</Tag>
|
||||
{/each}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<InputSelect
|
||||
required
|
||||
{#if topicOptions.length > 0}
|
||||
{#key $supportData.category}
|
||||
<Input.ComboBox
|
||||
id="topic"
|
||||
label="Choose a topic"
|
||||
placeholder="Select topic"
|
||||
bind:value={$supportData.topic}
|
||||
options={topicOptions} />
|
||||
{/key}
|
||||
{/if}
|
||||
<Input.ComboBox
|
||||
id="project"
|
||||
label="Choose a project"
|
||||
options={projectOptions ?? []}
|
||||
bind:value={$supportData.project}
|
||||
placeholder="Select project" />
|
||||
<InputSelect
|
||||
id="severity"
|
||||
label="Severity"
|
||||
options={severityOptions}
|
||||
bind:value={$supportData.severity}
|
||||
required
|
||||
placeholder="Select severity">
|
||||
<div slot="info">
|
||||
{@render severityPopover()}
|
||||
</div>
|
||||
</InputSelect>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
@@ -143,15 +262,16 @@
|
||||
bind:value={$supportData.message}
|
||||
placeholder="Type here..."
|
||||
label="Tell us a bit more"
|
||||
required
|
||||
maxlength={4096} />
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" gap="s">
|
||||
<Button.Button
|
||||
<Button
|
||||
size="s"
|
||||
variant="secondary"
|
||||
secondary
|
||||
on:click={() => {
|
||||
wizard.hide();
|
||||
}}>Cancel</Button.Button>
|
||||
<Button.Button size="s">Submit</Button.Button>
|
||||
}}>Cancel</Button>
|
||||
<Button submit size="s">Submit</Button>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Form>
|
||||
|
||||
@@ -4,6 +4,8 @@ export type SupportData = {
|
||||
message: string;
|
||||
subject: string;
|
||||
category: string;
|
||||
topic?: string;
|
||||
severity?: string;
|
||||
file?: File | null;
|
||||
project?: string;
|
||||
};
|
||||
@@ -11,7 +13,8 @@ export type SupportData = {
|
||||
export const supportData = writable<SupportData>({
|
||||
message: '',
|
||||
subject: '',
|
||||
category: 'general',
|
||||
category: 'technical',
|
||||
severity: 'question',
|
||||
file: null
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user