feat: embeddable routes

The changes in this commit refactor several console pages by extracting
the main UI logic into separate view components. This allows the same
views to be reused between the main console and embedding contexts.

Key changes:

- Move page content into view components for auth, database, messaging,
and storage sections - Add embedded versions of pages that reuse the
view components - Update layout configurations to support embedding -
Add new studio theme variations for embedded context

The refactoring improves code reuse and maintainability while enabling
the same UI components to be used in different contexts.
This commit is contained in:
Torsten Dittmann
2025-09-01 20:50:56 +02:00
parent a82e961863
commit b0b2fe8ffd
47 changed files with 1515 additions and 1307 deletions
+5 -4
View File
@@ -34,11 +34,12 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
if (currentOrgId) {
try {
projectsCount = (
await sdk.forConsole.projects.list({
queries: [Query.equal('teamId', currentOrgId), Query.limit(1)]
})
await sdk.forConsole.projects.list([
Query.equal('teamId', currentOrgId),
Query.limit(1)
])
).total;
} catch (e) {
} catch {
projectsCount = 0;
}
}
@@ -3,178 +3,16 @@
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import {
AvatarInitials,
Copy,
Empty,
EmptySearch,
PaginationWithLimit,
SearchQuery
} from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import Create from './createUser.svelte';
import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Tag } from '@appwrite.io/pink-svelte';
import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { canWriteUsers } from '$lib/stores/roles';
import type { Column } from '$lib/helpers/types';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { View } from '$lib/helpers/load';
import View from './view.svelte';
export let data;
const columns = writable<Column[]>([
{ id: '$id', title: 'User ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 260 } },
{ id: 'identifiers', title: 'Identifiers', type: 'string', width: { min: 260 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 140 } },
{ id: 'labels', title: 'Labels', type: 'string', hide: true, width: { min: 140 } },
{ id: 'joined', title: 'Joined', type: 'string', width: { min: 140 } },
{
id: 'lastActivity',
title: 'Last activity',
type: 'string',
hide: true,
width: { min: 140 }
}
]);
async function userCreated(event: CustomEvent<Models.User<Record<string, unknown>>>) {
await goto(
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${event.detail.$id}`
);
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name, email, phone, or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={View.Table} {columns} hideView />
<Button on:click={() => ($showCreateUser = true)} event="create_user" size="s">
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Create user</span>
</Button>
</Layout.Stack>
</Layout.Stack>
{#if data.users.total}
<Table.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title } (id)}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each data.users.users as user}
<Table.Row.Link
href={`${base}/project-${page.params.region}-${page.params.project}/auth/user-${user.$id}`}
{root}>
{#each $columns as { id } (id)}
<Table.Cell column={id} {root}>
{#if id === '$id'}
<Copy value={user.$id} event="user">
<Tag size="xs" variant="code">
<Icon size="s" icon={IconDuplicate} slot="start" />
{user.$id}
</Tag>
</Copy>
{:else if id === 'name'}
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if user.email || user.phone}
{#if user.name}
<AvatarInitials size="xs" name={user.name} />
<Typography.Text truncate>
{user.name}
</Typography.Text>
{:else}
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true"
></span>
</div>
{/if}
{:else}
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true"></span>
</div>
<Typography.Text truncate>
{user.name}
</Typography.Text>
{/if}
</Layout.Stack>
{:else if id === 'identifiers'}
<Typography.Text truncate>
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
</Typography.Text>
{:else if id === 'status'}
{#if user.status}
{@const success =
user.emailVerification || user.phoneVerification}
<Badge
size="xs"
variant="secondary"
type={success ? 'success' : undefined}
content={user.emailVerification && user.phoneVerification
? 'Verified'
: user.emailVerification
? 'Verified email'
: user.phoneVerification
? 'Verified phone'
: 'Unverified'} />
{:else}
<Badge
size="xs"
variant="secondary"
type="error"
content="blocked" />
{/if}
{:else if id === 'labels'}
<Typography.Text truncate>
{user.labels.join(', ')}
</Typography.Text>
{:else if id === 'joined'}
{toLocaleDateTime(user.registration)}
{:else if id === 'lastActivity'}
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
{:else}
{user[id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{/each}
</Table.Root>
<PaginationWithLimit
name="Users"
limit={data.limit}
offset={data.offset}
total={data.users.total} />
{:else if data.search}
<EmptySearch target="users" hidePagination>
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/auth`}
size="s"
secondary>Clear Search</Button>
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/references/cloud/server-nodejs/users"
target="user"
allowCreate={$canWriteUsers}
on:click={() => showCreateUser.set(true)} />
{/if}
</Container>
<Create bind:showCreate={$showCreateUser} on:created={userCreated} />
<View
users={{ total: data.users.total, users: data.users.users }}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
@@ -1,6 +1,5 @@
<script lang="ts">
import { Container } from '$lib/layout';
import UpdateMockNumbers from './updateMockNumbers.svelte';
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
@@ -21,6 +20,5 @@
<UpdatePersonalDataCheck />
<UpdateSessionAlerts />
<UpdateSessionInvalidation />
<UpdateMockNumbers />
<UpdateMembershipPrivacy />
</Container>
@@ -1,128 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { InputSwitch } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { app } from '$lib/stores/app';
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
import { addNotification } from '$lib/stores/notifications';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import { project } from '../../store';
import { base } from '$app/paths';
import { Avatar, Badge, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
const projectId = page.params.project;
let showProvider = false;
let selectedProvider: Models.AuthProvider | null = null;
async function authUpdate(box: AuthMethod) {
try {
await sdk.forConsole.projects.updateAuthStatus({
projectId,
method: box.method,
status: box.value
});
addNotification({
type: 'success',
message: `${box.label} authentication has been updated`
});
trackEvent(Submit.AuthStatusUpdate, {
method: box.method,
value: box.value
});
invalidate(Dependencies.PROJECT);
} catch (error) {
box.value = !box.value;
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AuthStatusUpdate);
}
}
$: authMethods.load($project);
import View from './view.svelte';
export const data = {};
</script>
{#if $authMethods && $project}
<Container>
<Layout.Stack gap="xxl">
<CardGrid>
<svelte:fragment slot="title">Auth methods</svelte:fragment>
Enable the authentication methods you wish to use.
<svelte:fragment slot="aside">
<Layout.Stack gap="l" direction="row" wrap="wrap">
{#each $authMethods.list as box}
<div style="flex-basis: 45%;">
<InputSwitch
label={box.label}
id={box.method}
bind:value={box.value}
on:change={() => authUpdate(box)} />
</div>
{/each}
</Layout.Stack>
</svelte:fragment>
</CardGrid>
<Layout.Stack>
<Typography.Title size="s">OAuth2 Providers</Typography.Title>
<ul class="grid-box" style:--grid-gap="1rem" style:--grid-item-size="15rem">
{#each $project.oAuthProviders
.filter((p) => p.name !== 'Mock')
.sort( (a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1) ) as provider}
{@const oAuthProvider = oAuthProviders[provider.key]}
{#if oAuthProvider}
<Card.Button
padding="s"
on:click={() => {
selectedProvider = provider;
showProvider = true;
trackEvent(`click_select_provider`, {
provider: provider.key.toLowerCase()
});
}}>
<Layout.Stack alignItems="flex-start" gap="xxl">
<Layout.Stack
direction="row"
justifyContent="flex-start"
alignItems="center">
<Avatar size="s">
<img
height="20"
width="20"
src={`${base}/icons/${$app.themeInUse}/color/${oAuthProvider.icon}.svg`}
alt={provider.name} />
</Avatar>
<Typography.Text>{provider.name}</Typography.Text>
</Layout.Stack>
<Badge
type={provider.enabled ? 'success' : undefined}
variant="secondary"
content={provider.enabled ? 'enabled' : 'disabled'} />
</Layout.Stack>
</Card.Button>
{/if}
{/each}
</ul>
</Layout.Stack>
</Layout.Stack>
</Container>
{/if}
{#if selectedProvider && showProvider}
{@const oAuthProvider = oAuthProviders[selectedProvider.key]}
<svelte:component
this={oAuthProvider.component}
bind:provider={selectedProvider}
bind:show={showProvider}
onclose={() => {
selectedProvider = null;
showProvider = false;
}} />
{/if}
<View />
@@ -0,0 +1,82 @@
<script lang="ts">
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { InputSwitch } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { app } from '$lib/stores/app';
import { authMethods, type AuthMethod } from '$lib/stores/auth-methods';
import { addNotification } from '$lib/stores/notifications';
import { oAuthProviders } from '$lib/stores/oauth-providers';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { base } from '$app/paths';
import { Avatar, Badge, Card, Layout, Typography } from '@appwrite.io/pink-svelte';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
const projectId = page.params.project;
let showProvider = false;
let selectedProvider = null;
async function authUpdate(box: AuthMethod) {
try {
await sdk.forConsole.projects.updateAuthStatus({ projectId, method: box.method, status: box.value });
addNotification({ type: 'success', message: `${box.label} authentication has been updated` });
trackEvent(Submit.AuthStatusUpdate, { method: box.method, value: box.value });
invalidate(Dependencies.PROJECT);
} catch (error) {
box.value = !box.value;
addNotification({ type: 'error', message: error.message });
trackError(error, Submit.AuthStatusUpdate);
}
}
$: authMethods.load($project);
</script>
{#if $authMethods && $project}
<Container>
<Layout.Stack gap="xxl">
<CardGrid>
<svelte:fragment slot="title">Auth methods</svelte:fragment>
Enable the authentication methods you wish to use.
<svelte:fragment slot="aside">
<Layout.Stack gap="l" direction="row" wrap="wrap">
{#each $authMethods.list as box}
<div style="flex-basis: 45%;">
<InputSwitch label={box.label} id={box.method} bind:value={box.value} on:change={() => authUpdate(box)} />
</div>
{/each}
</Layout.Stack>
</svelte:fragment>
</CardGrid>
<Layout.Stack>
<Typography.Title size="s">OAuth2 Providers</Typography.Title>
<ul class="grid-box" style:--grid-gap="1rem" style:--grid-item-size="15rem">
{#each $project.oAuthProviders.filter((p) => p.name !== 'Mock').sort((a, b) => (a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1)) as provider}
{@const oAuthProvider = oAuthProviders[provider.key]}
{#if oAuthProvider}
<Card.Button padding="s" on:click={() => { selectedProvider = provider; showProvider = true; trackEvent(`click_select_provider`, { provider: provider.key.toLowerCase() }); }}>
<Layout.Stack alignItems="flex-start" gap="xxl">
<Layout.Stack direction="row" justifyContent="flex-start" alignItems="center">
<Avatar size="s">
<img height="20" width="20" src={`${base}/icons/${$app.themeInUse}/color/${oAuthProvider.icon}.svg`} alt={provider.name} />
</Avatar>
<Typography.Text>{provider.name}</Typography.Text>
</Layout.Stack>
<Badge type={provider.enabled ? 'success' : undefined} variant="secondary" content={provider.enabled ? 'enabled' : 'disabled'} />
</Layout.Stack>
</Card.Button>
{/if}
{/each}
</ul>
</Layout.Stack>
</Layout.Stack>
</Container>
{/if}
{#if selectedProvider && showProvider}
{@const oAuthProvider = oAuthProviders[selectedProvider.key]}
<svelte:component this={oAuthProvider.component} bind:provider={selectedProvider} bind:show={showProvider} onclose={() => { selectedProvider = null; showProvider = false; }} />
{/if}
@@ -1,96 +1,17 @@
<script lang="ts" context="module">
import { writable } from 'svelte/store';
export let showCreateTeam = writable(false);
</script>
<script lang="ts">
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import {
Empty,
EmptySearch,
AvatarInitials,
SearchQuery,
PaginationWithLimit
} from '$lib/components';
import Create from '../createTeam.svelte';
import { goto } from '$app/navigation';
import { Container } from '$lib/layout';
import { base } from '$app/paths';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import { canWriteTeams } from '$lib/stores/roles';
import { Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import View from './view.svelte';
export let data;
const region = page.params.region;
const project = page.params.project;
const teamCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
await goto(`${base}/project-${region}-${project}/auth/teams/team-${event.detail.$id}`);
};
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button on:mousedown={() => ($showCreateTeam = true)} event="create_user" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create team
</Button>
</Layout.Stack>
</Layout.Stack>
{#if data.teams.total}
<Table.Root columns={3} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Name</Table.Header.Cell>
<Table.Header.Cell {root}>Members</Table.Header.Cell>
<Table.Header.Cell {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each data.teams.teams as team}
<Table.Row.Link
{root}
href={`${base}/project-${region}-${project}/auth/teams/team-${team.$id}`}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={team.name} />
<span class="u-trim">{team.name}</span>
</Layout.Stack>
</Table.Cell>
<Table.Cell {root}>
{team.total} members
</Table.Cell>
<Table.Cell {root}>
<DualTimeView time={team.$createdAt} />
</Table.Cell>
</Table.Row.Link>
{/each}
</Table.Root>
<PaginationWithLimit
name="Teams"
limit={data.limit}
offset={data.offset}
total={data.teams.total} />
{:else if data.search}
<EmptySearch target="teams" search={data.search} hidePagination={data.teams.total === 0}>
<Button secondary size="s" href={`${base}/project-${region}-${project}/auth/teams`}>
Clear Search
</Button>
</EmptySearch>
{:else}
<Empty
single
allowCreate={$canWriteTeams}
on:click={() => ($showCreateTeam = true)}
href="https://appwrite.io/docs/references/cloud/client-web/teams"
target="team" />
{/if}
</Container>
<Create bind:showCreate={$showCreateTeam} on:created={teamCreated} />
<View
teams={{ total: data.teams.total, teams: data.teams.teams }}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
@@ -0,0 +1,104 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { Button } from '$lib/elements/forms';
import { Empty, EmptySearch, AvatarInitials, SearchQuery, PaginationWithLimit } from '$lib/components';
import { Container } from '$lib/layout';
import { Icon, Layout, Table } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import CreateTeam from '../createTeam.svelte';
export let teams: { total: number; teams: Models.Team<Record<string, unknown>>[] };
export let limit: number;
export let offset: number;
export const pageNum: number = 0;
export let search: string | null;
const clearSearchHref = `${base}/project-${page.params.region}-${page.params.project}/auth/teams`;
const getTeamUrl = (t: Models.Team<Record<string, unknown>>) =>
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${t.$id}`;
let showCreateTeam = false;
async function onTeamCreated(e: CustomEvent<Models.Team<Record<string, unknown>>>) {
await goto(getTeamUrl(e.detail));
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button on:mousedown={() => (showCreateTeam = true)} event="create_user" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create team
</Button>
</Layout.Stack>
</Layout.Stack>
{#if teams.total}
<Table.Root columns={3} let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell {root}>Name</Table.Header.Cell>
<Table.Header.Cell {root}>Members</Table.Header.Cell>
<Table.Header.Cell {root}>Created</Table.Header.Cell>
</svelte:fragment>
{#each teams.teams as team}
{@const href = getTeamUrl(team)}
{#if href}
<Table.Row.Link {root} href={href}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={team.name} />
<span class="u-trim">{team.name}</span>
</Layout.Stack>
</Table.Cell>
<Table.Cell {root}>
{team.total} members
</Table.Cell>
<Table.Cell {root}>
<DualTimeView time={team.$createdAt} />
</Table.Cell>
</Table.Row.Link>
{:else}
<Table.Row.Base {root}>
<Table.Cell {root}>
<Layout.Stack direction="row" alignItems="center">
<AvatarInitials size="xs" name={team.name} />
<span class="u-trim">{team.name}</span>
</Layout.Stack>
</Table.Cell>
<Table.Cell {root}>
{team.total} members
</Table.Cell>
<Table.Cell {root}>
<DualTimeView time={team.$createdAt} />
</Table.Cell>
</Table.Row.Base>
{/if}
{/each}
</Table.Root>
<PaginationWithLimit name="Teams" {limit} {offset} total={teams.total} />
{:else if search}
<EmptySearch target="teams" search={search} hidePagination={teams.total === 0}>
<Button secondary size="s" href={clearSearchHref}>
Clear Search
</Button>
</EmptySearch>
{:else}
<Empty
single
allowCreate={true}
on:click={() => (showCreateTeam = true)}
href="https://appwrite.io/docs/references/cloud/client-web/teams"
target="team" />
{/if}
</Container>
<CreateTeam bind:showCreate={showCreateTeam} on:created={onTeamCreated} />
@@ -1,150 +1,6 @@
<script context="module" lang="ts">
import {
EmailTemplateLocale,
EmailTemplateType,
SmsTemplateLocale,
SmsTemplateType
} from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
export async function loadEmailTemplate(
projectId: string,
type: EmailTemplateType,
locale: EmailTemplateLocale
) {
try {
return await sdk.forConsole.projects.getEmailTemplate({
projectId,
type: type,
locale: locale
});
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
}
}
export async function loadSmsTemplate(
projectId: string,
type: SmsTemplateType,
locale: SmsTemplateLocale
) {
try {
return await sdk.forConsole.projects.getSMSTemplate({
projectId,
type,
locale
});
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
}
}
</script>
<script lang="ts">
import { base } from '$app/paths';
import { CardGrid } from '$lib/components';
import { Container } from '$lib/layout';
import { baseEmailTemplate, emailTemplate, templates } from './store';
import { Button } from '$lib/elements/forms';
import { currentPlan } from '$lib/stores/organization';
import EmailSignature from './emailSignature.svelte';
import { isCloud } from '$lib/system';
import { Accordion, Alert, Badge, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
import View from './view.svelte';
export let data;
let templateType = null;
let isTemplateLoading = false;
let openStates = Object.fromEntries(templates.map(({ key }) => [key, false]));
loadTemplateFor(EmailTemplateType.Verification);
async function loadTemplateFor(type: EmailTemplateType) {
// return, already loaded!
if (templateType === type) return;
templateType = type;
isTemplateLoading = true;
$emailTemplate = await loadEmailTemplate(page.params.project, type, EmailTemplateLocale.En);
$baseEmailTemplate = { ...$emailTemplate };
isTemplateLoading = false;
}
function toggleAccordion(type: EmailTemplateType) {
for (const key in openStates) {
openStates[key] = false;
}
openStates[type] = true;
loadTemplateFor(type);
}
</script>
<Container>
{#if !data.project.smtpEnabled}
<Alert.Inline
dismissible={false}
status="info"
title="Custom SMTP server is required for customizing emails">
Configure a custom SMTP server to enable custom email templates and prevent emails from
being labeled as spam.
<Button
compact
slot="actions"
href={`${base}/project-${page.params.region}-${page.params.project}/settings/smtp`}>
SMTP settings
</Button>
</Alert.Inline>
{/if}
<CardGrid>
<svelte:fragment slot="title">
Email templates <Badge variant="secondary" content="Experimental" />
</svelte:fragment>
Use templates to send and process account management emails.
<Link.Anchor
target="_blank"
href="https://appwrite.io/docs/advanced/platform/message-templates">
Learn more
</Link.Anchor>
<svelte:fragment slot="aside">
<Layout.Stack gap="s">
{#each templates as section (section.key)}
<Accordion
title={section.title}
hideDivider={section.hideDivider}
bind:open={openStates[section.key]}
on:toggle={(event) => event.detail && toggleAccordion(section.key)}>
<Layout.Stack>
<Typography.Text>{section.description}</Typography.Text>
<svelte:component
this={section.component}
loading={isTemplateLoading} />
</Layout.Stack>
</Accordion>
{/each}
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/settings/smtp`}
secondary>
SMTP settings
</Button>
</svelte:fragment>
</CardGrid>
{#if isCloud && $currentPlan.emailBranding}
<EmailSignature />
{/if}
</Container>
<View project={data.project} />
@@ -1,7 +1,7 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { page } from '$app/state';
import { baseEmailTemplate, emailTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
@@ -1,7 +1,7 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { page } from '$app/state';
import { baseEmailTemplate, emailTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
@@ -1,5 +1,5 @@
<script lang="ts">
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
@@ -1,5 +1,5 @@
<script lang="ts">
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
@@ -2,7 +2,7 @@
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { page } from '$app/state';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
@@ -1,7 +1,7 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { page } from '$app/state';
import { baseEmailTemplate, emailTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
@@ -1,7 +1,7 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { page } from '$app/state';
import { Id } from '$lib/components';
@@ -5,7 +5,7 @@
import { sdk } from '$lib/stores/sdk';
import type { EmailTemplateLocale, EmailTemplateType } from '@appwrite.io/console';
import { project } from '../../store';
import { loadEmailTemplate } from './+page.svelte';
import { loadEmailTemplate } from './view.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { Dialog, Layout, Alert } from '@appwrite.io/pink-svelte';
@@ -0,0 +1,101 @@
<script context="module" lang="ts">
import { EmailTemplateLocale, EmailTemplateType, SmsTemplateLocale, SmsTemplateType } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
export async function loadEmailTemplate(projectId: string, type: EmailTemplateType, locale: EmailTemplateLocale) {
try {
return await sdk.forConsole.projects.getEmailTemplate({ projectId, type, locale });
} catch (e) {
addNotification({ type: 'error', message: e.message });
}
}
export async function loadSmsTemplate(projectId: string, type: SmsTemplateType, locale: SmsTemplateLocale) {
try {
return await sdk.forConsole.projects.getSMSTemplate({ projectId, type, locale });
} catch (e) {
addNotification({ type: 'error', message: e.message });
}
}
</script>
<script lang="ts">
import { base } from '$app/paths';
import { CardGrid } from '$lib/components';
import { Container } from '$lib/layout';
import { baseEmailTemplate, emailTemplate, templates } from '../templates/store';
import { Button } from '$lib/elements/forms';
import { currentPlan } from '$lib/stores/organization';
import EmailSignature from '../templates/emailSignature.svelte';
import { isCloud } from '$lib/system';
import { Accordion, Alert, Badge, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
export let project: Models.Project;
let templateType = null;
let isTemplateLoading = false;
let openStates = Object.fromEntries(templates.map(({ key }) => [key, false]));
loadTemplateFor(EmailTemplateType.Verification);
async function loadTemplateFor(type: EmailTemplateType) {
if (templateType === type) return;
templateType = type;
isTemplateLoading = true;
$emailTemplate = await loadEmailTemplate(page.params.project, type, EmailTemplateLocale.En);
$baseEmailTemplate = { ...$emailTemplate };
isTemplateLoading = false;
}
function toggleAccordion(type: EmailTemplateType) {
for (const key in openStates) openStates[key] = false;
openStates[type] = true;
loadTemplateFor(type);
}
</script>
<Container>
{#if !project.smtpEnabled}
<Alert.Inline dismissible={false} status="info" title="Custom SMTP server is required for customizing emails">
Configure a custom SMTP server to enable custom email templates and prevent emails from being labeled as spam.
<Button compact slot="actions" href={`${base}/project-${page.params.region}-${page.params.project}/settings/smtp`}>
SMTP settings
</Button>
</Alert.Inline>
{/if}
<CardGrid>
<svelte:fragment slot="title">
Email templates <Badge variant="secondary" content="Experimental" />
</svelte:fragment>
Use templates to send and process account management emails.
<Link.Anchor target="_blank" href="https://appwrite.io/docs/advanced/platform/message-templates">Learn more</Link.Anchor>
<svelte:fragment slot="aside">
<Layout.Stack gap="s">
{#each templates as section (section.key)}
<Accordion
title={section.title}
hideDivider={section.hideDivider}
bind:open={openStates[section.key]}
on:toggle={(event) => event.detail && toggleAccordion(section.key)}>
<Layout.Stack>
<Typography.Text>{section.description}</Typography.Text>
<svelte:component this={section.component} loading={isTemplateLoading} />
</Layout.Stack>
</Accordion>
{/each}
</Layout.Stack>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button href={`${base}/project-${page.params.region}-${page.params.project}/settings/smtp`} secondary>
SMTP settings
</Button>
</svelte:fragment>
</CardGrid>
{#if isCloud && $currentPlan.emailBranding}
<EmailSignature />
{/if}
</Container>
@@ -1,24 +1,6 @@
<script lang="ts">
import { Container } from '$lib/layout';
import DangerZone from './dangerZone.svelte';
import UpdateEmail from './updateEmail.svelte';
import UpdateLabels from './updateLabels.svelte';
import UpdateMfa from './updateMfa.svelte';
import UpdateName from './updateName.svelte';
import UpdatePassword from './updatePassword.svelte';
import UpdatePhone from './updatePhone.svelte';
import UpdatePrefs from './updatePrefs.svelte';
import UpdateStatus from './updateStatus.svelte';
import View from './view.svelte';
export const data = {};
</script>
<Container>
<UpdateStatus />
<UpdateName />
<UpdateEmail />
<UpdatePhone />
<UpdatePassword />
<UpdateLabels />
<UpdatePrefs />
<UpdateMfa />
<DangerZone />
</Container>
<View />
@@ -0,0 +1,24 @@
<script lang="ts">
import { Container } from '$lib/layout';
import DangerZone from './dangerZone.svelte';
import UpdateEmail from './updateEmail.svelte';
import UpdateLabels from './updateLabels.svelte';
import UpdateMfa from './updateMfa.svelte';
import UpdateName from './updateName.svelte';
import UpdatePassword from './updatePassword.svelte';
import UpdatePhone from './updatePhone.svelte';
import UpdatePrefs from './updatePrefs.svelte';
import UpdateStatus from './updateStatus.svelte';
</script>
<Container>
<UpdateStatus />
<UpdateName />
<UpdateEmail />
<UpdatePhone />
<UpdatePassword />
<UpdateLabels />
<UpdatePrefs />
<UpdateMfa />
<DangerZone />
</Container>
@@ -0,0 +1,218 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { AvatarInitials, Copy, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components';
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
import type { Column } from '$lib/helpers/types';
import { Container } from '$lib/layout';
import { View } from '$lib/helpers/load';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { Button } from '$lib/elements/forms';
import { Badge, Icon, Table, Layout, Typography } from '@appwrite.io/pink-svelte';
import { Tag } from '@appwrite.io/pink-svelte';
import { IconDuplicate, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { writable } from 'svelte/store';
import CreateUser from './createUser.svelte';
export let users: { total: number; users: Models.User<{}>[] };
export let limit: number;
export let offset: number;
export const pageNum: number = 0;
export let search: string | null;
const clearSearchHref = `${base}/project-${page.params.region}-${page.params.project}/auth`;
const getUserUrl = (u: Models.User<{}>) =>
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${u.$id}`;
let showCreateUser = false;
async function onUserCreated(e: CustomEvent<Models.User<{}>>) {
await goto(getUserUrl(e.detail));
}
const columns = writable<Column[]>([
{ id: '$id', title: 'User ID', type: 'string', width: 200 },
{ id: 'name', title: 'Name', type: 'string', width: { min: 260 } },
{ id: 'identifiers', title: 'Identifiers', type: 'string', width: { min: 260 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 140 } },
{ id: 'labels', title: 'Labels', type: 'string', hide: true, width: { min: 140 } },
{ id: 'joined', title: 'Joined', type: 'string', width: { min: 140 } },
{ id: 'lastActivity', title: 'Last activity', type: 'string', hide: true, width: { min: 140 } }
]);
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name, email, phone, or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={View.Table} {columns} hideView />
<Button on:click={() => (showCreateUser = true)} event="create_user" size="s">
<Icon size="s" icon={IconPlus} slot="start" />
<span class="text">Create user</span>
</Button>
</Layout.Stack>
</Layout.Stack>
{#if users.total}
<Table.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title } (id)}
<Table.Header.Cell column={id} {root}>{title}</Table.Header.Cell>
{/each}
</svelte:fragment>
{#each users.users as user}
{@const href = getUserUrl(user)}
{#if href}
<Table.Row.Link href={href} {root}>
{#each $columns as { id } (id)}
<Table.Cell column={id} {root}>
{#if id === '$id'}
<Copy value={user.$id} event="user">
<Tag size="xs" variant="code">
<Icon size="s" icon={IconDuplicate} slot="start" />
{user.$id}
</Tag>
</Copy>
{:else if id === 'name'}
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if user.email || user.phone}
{#if user.name}
<AvatarInitials size="xs" name={user.name} />
<Typography.Text truncate>
{user.name}
</Typography.Text>
{:else}
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true"></span>
</div>
{/if}
{:else}
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true"></span>
</div>
<Typography.Text truncate>
{user.name}
</Typography.Text>
{/if}
</Layout.Stack>
{:else if id === 'identifiers'}
<Typography.Text truncate>
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
</Typography.Text>
{:else if id === 'status'}
{#if user.status}
{@const success = user.emailVerification || user.phoneVerification}
<Badge
size="xs"
variant="secondary"
type={success ? 'success' : undefined}
content={user.emailVerification && user.phoneVerification
? 'Verified'
: user.emailVerification
? 'Verified email'
: user.phoneVerification
? 'Verified phone'
: 'Unverified'} />
{:else}
<Badge size="xs" variant="secondary" type="error" content="blocked" />
{/if}
{:else if id === 'labels'}
<Typography.Text truncate>
{user.labels.join(', ')}
</Typography.Text>
{:else if id === 'joined'}
{toLocaleDateTime(user.registration)}
{:else if id === 'lastActivity'}
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
{:else}
{user[id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{:else}
<Table.Row.Base {root}>
{#each $columns as { id } (id)}
<Table.Cell column={id} {root}>
{#if id === '$id'}
<Copy value={user.$id} event="user">
<Tag size="xs" variant="code">
<Icon size="s" icon={IconDuplicate} slot="start" />
{user.$id}
</Tag>
</Copy>
{:else if id === 'name'}
<Layout.Stack direction="row" alignItems="center" gap="s">
{#if user.email || user.phone}
{#if user.name}
<AvatarInitials size="xs" name={user.name} />
<Typography.Text truncate>
{user.name}
</Typography.Text>
{:else}
<div class="avatar is-size-small">
<span class="icon-minus-sm" aria-hidden="true"></span>
</div>
{/if}
{:else}
<div class="avatar is-size-small">
<span class="icon-anonymous" aria-hidden="true"></span>
</div>
<Typography.Text truncate>
{user.name}
</Typography.Text>
{/if}
</Layout.Stack>
{:else if id === 'identifiers'}
<Typography.Text truncate>
{user.email && user.phone
? [user.email, user.phone].join(',')
: user.email || user.phone}
</Typography.Text>
{:else if id === 'status'}
{#if user.status}
{@const success = user.emailVerification || user.phoneVerification}
<Badge size="xs" variant="secondary" type={success ? "success" : "warning"} content={success ? "verified" : "unverified"} />
{:else}
<Badge size="xs" variant="secondary" type="error" content="blocked" />
{/if}
{:else if id === 'labels'}
<Typography.Text truncate>
{user.labels.join(', ')}
</Typography.Text>
{:else if id === 'joined'}
{toLocaleDateTime(user.registration)}
{:else if id === 'lastActivity'}
{user.accessedAt ? toLocaleDate(user.accessedAt) : 'never'}
{:else}
{user[id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Base>
{/if}
{/each}
</Table.Root>
<PaginationWithLimit name="Users" {limit} {offset} total={users.total} />
{:else if search}
<EmptySearch target="users" hidePagination>
<Button href={clearSearchHref} size="s" secondary>Clear Search</Button>
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/references/cloud/server-nodejs/users"
target="user"
allowCreate={true}
on:click={() => (showCreateUser = true)} />
{/if}
</Container>
<CreateUser bind:showCreate={showCreateUser} on:created={onUserCreated} />
@@ -1,97 +1,18 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, ViewSelector } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import Create from './create.svelte';
import Grid from './grid.svelte';
import { columns } from './store';
import Table from './table.svelte';
import { registerCommands } from '$lib/commandCenter';
import { canWriteDatabases } from '$lib/stores/roles';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import EmptySearch from '$lib/components/emptySearch.svelte';
export let data: PageData;
let showCreate = false;
const project = page.params.project;
async function handleCreate(event: CustomEvent<Models.Database>) {
showCreate = false;
await goto(
`${base}/project-${page.params.region}-${project}/databases/database-${event.detail.$id}`
);
}
$: $registerCommands([
{
label: 'Create database',
callback: () => {
showCreate = true;
},
keys: ['c'],
disabled: showCreate || !$canWriteDatabases,
icon: IconPlus,
group: 'databases',
rank: 10
}
]);
import View from './view.svelte';
export let data;
import { base } from "\$app/paths";
import { page } from "\$app/state";
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
{columns}
view={data.view}
hideColumns={!data.databases.total}
hideView={!data.databases.total} />
{#if $canWriteDatabases}
<Button event="create_database" on:click={() => (showCreate = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create database
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if data.databases.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate />
{:else}
<Table {data} />
{/if}
<PaginationWithLimit
name="Databases"
limit={data.limit}
offset={data.offset}
total={data.databases.total} />
{:else if data.search}
<EmptySearch target="databases" hidePagination>
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/databases`}
size="s"
secondary>Clear Search</Button>
</EmptySearch>
{:else}
<Empty
single
href="https://appwrite.io/docs/products/databases/databases"
target="database"
allowCreate={$canWriteDatabases}
on:click={() => (showCreate = true)} />
{/if}
</Container>
<Create bind:showCreate on:created={handleCreate} />
<View
databases={data.databases}
tables={data.tables}
policies={data.policies}
lastBackups={data.lastBackups}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
clearSearchHref={`${base}/project-${page.params.region}-${page.params.project}/databases`}
/>
@@ -1,94 +1,6 @@
<script lang="ts">
import { EmptySearch, PaginationWithLimit, SearchQuery, ViewSelector } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { showCreate, tableViewColumns } from './store';
import Table from './table.svelte';
import Grid from './grid.svelte';
import type { PageData } from './$types';
import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte';
import { base } from '$app/paths';
import { app } from '$lib/stores/app';
import { canWriteTables } from '$lib/stores/roles';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
export let data: PageData;
const databaseId = page.params.database;
import View from './view.svelte';
export let data;
</script>
<Container paddingInlineEnd={false}>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
view={data.view}
columns={tableViewColumns}
hideColumns={!data.tables.total}
hideView={!data.tables.total} />
{#if $canWriteTables}
<Button event="create_table" on:click={() => ($showCreate = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create table
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if data.tables.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate={$showCreate} />
{:else}
<Table {data} />
{/if}
<PaginationWithLimit
name="Tables"
limit={data.limit}
offset={data.offset}
total={data.tables.total} />
{:else if data.search}
<EmptySearch target="tables" hidePagination>
<Button
size="s"
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}`}
>Clear Search</Button>
</EmptySearch>
{:else}
<Card.Base padding="none">
<Empty
title="Create your first table"
src={$app.themeInUse === 'dark'
? `${base}/images/empty-database-dark.svg`
: `${base}/images/empty-database-light.svg`}>
<span slot="description">
Create, organize, and query structured data with Tables.
</span>
<span slot="actions">
<Button
external
href="https://appwrite.io/docs/products/databases/databases"
text
event="empty_documentation"
ariaLabel={`create table`}>Documentation</Button>
{#if $canWriteTables}
<Button
secondary
on:click={() => {
$showCreate = true;
}}>
Create table
</Button>
{/if}
</span>
</Empty>
</Card.Base>
{/if}
</Container>
<View tables={data.tables} limit={data.limit} offset={data.offset} view={data.view} search={data.search} />
@@ -0,0 +1,95 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { EmptySearch, PaginationWithLimit, SearchQuery, ViewSelector, Id, CardContainer, GridItem1 } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { showCreate, tableViewColumns } from './store';
import { Card, Empty, Icon, Layout, Table as PinkTable } from '@appwrite.io/pink-svelte';
import { canWriteTables } from '$lib/stores/roles';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
export let tables: { total: number; tables: Models.Table[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
const databaseId = page.params.database;
</script>
<Container paddingInlineEnd={false}>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector view={view} columns={tableViewColumns} hideColumns={!tables.total} hideView={!tables.total} />
{#if $canWriteTables}
<Button event="create_table" on:click={() => ($showCreate = true)}>
<Icon icon={IconPlus} slot="start" size="s" />
Create table
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if tables.total}
{#if view === 'grid'}
<CardContainer disableEmpty={!$canWriteTables} total={tables.total} {offset} event="table" service="tables" on:click={() => ($showCreate = true)}>
{#each tables.tables as table}
<GridItem1 href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`}>
<svelte:fragment slot="title">{table.name}</svelte:fragment>
<Id value={table.$id}>{table.$id}</Id>
</GridItem1>
{/each}
</CardContainer>
{:else}
<PinkTable.Root columns={$tableViewColumns} let:root>
<svelte:fragment slot="header" let:root>
{#each $tableViewColumns as { id, title }}
<PinkTable.Header.Cell column={id} {root}>{title}</PinkTable.Header.Cell>
{/each}
</svelte:fragment>
{#each tables.tables as table (table.$id)}
<PinkTable.Row.Link {root} href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/table-${table.$id}`}>
{#each $tableViewColumns as column}
<PinkTable.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key $tableViewColumns}
<Id value={table.$id}>{table.$id}</Id>
{/key}
{:else if column.id === 'name'}
{table.name}
{:else}
{table[column.id]}
{/if}
</PinkTable.Cell>
{/each}
</PinkTable.Row.Link>
{/each}
</PinkTable.Root>
{/if}
<PaginationWithLimit name="Tables" {limit} {offset} total={tables.total} />
{:else if search}
<EmptySearch target="tables" hidePagination>
<Button size="s" secondary href={`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}`}>Clear Search</Button>
</EmptySearch>
{:else}
<Card.Base padding="none">
<Empty title="Create your first table" src={$app.themeInUse === 'dark' ? `${base}/images/empty-database-dark.svg` : `${base}/images/empty-database-light.svg`}>
<span slot="description">Create, organize, and query structured data with Tables.</span>
<span slot="actions">
<Button external href="https://appwrite.io/docs/products/databases/databases" text event="empty_documentation" ariaLabel={`create table`}>Documentation</Button>
{#if $canWriteTables}
<Button secondary on:click={() => { $showCreate = true; }}>Create table</Button>
{/if}
</span>
</Empty>
</Card.Base>
{/if}
</Container>
@@ -0,0 +1,143 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, ViewSelector, Id, CardContainer, GridItem1 } from '$lib/components';
import { canWriteDatabases } from '$lib/stores/roles';
import { columns } from './store';
import { Icon, Layout, Table as PinkTable, Badge, Tooltip, Button } from '@appwrite.io/pink-svelte';
import Create from './create.svelte';
import { goto } from '$app/navigation';
import { IconPlus, IconExclamation } from '@appwrite.io/pink-icons-svelte';
import type { Models } from '@appwrite.io/console';
import EmptySearch from '$lib/components/emptySearch.svelte';
export let databases: { total: number; databases: Models.Database[] };
export let tables: Record<string, string>;
export let policies: Record<string, any>;
export let lastBackups: Record<string, string>;
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
export let clearSearchHref: string | null = null;
export let getDatabaseUrl: (db: Models.Database, firstTableId?: string | null) => string | null = (db, t) => db ? dbHref(db.$id, t ?? null) : null;
let showCreate = false;
async function onCreated(e: CustomEvent<Models.Database>) {
showCreate = false;
const href = getDatabaseUrl(e.detail, null);
if (href) await goto(href);
}
function dbHref(id: string, tableId?: string | null) {
const t = tableId ? `/table-${tableId}` : '';
return `${base}/project-${page.params.region}-${page.params.project}/databases/database-${id}${t}`;
}
function getPolicyDescription(cron: string): string {
const [minute, hour, dayOfMonth, , dayOfWeek] = cron.split(' ');
if (dayOfMonth !== '*') return 'Monthly';
if (dayOfWeek !== '*') return 'Weekly on Mondays';
if (minute !== '*' && hour === '*') return 'Hourly';
if (hour !== '*') return 'Daily';
return '';
}
</script>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector {columns} {view} hideColumns={!databases.total} hideView={!databases.total} />
{#if $canWriteDatabases}
<Button event="create_database">
<Icon icon={IconPlus} slot="start" size="s" />
Create database
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if databases.total}
{#if view === 'grid'}
<CardContainer disableEmpty={!$canWriteDatabases} total={databases.total} {offset} event="database" service="databases" on:click={() => (showCreate = true)}>
{#each databases.databases as database}
{@const href = getDatabaseUrl(database, tables?.[database.$id] ?? null)}
<GridItem1 href={href}>
<svelte:fragment slot="title">{database.name}</svelte:fragment>
<svelte:fragment slot="subtitle">
{#if lastBackups && lastBackups[database.$id]}
Last backup: {lastBackups[database.$id]}
{:else if !policies || !policies[database.$id]}
<span class="icon-exclamation"></span> No backup policies
{:else}
Last backup: No backups yet
{/if}
</svelte:fragment>
<Id value={database.$id}>{database.$id}</Id>
</GridItem1>
{/each}
<svelte:fragment slot="empty"><p>Create a database</p></svelte:fragment>
</CardContainer>
{:else}
<PinkTable.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title }}
<PinkTable.Header.Cell column={id} {root}>{title}</PinkTable.Header.Cell>
{/each}
</svelte:fragment>
{#each databases.databases as database (database.$id)}
{@const tableId = tables?.[database.$id] ?? null}
{@const href = getDatabaseUrl(database, tableId)}
<PinkTable.Row.Link {root} {href}>
{#each $columns as column}
<PinkTable.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key $columns}
<Id value={database.$id}>{database.$id}</Id>
{/key}
{:else if column.id === 'name'}
{database.name}
{:else if column.id === 'backup'}
{@const p = policies?.[database.$id] ?? null}
{@const last = lastBackups?.[database.$id] ?? null}
{@const description = p?.map((policy) => getPolicyDescription(policy.schedule)).join(', ')}
<Tooltip placement="bottom" disabled={!p || !last} maxWidth="fit-content">
<span class="u-trim">
{#if !p}
<Layout.Stack direction="row" gap="xxs" alignItems="center">
<Icon icon={IconExclamation} size="s" color="--bgcolor-warning" />
No backup policies
</Layout.Stack>
{:else}
{description}
{/if}
</span>
<span slot="tooltip">Last backup: {last}</span>
</Tooltip>
{:else}
{toLocaleDateTime(database[column.id])}
{/if}
</PinkTable.Cell>
{/each}
</PinkTable.Row.Link>
{/each}
</PinkTable.Root>
{/if}
<PaginationWithLimit name="Databases" {limit} {offset} total={databases.total} />
{:else if search}
<EmptySearch target="databases" hidePagination>
<Button href={clearSearchHref ?? `${base}/project-${page.params.region}-${page.params.project}/databases`} size="s" secondary>Clear Search</Button>
</EmptySearch>
{:else}
<Empty single href="https://appwrite.io/docs/products/databases/databases" target="database" allowCreate={$canWriteDatabases} />
{/if}
<Create bind:showCreate on:created={onCreated} />
<style>
.icon-exclamation { color: hsl(var(--color-warning-100)) !important; }
</style>
@@ -1,244 +1,6 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
import MessageStatusPill from './messageStatusPill.svelte';
import ProviderType from './providerType.svelte';
import { showCreate } from './store';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
import { canWriteMessages } from '$lib/stores/roles';
import {
Badge,
FloatingActionBar,
Layout,
Link,
Table,
Typography
} from '@appwrite.io/pink-svelte';
import { Confirm } from '$lib/components';
import { onDestroy, onMount } from 'svelte';
import { stopPolling, pollMessagesStatus } from './helper';
import View from './view.svelte';
export let data;
let selected: string[] = [];
let showDelete = false;
let deleting = false;
let showFailed = false;
let errors: string[] = [];
const columns = writable<Column[]>([
{ id: '$id', title: 'Message ID', type: 'string', width: 200 },
{
id: 'message',
title: 'Message',
type: 'string',
hide: true,
filter: false,
width: { min: 140 }
},
{ id: 'providerType', title: 'Type', type: 'string', width: { min: 100 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 120 } },
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', width: { min: 120 } },
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', width: { min: 120 } }
]);
const region = page.params.region;
const project = page.params.project;
async function handleDelete() {
showDelete = false;
const promises = selected.map((id) =>
sdk.forProject(region, project).messaging.delete({ messageId: id })
);
try {
await Promise.all(promises);
trackEvent(Submit.MessagingMessageDelete, {
total: selected.length
});
addNotification({
type: 'success',
message: `${selected.length} message${selected.length > 1 ? 's' : ''} deleted`
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingMessageDelete);
} finally {
invalidate(Dependencies.MESSAGING_MESSAGES);
selected = [];
showDelete = false;
}
}
onMount(() => {
const processingMessages = data.messages.messages.filter(
(message) => message.status === 'processing'
);
pollMessagesStatus(processingMessages);
});
onDestroy(stopPolling);
</script>
<Container>
<ResponsiveContainerHeader
{columns}
bind:view={data.view}
hideView
hasFilters
hasSearch
analyticsSource="messaging_messages"
searchPlaceholder="Search by description, type, status, or ID">
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</ResponsiveContainerHeader>
{#if data.messages.total}
<Table.Root
columns={$columns}
allowSelection={$canWriteMessages}
let:root
bind:selectedRows={selected}>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title }}
<Table.Cell column={id} {root}>{title}</Table.Cell>
{/each}
</svelte:fragment>
{#each data.messages.messages as message (message.$id)}
<Table.Row.Link
{root}
id={message.$id}
href={`${base}/project-${region}-${project}/messaging/message-${message.$id}`}>
{#each $columns as column (column.id)}
<Table.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key $columns}
<Id value={message.$id}>{message.$id}</Id>
{/key}
{:else if column.id === 'message'}
{#if message.providerType === MessagingProviderType.Push}
{message.data.title}
{:else if message.providerType === MessagingProviderType.Sms}
{message.data.content}
{:else if message.providerType === MessagingProviderType.Email}
{message.data.subject}
{:else}
Invalid provider
{/if}
{:else if column.id === 'providerType'}
<ProviderType type={message.providerType} size="xs" />
{:else if column.id === 'status'}
<Layout.Stack direction="row" gap="s">
<MessageStatusPill status={message.status} />
{#if message.status === 'failed'}
<Link.Button
on:click={(e) => {
e.preventDefault();
errors = message.deliveryErrors;
showFailed = true;
}}>Details</Link.Button>
{/if}
</Layout.Stack>
{:else if column.type === 'datetime'}
{#if !message[column.id]}
-
{:else}
{toLocaleDateTime(message[column.id])}
{/if}
{:else}
{message[column.id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{/each}
</Table.Root>
{#if selected.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selected.length.toString()} />
<span>
{selected.length > 1 ? 'messages' : 'message'}
selected
</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selected = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
<PaginationWithLimit
name="Messages"
limit={data.limit}
offset={data.offset}
total={data.messages.total} />
{:else if $hasPageQueries}
<EmptyFilter resource="messages" />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no messages that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>
Documentation
</Button>
<Button secondary href={`${base}/project-${region}-${project}/messaging`}>
Clear search
</Button>
</div>
</EmptySearch>
{:else}
<Empty single target="message" on:click={() => ($showCreate = true)}>
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/messaging/messages"
text
event="empty_documentation"
ariaLabel={`create message`}>
Documentation
</Button>
{#if $canWriteMessages}
<CreateMessageDropdown let:toggle>
<Button secondary on:click={toggle} event="create_message">
<span class="text">Create message</span>
</Button>
</CreateMessageDropdown>
{/if}
</svelte:fragment>
</Empty>
{/if}
</Container>
<FailedModal bind:show={showFailed} {errors} />
<Confirm title="Delete messages" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
<Typography.Text>
Are you sure you want to delete <b>{selected.length}</b>
{selected.length > 1 ? 'messages' : 'message'}?
</Typography.Text>
</Confirm>
<View messages={data.messages} limit={data.limit} offset={data.offset} view={data.view} search={data.search} query={data.query} pageNum={data.page} />
@@ -0,0 +1,179 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, EmptyFilter, EmptySearch, Id, PaginationWithLimit } from '$lib/components';
import { hasPageQueries } from '$lib/components/filters';
import { Button } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
import MessageStatusPill from './messageStatusPill.svelte';
import ProviderType from './providerType.svelte';
import { showCreate } from './store';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
import { canWriteMessages } from '$lib/stores/roles';
import { Badge, FloatingActionBar, Layout, Link, Table, Typography } from '@appwrite.io/pink-svelte';
import { Confirm } from '$lib/components';
import { onDestroy, onMount } from 'svelte';
import { stopPolling, pollMessagesStatus } from './helper';
export let messages: { total: number; messages: any[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
export let query: string | null;
export let pageNum: number;
export let clearSearchHref: string | null = null;
export let getMessageUrl: (m: any) => string = (m) => `${base}/project-${region}-${project}/messaging/message-${m.$id}`;
let selected: string[] = [];
let showDelete = false;
let deleting = false;
let showFailed = false;
let errors: string[] = [];
const columns = writable<Column[]>([
{ id: '$id', title: 'Message ID', type: 'string', width: 200 },
{ id: 'message', title: 'Message', type: 'string', hide: true, filter: false, width: { min: 140 } },
{ id: 'providerType', title: 'Type', type: 'string', width: { min: 100 } },
{ id: 'status', title: 'Status', type: 'string', width: { min: 120 } },
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', width: { min: 120 } },
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', width: { min: 120 } }
]);
const region = page.params.region;
const project = page.params.project;
async function handleDelete() {
showDelete = false;
const promises = selected.map((id) => sdk.forProject(region, project).messaging.delete({ messageId: id }));
try {
await Promise.all(promises);
trackEvent(Submit.MessagingMessageDelete, { total: selected.length });
addNotification({ type: 'success', message: `${selected.length} message${selected.length > 1 ? 's' : ''} deleted` });
} catch (error) {
addNotification({ type: 'error', message: error.message });
trackError(error, Submit.MessagingMessageDelete);
} finally {
invalidate(Dependencies.MESSAGING_MESSAGES);
selected = [];
showDelete = false;
}
}
onMount(() => {
const processingMessages = messages.messages.filter((m) => m.status === 'processing');
pollMessagesStatus(processingMessages);
});
onDestroy(stopPolling);
</script>
<Container>
<ResponsiveContainerHeader
{columns}
bind:view
hideView
hasFilters
hasSearch
analyticsSource="messaging_messages"
searchPlaceholder="Search by description, type, status, or ID">
{#if $canWriteMessages}
<CreateMessageDropdown />
{/if}
</ResponsiveContainerHeader>
{#if messages.total}
<Table.Root columns={$columns} allowSelection={$canWriteMessages} let:root bind:selectedRows={selected}>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title }}
<Table.Cell column={id} {root}>{title}</Table.Cell>
{/each}
</svelte:fragment>
{#each messages.messages as message (message.$id)}
<Table.Row.Link {root} id={message.$id} href={getMessageUrl(message)}>
{#each $columns as column (column.id)}
<Table.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key $columns}<Id value={message.$id}>{message.$id}</Id>{/key}
{:else if column.id === 'message'}
{#if message.providerType === MessagingProviderType.Push}
{message.data.title}
{:else if message.providerType === MessagingProviderType.Sms}
{message.data.content}
{:else if message.providerType === MessagingProviderType.Email}
{message.data.subject}
{:else}Invalid provider{/if}
{:else if column.id === 'providerType'}
<ProviderType type={message.providerType} size="xs" />
{:else if column.id === 'status'}
<Layout.Stack direction="row" gap="s">
<MessageStatusPill status={message.status} />
{#if message.status === 'failed'}
<Link.Button on:click={(e) => { e.preventDefault(); errors = message.deliveryErrors; showFailed = true; }}>Details</Link.Button>
{/if}
</Layout.Stack>
{:else if column.type === 'datetime'}
{#if !message[column.id]}-{:else}{toLocaleDateTime(message[column.id])}{/if}
{:else}
{message[column.id]}
{/if}
</Table.Cell>
{/each}
</Table.Row.Link>
{/each}
</Table.Root>
{#if selected.length > 0}
<FloatingActionBar>
<svelte:fragment slot="start">
<Badge content={selected.length.toString()} />
<span>{selected.length > 1 ? 'messages' : 'message'} selected</span>
</svelte:fragment>
<svelte:fragment slot="end">
<Button text on:click={() => (selected = [])}>Cancel</Button>
<Button secondary on:click={() => (showDelete = true)}>Delete</Button>
</svelte:fragment>
</FloatingActionBar>
{/if}
<PaginationWithLimit name="Messages" {limit} {offset} total={messages.total} />
{:else if $hasPageQueries}
<EmptyFilter resource="messages" />
{:else if search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{search}'</b>
<p>There are no messages that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>Documentation</Button>
<Button secondary href={clearSearchHref ?? `${base}/project-${region}-${project}/messaging`}>Clear search</Button>
</div>
</EmptySearch>
{:else}
<Empty single target="message" on:click={() => ($showCreate = true)}>
<svelte:fragment slot="actions">
<Button external href="https://appwrite.io/docs/products/messaging/messages" text event="empty_documentation" ariaLabel={`create message`}>Documentation</Button>
{#if $canWriteMessages}
<CreateMessageDropdown let:toggle>
<Button secondary on:click={toggle} event="create_message"><span class="text">Create message</span></Button>
</CreateMessageDropdown>
{/if}
</svelte:fragment>
</Empty>
{/if}
</Container>
<FailedModal bind:show={showFailed} {errors} />
<Confirm title="Delete messages" bind:open={showDelete} onSubmit={handleDelete} disabled={deleting}>
<Typography.Text>Are you sure you want to delete <b>{selected.length}</b> {selected.length > 1 ? 'messages' : 'message'}?</Typography.Text>
</Confirm>
@@ -3,73 +3,15 @@
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { Empty, PaginationWithLimit, SearchQuery } from '$lib/components';
import Create from './create.svelte';
import { Container } from '$lib/layout';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import View from './view.svelte';
import { writable } from 'svelte/store';
import { canWriteBuckets } from '$lib/stores/roles';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { columns } from './store';
import Grid from './grid.svelte';
import Table from './table.svelte';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let data;
const project = page.params.project;
async function bucketCreated(event: CustomEvent<Models.Bucket>) {
showCreateBucket.set(false);
await goto(
`${base}/project-${page.params.region}-${project}/storage/bucket-${event.detail.$id}`
);
}
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector
{columns}
view={data.view}
hideColumns={!data.buckets.total}
hideView={!data.buckets.total} />
{#if $canWriteBuckets}
<Button on:click={() => ($showCreateBucket = true)} event="create_bucket" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create bucket
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if data.buckets.total}
{#if data.view === 'grid'}
<Grid {data} bind:showCreate={$showCreateBucket} />
{:else}
<Table {data} />
{/if}
<PaginationWithLimit
name="Databases"
limit={data.limit}
offset={data.offset}
total={data.buckets.total} />
{:else}
<Empty
single
href="https://appwrite.io/docs/products/storage"
target="bucket"
on:click={() => showCreateBucket.set(true)} />
{/if}
</Container>
<Create bind:showCreate={$showCreateBucket} on:created={bucketCreated} />
<View
buckets={data.buckets}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
/>
@@ -1,249 +1,6 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Avatar, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Badge } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
import { uploader } from '$lib/stores/uploader';
import { sdk } from '$lib/stores/sdk.js';
import DeleteFile from './deleteFile.svelte';
import { Layout, Table, Icon, Popover, ActionMenu } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import {
IconDotsHorizontal,
IconPencil,
IconPlus,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import View from './view.svelte';
export let data;
let showDelete = false;
let selectedFile: Models.File = null;
const bucketId = page.params.bucket;
function getPreview(fileId: string) {
return (
sdk
.forProject(page.params.region, page.params.project)
.storage.getFilePreview({
bucketId,
fileId,
height: 128,
width: 128
})
.toString() + '&mode=admin'
);
}
async function fileDeleted(event: CustomEvent<Models.File>) {
showDelete = false;
uploader.removeFile(event.detail);
await invalidate(Dependencies.FILES);
}
async function deleteFile(file: Models.File) {
try {
await sdk
.forProject(page.params.region, page.params.project)
.storage.deleteFile({ bucketId: file.bucketId, fileId: file.$id });
await invalidate(Dependencies.FILES);
await uploader.removeFile(file);
trackEvent(Submit.FileDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.FileDelete);
}
}
let isUploading = false;
const beforeunload = (event: BeforeUnloadEvent) => {
// legacy browser **may** support showing a custom message.
const message = 'An upload is in progress. Are you sure you want to leave?';
if (isUploading) {
event.preventDefault();
event.returnValue = message;
return message;
}
};
onMount(() => {
return uploader.subscribe(() => {
isUploading = $uploader.files.some(
(file) =>
file.status !== 'success' && file.progress < 100 && file.status !== 'failed'
);
});
});
</script>
<svelte:window on:beforeunload={beforeunload} />
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search files" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`}
event="create_file"
size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create file
</Button>
</Layout.Stack>
</Layout.Stack>
{#if data.files.total}
<Table.Root
let:root
columns={[
{ id: 'filename' },
{ id: 'type', width: { min: 140 } },
{ id: 'size', width: { min: 100 } },
{ id: 'created', width: { min: 120 } },
{ id: 'actions', width: 40 }
]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="filename" {root}>Filename</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="size" {root}>Size</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each data.files.files as file}
{#if file.chunksTotal / file.chunksUploaded !== 1}
<Table.Row.Base {root}>
<Table.Cell column="filename" {root}>
<Layout.Stack direction="row" alignItems="center">
<span class="avatar is-size-small is-color-empty"></span>
<span class="text u-trim">{file.name}</span>
<div>
<Badge variant="secondary" type="warning" content="Pending" />
</div>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="created" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
<Table.Cell column="actions" {root}>
<div class="u-flex u-main-center">
<button
class="button is-only-icon is-text"
aria-label="Delete item"
on:click|preventDefault={() => deleteFile(file)}>
<span class="icon-trash" aria-hidden="true"></span>
</button>
</div>
</Table.Cell>
</Table.Row.Base>
{:else}
{@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/file-${file.$id}`}
<Table.Row.Link {href} {root}>
<Table.Cell column="filename" {root}>
<div class="u-flex u-gap-12 u-cross-center">
<Avatar size="xs" src={getPreview(file.$id)} alt={file.name} />
<span class="text u-trim">{file.name}</span>
</div>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>
{calculateSize(file.sizeOriginal)}
</Table.Cell>
<Table.Cell column="created" {root}>
<DualTimeView time={file.$createdAt} />
</Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button
text
icon
ariaLabel="more options"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
toggle();
}}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Anchor {href} leadingIcon={IconPencil}>
Update
</ActionMenu.Item.Anchor>
<ActionMenu.Item.Button
leadingIcon={IconTrash}
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedFile = file;
showDelete = true;
}}>
Delete
</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</Table.Cell>
</Table.Row.Link>
{/if}
{/each}
</Table.Root>
<PaginationWithLimit
name="Files"
limit={data.limit}
offset={data.offset}
total={data.files.total} />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no files that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<Button
external
href="https://appwrite.io/docs/products/storage/upload-download"
text>
Documentation
</Button>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}`}>
Clear Search
</Button>
</div>
</EmptySearch>
{:else}
<Empty
single
target="file"
href="https://appwrite.io/docs/products/storage/upload-download"
on:click={() =>
goto(
`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`
)} />
{/if}
</Container>
{#if selectedFile}
<DeleteFile file={selectedFile} bind:showDelete on:deleted={fileDeleted} />
{/if}
<View files={data.files} limit={data.limit} offset={data.offset} search={data.search} />
@@ -0,0 +1,168 @@
<script lang="ts">
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Avatar, Empty, EmptySearch, PaginationWithLimit, SearchQuery } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Badge } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { calculateSize } from '$lib/helpers/sizeConvertion';
import { Container } from '$lib/layout';
import type { Models } from '@appwrite.io/console';
import { addNotification } from '$lib/stores/notifications';
import { uploader } from '$lib/stores/uploader';
import { sdk } from '$lib/stores/sdk.js';
import DeleteFile from './deleteFile.svelte';
import { Layout, Table, Icon, Popover, ActionMenu } from '@appwrite.io/pink-svelte';
import { onMount } from 'svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { IconDotsHorizontal, IconPencil, IconPlus, IconTrash } from '@appwrite.io/pink-icons-svelte';
export let files: { total: number; files: Models.File[] };
export let limit: number;
export let offset: number;
export let search: string | null;
let showDelete = false;
let selectedFile: Models.File = null;
const bucketId = page.params.bucket;
function getPreview(fileId: string) {
return (
sdk
.forProject(page.params.region, page.params.project)
.storage.getFilePreview({ bucketId, fileId, height: 128, width: 128 })
.toString() + '&mode=admin'
);
}
async function fileDeleted(event: CustomEvent<Models.File>) {
showDelete = false;
uploader.removeFile(event.detail);
await invalidate(Dependencies.FILES);
}
async function deleteFile(file: Models.File) {
try {
await sdk.forProject(page.params.region, page.params.project).storage.deleteFile({ bucketId: file.bucketId, fileId: file.$id });
await invalidate(Dependencies.FILES);
await uploader.removeFile(file);
trackEvent(Submit.FileDelete);
} catch (error) {
addNotification({ type: 'error', message: error.message });
trackError(error, Submit.FileDelete);
}
}
let isUploading = false;
const beforeunload = (event: BeforeUnloadEvent) => {
const message = 'An upload is in progress. Are you sure you want to leave?';
if (isUploading) {
event.preventDefault();
event.returnValue = message;
return message;
}
};
onMount(() => {
return uploader.subscribe(() => {
isUploading = $uploader.files.some((file) => file.status !== 'success' && file.progress < 100 && file.status !== 'failed');
});
});
</script>
<svelte:window on:beforeunload={beforeunload} />
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search files" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Button href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`} event="create_file" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create file
</Button>
</Layout.Stack>
</Layout.Stack>
{#if files.total}
<Table.Root let:root columns={[{ id: 'filename' }, { id: 'type', width: { min: 140 } }, { id: 'size', width: { min: 100 } }, { id: 'created', width: { min: 120 } }, { id: 'actions', width: 40 }]}>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="filename" {root}>Filename</Table.Header.Cell>
<Table.Header.Cell column="type" {root}>Type</Table.Header.Cell>
<Table.Header.Cell column="size" {root}>Size</Table.Header.Cell>
<Table.Header.Cell column="created" {root}>Created</Table.Header.Cell>
<Table.Header.Cell column="actions" {root} />
</svelte:fragment>
{#each files.files as file (file.$id)}
{#if $uploader.files.find(({ $id }) => $id === file.$id)}
<Table.Row.Base {root}>
<Table.Cell column="filename" {root}>
<Layout.Stack direction="row" alignItems="center">
<span class="avatar is-size-small is-color-empty"></span>
<span class="text u-trim">{file.name}</span>
<div><Badge variant="secondary" type="warning" content="Pending" /></div>
</Layout.Stack>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>{calculateSize(file.sizeOriginal)}</Table.Cell>
<Table.Cell column="created" {root}><DualTimeView time={file.$createdAt} /></Table.Cell>
<Table.Cell column="actions" {root}>
<div class="u-flex u-main-center">
<button class="button is-only-icon is-text" aria-label="Delete item" on:click|preventDefault={() => deleteFile(file)}>
<span class="icon-trash" aria-hidden="true"></span>
</button>
</div>
</Table.Cell>
</Table.Row.Base>
{:else}
{@const href = `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${bucketId}/file-${file.$id}`}
<Table.Row.Link {href} {root}>
<Table.Cell column="filename" {root}>
<div class="u-flex u-gap-12 u-cross-center">
<Avatar size="xs" src={getPreview(file.$id)} alt={file.name} />
<span class="text u-trim">{file.name}</span>
</div>
</Table.Cell>
<Table.Cell column="type" {root}>{file.mimeType}</Table.Cell>
<Table.Cell column="size" {root}>{calculateSize(file.sizeOriginal)}</Table.Cell>
<Table.Cell column="created" {root}><DualTimeView time={file.$createdAt} /></Table.Cell>
<Table.Cell column="actions" {root}>
<Popover let:toggle placement="bottom-start" padding="none">
<Button text icon ariaLabel="more options" on:click={(e) => { e.stopPropagation(); e.preventDefault(); toggle(); }}>
<Icon icon={IconDotsHorizontal} size="s" />
</Button>
<ActionMenu.Root slot="tooltip">
<ActionMenu.Item.Anchor {href} leadingIcon={IconPencil}>Update</ActionMenu.Item.Anchor>
<ActionMenu.Item.Button leadingIcon={IconTrash} on:click={(e) => { e.stopPropagation(); e.preventDefault(); selectedFile = file; showDelete = true; }}>Delete</ActionMenu.Item.Button>
</ActionMenu.Root>
</Popover>
</Table.Cell>
</Table.Row.Link>
{/if}
{/each}
</Table.Root>
<PaginationWithLimit name="Files" {limit} {offset} total={files.total} />
{:else if search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{search}'</b>
<p>There are no files that match your search.</p>
</div>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/storage/upload-download" text>Documentation</Button>
<Button secondary href={`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}`}>Clear Search</Button>
</div>
</EmptySearch>
{:else}
<Empty single target="file" href="https://appwrite.io/docs/products/storage/upload-download" on:click={() => goto(`${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${page.params.bucket}/create`)} />
{/if}
</Container>
{#if selectedFile}
<DeleteFile file={selectedFile} bind:showDelete on:deleted={fileDeleted} />
{/if}
@@ -0,0 +1,104 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { Empty, PaginationWithLimit, SearchQuery, Id, CardContainer, GridItem1 } from '$lib/components';
import { canWriteBuckets } from '$lib/stores/roles';
import { toLocaleDateTime } from '$lib/helpers/date';
import { columns } from './store';
import ViewSelector from '$lib/components/viewSelector.svelte';
import { Icon, Layout, Table as PinkTable, Badge, Tooltip } from '@appwrite.io/pink-svelte';
import { Button } from '$lib/elements/forms';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import Create from './create.svelte';
import type { Models } from '@appwrite.io/console';
import { showCreateBucket } from './+page.svelte';
export let buckets: { total: number; buckets: Models.Bucket[] };
export let limit: number;
export let offset: number;
export let view: 'grid' | 'table';
export let search: string | null;
function bucketHref(id: string) {
return `${base}/project-${page.params.region}-${page.params.project}/storage/bucket-${id}`;
}
async function bucketCreated(event: CustomEvent<Models.Bucket>) {
showCreateBucket.set(false);
await goto(bucketHref(event.detail.$id));
}
</script>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search by name or ID" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<ViewSelector {columns} {view} hideColumns={!buckets.total} hideView={!buckets.total} />
{#if $canWriteBuckets}
<Button on:click={() => ( $showCreateBucket = true )} event="create_bucket" size="s">
<Icon icon={IconPlus} slot="start" size="s" />
Create bucket
</Button>
{/if}
</Layout.Stack>
</Layout.Stack>
{#if buckets.total}
{#if view === 'grid'}
<CardContainer disableEmpty={!$canWriteBuckets} total={buckets.total} {offset} event="bucket" service="buckets" on:click={() => ($showCreateBucket = true)}>
{#each buckets.buckets as bucket}
<GridItem1 href={bucketHref(bucket.$id)}>
<svelte:fragment slot="title">{bucket.name}</svelte:fragment>
<svelte:fragment slot="status">
{#if !bucket.enabled}
<div>
<Badge size="s" variant="secondary" content="Disabled" />
</div>
{/if}
</svelte:fragment>
<Id value={bucket.$id}>{bucket.$id}</Id>
<svelte:fragment slot="icons">
<Tooltip>
<span class:u-opacity-20={!bucket.encryption} class="icon-lock-closed" aria-hidden="true"></span>
<span slot="tooltip">{bucket.encryption ? 'Encryption enabled' : 'Encryption disabled'}</span>
</Tooltip>
</svelte:fragment>
</GridItem1>
{/each}
<svelte:fragment slot="empty"><p>Create a bucket</p></svelte:fragment>
</CardContainer>
{:else}
<PinkTable.Root columns={$columns} let:root>
<svelte:fragment slot="header" let:root>
{#each $columns as { id, title }}
<PinkTable.Header.Cell column={id} {root}>{title}</PinkTable.Header.Cell>
{/each}
</svelte:fragment>
{#each buckets.buckets as bucket (bucket.$id)}
<PinkTable.Row.Link {root} href={bucketHref(bucket.$id)}>
{#each $columns as column}
<PinkTable.Cell column={column.id} {root}>
{#if column.id === '$id'}
{#key $columns}
<Id value={bucket.$id}>{bucket.$id}</Id>
{/key}
{:else if column.id === 'name'}
{bucket.name}
{:else}
{toLocaleDateTime(bucket[column.id])}
{/if}
</PinkTable.Cell>
{/each}
</PinkTable.Row.Link>
{/each}
</PinkTable.Root>
{/if}
<PaginationWithLimit name="Buckets" {limit} {offset} total={buckets.total} />
{:else}
<Empty single href="https://appwrite.io/docs/products/storage" target="bucket" on:click={() => showCreateBucket.set(true)} />
{/if}
<Create bind:showCreate={$showCreateBucket} on:created={bucketCreated} />
@@ -0,0 +1,9 @@
import type { LayoutLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
export const load: LayoutLoad = async ({ params }) => {
// Minimal layout data for embeds; no UI wrapping.
const project = await sdk.forConsole.projects.get(params.project);
return { project } as const;
};
@@ -0,0 +1,15 @@
<script lang="ts">
import View from '$routes/(console)/project-[region]-[project]/auth/view.svelte';
export let data;
</script>
<View
users={data.users}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
@@ -0,0 +1,26 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route, params }) => {
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
search,
page,
users: await sdk.forProject(params.region, params.project).users.list([
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...(search ? [Query.search('name', search)] : [])
])
};
};
@@ -0,0 +1,25 @@
<script lang="ts">
import { Container } from '$lib/layout';
import UpdatePasswordDictionary from '$routes/(console)/project-[region]-[project]/auth/security/updatePasswordDictionary.svelte';
import UpdatePasswordHistory from '$routes/(console)/project-[region]-[project]/auth/security/updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from '$routes/(console)/project-[region]-[project]/auth/security/updatePersonalDataCheck.svelte';
import UpdateSessionAlerts from '$routes/(console)/project-[region]-[project]/auth/security/updateSessionAlerts.svelte';
import UpdateSessionLength from '$routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte';
import UpdateSessionsLimit from '$routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte';
import UpdateMembershipPrivacy from '$routes/(console)/project-[region]-[project]/auth/security/updateMembershipPrivacy.svelte';
import UpdateUsersLimit from '$routes/(console)/project-[region]-[project]/auth/security/updateUsersLimit.svelte';
import UpdateSessionInvalidation from '$routes/(console)/project-[region]-[project]/auth/security/updateSessionInvalidation.svelte';
</script>
<Container>
<UpdateUsersLimit />
<UpdateSessionLength />
<UpdateSessionsLimit />
<UpdatePasswordHistory />
<UpdatePasswordDictionary />
<UpdatePersonalDataCheck />
<UpdateSessionAlerts />
<UpdateSessionInvalidation />
<UpdateMembershipPrivacy />
</Container>
@@ -0,0 +1,5 @@
<script lang="ts">
import View from '$routes/(console)/project-[region]-[project]/auth/settings/view.svelte';
</script>
<View />
@@ -0,0 +1,12 @@
<script lang="ts">
import View from '$routes/(console)/project-[region]-[project]/auth/teams/view.svelte';
export let data;
</script>
<View
teams={data.teams}
limit={data.limit}
offset={data.offset}
pageNum={data.page}
search={data.search}
/>
@@ -0,0 +1,26 @@
import { Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load';
import { PAGE_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, route, params }) => {
const page = getPage(url);
const search = getSearch(url);
const limit = getLimit(url, route, PAGE_LIMIT);
const offset = pageToOffset(page, limit);
return {
offset,
limit,
search,
page,
teams: await sdk.forProject(params.region, params.project).teams.list([
Query.limit(limit),
Query.offset(offset),
Query.orderDesc(''),
...(search ? [Query.search('name', search)] : [])
])
};
};
@@ -0,0 +1,6 @@
<script lang="ts">
import TeamPage from '$routes/(console)/project-[region]-[project]/auth/teams/team-[team]/+page.svelte';
</script>
<TeamPage />
@@ -0,0 +1,6 @@
<script lang="ts">
import View from '$routes/(console)/project-[region]-[project]/auth/templates/view.svelte';
export let data;
</script>
<View project={data.project} />
@@ -0,0 +1,5 @@
<script lang="ts">
import View from '$routes/(console)/project-[region]-[project]/auth/user-[user]/view.svelte';
</script>
<View />
@@ -0,0 +1,12 @@
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async ({ params }) => {
const [user, userFactors] = await Promise.all([
sdk.forProject(params.region, params.project).users.get(params.user),
sdk.forProject(params.region, params.project).users.listMfaFactors(params.user)
]);
return { user, userFactors };
};
@@ -0,0 +1,21 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/state';
import View from '$routes/(console)/project-[region]-[project]/messaging/view.svelte';
export let data;
const clearSearchHref = `${base}/embed/project-${page.params.region}-${page.params.project}/messaging`;
const getMessageUrl = (m: any) => `${base}/embed/project-${page.params.region}-${page.params.project}/messaging/message-${m.$id}`;
</script>
<View
messages={data.messages}
limit={data.limit}
offset={data.offset}
view={data.view}
search={data.search}
query={data.query}
pageNum={data.page}
{clearSearchHref}
{getMessageUrl}
/>
@@ -0,0 +1,30 @@
import { queries, queryParamToMap } from '$lib/components/filters';
import { CARD_LIMIT, Dependencies } from '$lib/constants';
import { View, getLimit, getPage, getQuery, getSearch, getView, pageToOffset } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import { Query, type Models } from '@appwrite.io/console';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ depends, url, route, params }) => {
depends(Dependencies.MESSAGING_MESSAGES);
const page = getPage(url);
const search = getSearch(url);
const view = getView(url, route, View.Grid);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
const query = getQuery(url);
const parsedQueries = queryParamToMap(query || '[]');
queries.set(parsedQueries);
const messages = (await sdk.forProject(params.region, params.project).messaging.listMessages({
queries: [Query.limit(limit), Query.offset(offset), Query.orderDesc(''), ...parsedQueries.values()],
search: search || undefined
})) as {
total: number;
messages: (Models.Message & { data: Record<string, string> })[];
};
return { offset, limit, search, query, page, view, messages } as const;
};
+11 -3
View File
@@ -14,16 +14,24 @@
import { user } from '$lib/stores/user';
import { loading } from '$routes/store';
import { Root } from '@appwrite.io/pink-svelte';
import { ThemeDark, ThemeLight, ThemeDarkCloud, ThemeLightCloud } from '../themes';
import {
ThemeDark,
ThemeLight,
ThemeDarkCloud,
ThemeLightCloud,
ThemeDarkStudio,
ThemeLightStudio
} from '../themes';
import { isSmallViewport, updateViewport } from '$lib/stores/viewport';
import { feedback } from '$lib/stores/feedback';
function resolveTheme(theme: AppStore['themeInUse']) {
const isEmbedded = page.route.id.includes('/(embeddings)/');
switch (theme) {
case 'dark':
return isCloud ? ThemeDarkCloud : ThemeDark;
return isEmbedded ? ThemeDarkStudio : isCloud ? ThemeDarkCloud : ThemeDark;
case 'light':
return isCloud ? ThemeLightCloud : ThemeLight;
return isEmbedded ? ThemeLightStudio : isCloud ? ThemeLightCloud : ThemeLight;
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"bgcolor-accent": "var(--neutral-50)",
"bgcolor-accent-secondary": "var(--neutral-100)",
"bgcolor-accent-tertiary": "var(--neutral-150)",
"fgcolor-on-accent": "var(--neutral-950)"
}
+9 -4
View File
@@ -1,5 +1,10 @@
export { default as ThemeDark } from './dark.json';
export { default as ThemeLight } from './light.json';
import ThemeDark from './dark.json';
import ThemeLight from './light.json';
import ThemeDarkCloud from './dark-cloud.json';
import ThemeLightCloud from './light-cloud.json';
import darkStudio from './dark-studio.json';
import lightStudio from './light-studio.json';
export { default as ThemeDarkCloud } from './dark-cloud.json';
export { default as ThemeLightCloud } from './light-cloud.json';
export const ThemeDarkStudio = { ...ThemeDarkCloud, ...darkStudio };
export const ThemeLightStudio = { ...ThemeLightCloud, ...lightStudio };
export { ThemeDark, ThemeLight, ThemeDarkCloud, ThemeLightCloud };
+5
View File
@@ -0,0 +1,5 @@
{
"bgcolor-accent": "var(--neutral-1000)",
"bgcolor-accent-secondary": "var(--neutral-900)",
"bgcolor-accent-tertiary": "var(--neutral-850)"
}