Merge branch 'main' into feat-improve-domain-flows

This commit is contained in:
Khushboo Verma
2025-10-15 16:25:33 +05:30
22 changed files with 183 additions and 162 deletions
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
@@ -83,7 +83,7 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
+1 -1
View File
@@ -11,7 +11,7 @@ RUN corepack prepare pnpm@10.0.0 --activate
ADD ./package.json /app/package.json
ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile
ADD ./build.js /app/build.js
ADD ./tsconfig.json /app/tsconfig.json
@@ -106,11 +106,11 @@
{#if hasSearch}
<SearchQuery placeholder={searchPlaceholder} />
{/if}
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
{#if hasFilters && $columns?.length}
<QuickFilters {columns} {analyticsSource} {filterCols} />
{/if}
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
{#if hasDisplaySettings}
<ViewSelector ui="new" {view} {columns} {hideView} {hideColumns} />
{/if}
+19 -8
View File
@@ -21,14 +21,21 @@
</script>
<Layout.Stack>
<header class="form-header" class:hide-divider={!$$slots.subtitle}>
<Typography.Title><slot name="title" /></Typography.Title>
{#if $$slots.subtitle}
<p>
<slot name="subtitle" />
</p>
{/if}
</header>
{#if $$slots.title || $$slots.subtitle}
<header
class="form-header"
class:hide-divider={!$$slots.subtitle}
class:only-subtitle={!$$slots.title && $$slots.subtitle}>
{#if $$slots.title}
<Typography.Title><slot name="title" /></Typography.Title>
{/if}
{#if $$slots.subtitle}
<p>
<slot name="subtitle" />
</p>
{/if}
</header>
{/if}
<slot />
</Layout.Stack>
@@ -42,4 +49,8 @@
padding-block-end: 0;
border-block-end: none;
}
.only-subtitle {
margin-block-end: 0.5rem;
}
</style>
@@ -83,12 +83,13 @@
}
$: projectCreationDisabled =
(isCloud && getServiceLimit('projects') <= data.projects.total) ||
(isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total) ||
(isCloud && $readOnly && !GRACE_PERIOD_OVERRIDE) ||
!$canWriteProjects;
$: reachedProjectLimit = isCloud && getServiceLimit('projects') <= data.projects.total;
$: projectsLimit = getServiceLimit('projects');
$: reachedProjectLimit =
isCloud && getServiceLimit('projects', null, data.currentPlan) <= data.projects.total;
$: projectsLimit = getServiceLimit('projects', null, data.currentPlan);
$: $registerCommands([
{
@@ -29,7 +29,7 @@
let areMembersLimited: boolean = $state(false);
$effect(() => {
const limit = getServiceLimit('members') || Infinity;
const limit = getServiceLimit('members', null, page.data.currentPlan) || Infinity;
const isLimited = limit !== 0 && limit < Infinity;
areMembersLimited =
isCloud &&
@@ -243,11 +243,7 @@
{: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>
<EmptySearch target="messages" search={data.search}>
<div class="u-flex u-gap-16">
<Button external href="https://appwrite.io/docs/products/messaging/messages" text>
Documentation
@@ -21,15 +21,18 @@
const options = [
{ label: 'Now', value: 'now' },
{ label: 'Schedule', value: 'later' }
{ label: 'Custom', value: 'later' }
];
const formatOptions: Intl.DateTimeFormatOptions = {
const dateOptions: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric',
year: 'numeric'
};
const timeOptions: Intl.DateTimeFormatOptions = {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hourCycle: 'h23',
timeZoneName: 'longGeneric'
};
@@ -57,28 +60,23 @@
</script>
<Layout.Stack>
<InputSelect id="when" required {options} bind:value={when} />
<InputDate
id="date"
label="Date"
disabled={when === 'now'}
required={when === 'later'}
min={minDate}
bind:value={date} />
<InputTime
id="time"
label="Time"
disabled={when === 'now'}
required={when === 'later'}
min={minTime}
bind:value={time} />
<InputSelect id="when" label="Schedule" required {options} bind:value={when} />
{#if when === 'later'}
<Layout.Stack direction="row" gap="m">
<InputDate id="date" min={minDate} bind:value={date} required />
<InputTime id="time" min={minTime} bind:value={time} required />
</Layout.Stack>
{/if}
<Helper type="neutral">
{#if when === 'now'}
The message will be sent immediately
{:else if !dateTime || isNaN(dateTime.getTime())}
The message will be sent later
{:else}
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
The message will be sent on {dateTime.toLocaleDateString('en', dateOptions)} at {dateTime.toLocaleTimeString(
'en',
timeOptions
)}
{/if}
</Helper>
</Layout.Stack>
@@ -133,7 +133,7 @@
<Fieldset legend="Targets">
<Targets type={MessagingProviderType.Email} bind:topics bind:targets />
</Fieldset>
<Fieldset legend="Schedule">
<Fieldset legend="Settings">
<Schedule bind:scheduledAt {targets} />
</Fieldset>
</Layout.Stack>
@@ -187,7 +187,7 @@
<Fieldset legend="Targets">
<Targets type={MessagingProviderType.Push} bind:topics bind:targets />
</Fieldset>
<Fieldset legend="Schedule">
<Fieldset legend="Settings">
<Schedule bind:scheduledAt {targets} />
</Fieldset>
</Layout.Stack>
@@ -115,7 +115,7 @@
<Fieldset legend="Targets">
<Targets type={MessagingProviderType.Sms} bind:topics bind:targets />
</Fieldset>
<Fieldset legend="Schedule">
<Fieldset legend="Settings">
<Schedule bind:scheduledAt {targets} />
</Fieldset>
</Layout.Stack>
@@ -7,7 +7,7 @@
import { page } from '$app/state';
</script>
<Popover let:toggle padding="none" placement="bottom-end">
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle}>
<Button on:click={toggle} event="create_message">
<Icon icon={IconPlus} slot="start" size="s" />
@@ -16,6 +16,7 @@
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Table, Typography } from '@appwrite.io/pink-svelte';
import { page } from '$app/state';
import { Link } from '$lib/elements';
export let message: Models.Message;
export let selectedTopicsById: Record<string, Models.Topic>;
@@ -141,17 +142,13 @@
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
{:else}
<Card.Base padding="none">
<PinkEmpty
type="secondary"
title="No topics were selected"
description="No topics were selected for this message. Need a hand? Check out our documentation.">
<svelte:fragment slot="actions">
<Button
secondary
<PinkEmpty type="secondary" title="No topics were selected">
<svelte:fragment slot="description">
Need a hand? Check out our <Link
variant="muted"
href="https://appwrite.io/docs/products/messaging/topics"
external>
Documentation
</Button>
external>documentation</Link
>.
</svelte:fragment>
</PinkEmpty>
</Card.Base>
@@ -1,45 +1,40 @@
<script lang="ts">
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import {
EmptySearch,
SearchQuery,
PaginationWithLimit,
ViewSelector,
EmptyFilter
} from '$lib/components';
import { Container } from '$lib/layout';
import { EmptySearch, PaginationWithLimit, EmptyFilter } from '$lib/components';
import { Container, ResponsiveContainerHeader } from '$lib/layout';
import { columns } from './store';
import { Filters, hasPageQueries } from '$lib/components/filters';
import { hasPageQueries } from '$lib/components/filters';
import CreateProviderDropdown from './createProviderDropdown.svelte';
import Table from './table.svelte';
import { base } from '$app/paths';
import { canWriteProviders } from '$lib/stores/roles';
import { Card, Layout, Empty, Icon } from '@appwrite.io/pink-svelte';
import { Card, Empty, Icon } from '@appwrite.io/pink-svelte';
import { View } from '$lib/helpers/load';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import type { PageData } from './$types';
export let data;
let { data }: { data: PageData } = $props();
</script>
<Container>
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center">
<SearchQuery placeholder="Search providers" />
</Layout.Stack>
<Layout.Stack direction="row" alignItems="center" justifyContent="flex-end">
<Filters query={data.query} {columns} analyticsSource="messaging_providers" />
<ViewSelector ui="new" view={View.Table} {columns} hideView />
{#if $canWriteProviders}
<CreateProviderDropdown let:toggle>
<Button on:click={toggle} event="create_provider">
<Icon icon={IconPlus} slot="start" size="s" />
Create provider
</Button>
</CreateProviderDropdown>
{/if}
</Layout.Stack>
</Layout.Stack>
<ResponsiveContainerHeader
{columns}
view={View.Table}
hideView
hasFilters
hasSearch
analyticsSource="messaging_providers"
searchPlaceholder="Search by name or ID">
{#if $canWriteProviders}
<CreateProviderDropdown let:toggle>
<Button on:click={toggle} event="create_provider">
<Icon icon={IconPlus} slot="start" size="s" />
Create provider
</Button>
</CreateProviderDropdown>
{/if}
</ResponsiveContainerHeader>
{#if data.providers.total}
<Table {data} />
@@ -52,11 +47,7 @@
{:else if $hasPageQueries}
<EmptyFilter resource="providers" />
{:else if data.search && data.search !== 'empty'}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no providers that match your search.</p>
</div>
<EmptySearch target="providers" search={data.search}>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/messaging/providers`}>
@@ -68,7 +59,7 @@
<Empty
title="Create your first provider"
description="Need a hand? Learn more in our documentation.">
<slot name="actions" slot="actions">
<svelte:fragment slot="actions">
<Button
external
href="https://appwrite.io/docs/products/messaging/providers"
@@ -82,7 +73,7 @@
</Button>
</CreateProviderDropdown>
{/if}
</slot>
</svelte:fragment>
</Empty>
</Card.Base>
{/if}
@@ -21,6 +21,8 @@
import { providers } from './store';
import CreateMember from '$routes/(console)/organization-[organization]/createMember.svelte';
let formRef: Form;
async function create() {
try {
let response: Models.Provider;
@@ -185,7 +187,7 @@
</script>
<Wizard title="Create provider" columnSize="l">
<Form onSubmit={create} isModal={false}>
<Form bind:this={formRef} onSubmit={create} isModal={false}>
<Layout.Stack gap="xxl">
<Fieldset legend="Provider">
<Provider />
@@ -193,9 +195,6 @@
<Fieldset legend="Settings">
<Settings />
</Fieldset>
<Layout.Stack justifyContent="flex-end" direction="row">
<Button submit>Create</Button>
</Layout.Stack>
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
@@ -227,7 +226,9 @@
hasDivider
href={`https://appwrite.io/docs/products/messaging/${$provider}`}
title="Read the guide in the docs"
icon={IconBookOpen} />
icon={IconBookOpen}
target="_blank"
rel="noreferrer" />
<ActionList.Item.Button
on:click={() => {
$newMemberModal = true;
@@ -241,4 +242,9 @@
{#if $newMemberModal}
<CreateMember bind:showCreate={$newMemberModal} />
{/if}
<svelte:fragment slot="footer">
<Layout.Stack justifyContent="flex-end" direction="row">
<Button on:click={() => formRef.triggerSubmit()}>Create</Button>
</Layout.Stack>
</svelte:fragment>
</Wizard>
@@ -9,7 +9,7 @@
import { ActionMenu, Popover } from '@appwrite.io/pink-svelte';
</script>
<Popover let:toggle padding="none" placement="bottom-end">
<Popover let:toggle padding="none" placement="bottom-start">
<slot {toggle} />
<ActionMenu.Root slot="tooltip">
{#each Object.entries(providers) as [type, option]}
@@ -1,30 +1,25 @@
<script lang="ts">
import { ImagePreview } from '$lib/components';
import { Layout } from '@appwrite.io/pink-svelte';
export let lines = [];
export let image: { src: { dark: string; light: string }; alt: string } | null = null;
</script>
<div class="u-flex-vertical u-gap-16">
<Layout.Stack direction="column" gap="m" style="max-inline-size: 260px;">
{#each lines as line}
<p>{@html line}</p>
<p class="popover-content">{@html line}</p>
{/each}
{#if image}
<ImagePreview darkSrc={image.src.dark} lightSrc={image.src.light} alt={image.alt} />
<Layout.Stack>
<ImagePreview darkSrc={image.src.dark} lightSrc={image.src.light} alt={image.alt} />
</Layout.Stack>
{/if}
</div>
</Layout.Stack>
<style>
div > p {
color: hsl(var(--color-neutral-50));
&:first-child {
color: hsl(var(--color-neutral-70));
}
}
:global(.theme-dark) div > p {
:global(.theme-dark) .popover-content {
color: hsl(var(--color-neutral-20));
&:first-child {
@@ -97,11 +97,7 @@
{:else if $hasPageQueries}
<EmptyFilter resource="topics" />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no topics that match your search.</p>
</div>
<EmptySearch target="topics" search={data.search}>
<Button
secondary
href={`${base}/project-${page.params.region}-${page.params.project}/messaging/topics`}>
@@ -25,8 +25,9 @@
import { writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
import { base } from '$app/paths';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Link } from '$lib/elements';
export let data;
let showAdd = false;
@@ -71,9 +72,7 @@
await Promise.all(promises);
addNotification({
type: 'success',
message: `Added ${targetIds.length} subscriber${
targetIds.length > 1 ? 's' : ''
} to topic`
message: `${targetIds.length} subscriber${targetIds.length !== 1 ? 's' : ''} have been added`
});
trackEvent(Submit.MessagingTopicSubscriberAdd);
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
@@ -141,13 +140,14 @@
bind:targetsById={$targetsById}
on:update={addTargets}>
<svelte:fragment slot="description">
<p class="text">
Add subscribers to this topic by selecting the targets for directing messages. <a
<Typography.Text>
Add subscribers to this topic by selecting the targets for directing messages.{' '}
<Link
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about subscribers</a
>.
</p>
external>
Learn more
</Link>
.
</Typography.Text>
</svelte:fragment>
</UserTargetsModal>
@@ -4,7 +4,6 @@
import { sdk } from '$lib/stores/sdk';
import { MessagingProviderType, type Models, Query } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { getTotal } from './wizard/store';
import {
Badge,
Card,
@@ -17,21 +16,42 @@
import { getProviderText } from './helper';
import { page } from '$app/state';
export let providerType: MessagingProviderType;
export let show: boolean;
export let topicsById: Record<string, Models.Topic>;
export let title = 'Select topics';
let {
providerType,
show = $bindable(),
topicsById = $bindable(),
title = 'Select topics'
} = $props<{
providerType: MessagingProviderType;
show: boolean;
topicsById: Record<string, Models.Topic>;
title?: string;
}>();
const dispatch = createEventDispatcher();
let search = '';
let offset = 0;
let totalResults = 0;
let topicResultsById: Record<string, Models.Topic> = {}; // use a hash map so we can quickly look up a user by id
let selected: Record<string, Models.Topic> = {};
let hasSelection = false;
let search = $state('');
let offset = $state(0);
let totalResults = $state(0);
let topicResultsById = $state<Record<string, Models.Topic>>({});
let selected = $state<Record<string, Models.Topic>>({});
let emptyTopicsExists = $state(false);
let emptyTopicsExists = false;
let previousSearch = $state('');
let wasOpen = $state(false);
function getTopicTotal(topic: Models.Topic): number {
switch (providerType) {
case MessagingProviderType.Email:
return topic.emailTotal;
case MessagingProviderType.Sms:
return topic.smsTotal;
case MessagingProviderType.Push:
return topic.pushTotal;
default:
return 0;
}
}
function reset() {
offset = 0;
@@ -87,31 +107,42 @@
}
}
$: if (show) {
request();
}
$: if (offset !== null) {
request();
}
$: if (search !== null) {
offset = 0;
request();
}
$effect(() => {
if (search !== previousSearch) {
previousSearch = search;
offset = 0;
}
});
$: selectedSize = Object.keys(selected).length;
$effect(() => {
if (!show) return;
offset ?? null;
search ?? null;
request();
});
$: hasSelection = selectedSize > 0;
$effect(() => {
if (show && !wasOpen) {
selected = topicsById;
}
wasOpen = show;
});
$: if (show) {
selected = topicsById;
}
let selectedSize = $derived(Object.keys(selected).length);
let hasSelection = $derived(selectedSize > 0);
let topicSelectionStates = $derived(
Object.fromEntries(Object.keys(topicResultsById).map((id) => [id, !!selected[id]]))
);
</script>
<Modal {title} bind:show onSubmit={submit} on:close={reset}>
<Typography.Text>
Select existing topics you want to send this message to its targets. The message will be
sent only to {getProviderText(providerType)} targets.
</Typography.Text>
<svelte:fragment slot="description">
<Typography.Text>
Select existing topics you want to send this message to its targets. The message will be
sent only to {getProviderText(providerType)} targets.
</Typography.Text>
</svelte:fragment>
<Layout.Stack>
<InputSearch
autofocus
@@ -128,11 +159,11 @@
id={topicId}
label={topic.name}
disabled={!!topicsById[topicId]}
checked={!!selected[topicId]}
checked={topicSelectionStates[topicId] || false}
on:change={(event) => onTopicSelection(event, topic)}>
</Selector.Checkbox>
<span>
({getTotal(topic)} targets)
({getTopicTotal(topic)} targets)
</span>
</Layout.Stack>
</Table.Cell>
@@ -174,7 +205,7 @@
<Badge variant="secondary" content={selectedSize.toString()} />
<span>Topics selected</span>
</Layout.Stack>
<Button submit disabled={!hasSelection}>Add</Button>
<Button submit disabled={!hasSelection}>Create</Button>
</Layout.Stack>
</svelte:fragment>
</Modal>
@@ -216,7 +216,7 @@
<Badge variant="secondary" content={selectedUsers.toString()} />
<span>Users selected</span>
</Layout.Stack>
<Button submit disabled={!hasSelection}>Add</Button>
<Button submit disabled={!hasSelection}>Create</Button>
</Layout.Stack>
</svelte:fragment>
</Modal>
+1 -2
View File
@@ -39,7 +39,6 @@ export const load: PageLoad = async ({ parent, depends, url }) => {
} finally {
redirect(303, resolve('/'));
}
} else {
redirect(303, resolve('/'));
}
return {};
};