Merge branch '1.5.x' into fix-mfa-screens

This commit is contained in:
Torsten Dittmann
2024-03-06 11:45:56 +01:00
committed by GitHub
54 changed files with 959 additions and 554 deletions
+13 -13
View File
@@ -6,7 +6,7 @@
"": {
"name": "@appwrite/console",
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.14",
"@appwrite.io/console": "^0.6.0-rc.17",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@popperjs/core": "^2.11.8",
@@ -158,9 +158,9 @@
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
},
"node_modules/@appwrite.io/console": {
"version": "0.6.0-rc.14",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.14.tgz",
"integrity": "sha512-7Od0RuGRF01YUCWGTAvlTvgUG7sUCVHG9ig11OKz9jjBxE91BVcWsmqGWiXw+xb4HhF2jiyHd9ZU3sKAo0IC3w==",
"version": "0.6.0-rc.17",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.17.tgz",
"integrity": "sha512-QCUMpvQIt9NseNHlVNFk8wuTro0i6UXeGxO9nuXtIOC9oC20vi5tdiFz1ImjYS2SVtlB0PJRGf58Q/p2WPoR1g==",
"dependencies": {
"cross-fetch": "3.1.5",
"isomorphic-form-data": "2.0.0"
@@ -2336,9 +2336,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz",
"integrity": "sha512-1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
"integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -3256,9 +3256,9 @@
}
},
"node_modules/ai": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.35.tgz",
"integrity": "sha512-YzR56VAMjqvGIVzMUFwH7jjo7K+Z3Old4nXcigiWsuqXnWQeQi3iPR8p2/xOZzmuZyMTOC+6jjbIJ8hoEOhfcQ==",
"version": "2.2.36",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.36.tgz",
"integrity": "sha512-QX3ZXgHjn4cQ233PHne1m7ZP+nfP93CkDEgNCIxuiDp+mhLEIEV10F1Qk3NozJ8mmSoyIJR82ea16kMZKOi8hw==",
"dependencies": {
"eventsource-parser": "1.0.0",
"nanoid": "3.3.6",
@@ -9514,9 +9514,9 @@
}
},
"node_modules/vitest/node_modules/npm-run-path": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
"integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
"dev": true,
"dependencies": {
"path-key": "^4.0.0"
+1 -1
View File
@@ -18,7 +18,7 @@
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.14",
"@appwrite.io/console": "^0.6.0-rc.17",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@popperjs/core": "^2.11.8",
+1
View File
@@ -245,6 +245,7 @@ export enum Submit {
WebhookUpdateUrl = 'submit_webhook_update_url',
WebhookUpdateEvents = 'submit_webhook_update_events',
WebhookUpdateName = 'submit_webhook_update_name',
WebhookUpdateEnabled = 'submit_webhook_update_enabled',
WebhookUpdateSecurity = 'submit_webhook_update_security',
BucketCreate = 'submit_bucket_create',
BucketDelete = 'submit_bucket_delete',
+2
View File
@@ -12,6 +12,7 @@
export let childStart = false;
export let noStyle = false;
export let fullWidth = false;
export let wrapperFullWidth = false;
export let fixed = false;
export let display = 'block';
@@ -104,6 +105,7 @@
<div
class:drop-wrapper={!noStyle}
class:u-cross-child-start={childStart}
class:u-width-full-line={wrapperFullWidth}
bind:this={element}
style:display>
<slot />
+11 -1
View File
@@ -11,10 +11,20 @@
export let noStyle = false;
export let width: string = null;
export let fullWidth = false;
export let wrapperFullWidth = false;
export let position: 'relative' | 'static' = 'relative';
</script>
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
<Drop
bind:show
{placement}
{childStart}
{noArrow}
{noStyle}
{fullWidth}
{wrapperFullWidth}
{fixed}
on:blur>
<slot />
<svelte:fragment slot="list">
<div
+2 -1
View File
@@ -9,6 +9,7 @@
export let query = '[]';
export let columns: Writable<Column[]>;
export let fullWidthMobile = false;
const parsedQueries = queryParamToMap(query);
queries.set(parsedQueries);
@@ -55,7 +56,7 @@
</div>
<div class="is-only-mobile">
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)}>
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
<i class="icon-filter u-opacity-50" />
Filters
{#if applied > 0}
+2 -1
View File
@@ -12,6 +12,7 @@
export let disabled = false;
export let autofocus = false;
export let isWithEndButton = true;
export let fullWidth = false;
let element: HTMLInputElement;
let timer: ReturnType<typeof setTimeout>;
@@ -58,7 +59,7 @@
</script>
<div class="u-flex u-gap-12 common-section u-main-space-between">
<div class="u-flex-basis-50-percent">
<div class={fullWidth ? 'u-width-full-line' : 'u-flex-basis-50-percent'}>
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
<input
{placeholder}
+15 -8
View File
@@ -16,6 +16,7 @@
export let hideColumns = false;
export let allowNoColumns = false;
export let showColsTextMobile = false;
export let fullWidthMobile = false;
let showSelectColumns = false;
@@ -31,12 +32,15 @@
} else {
const prefs = preferences.get($page.route);
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
// Override the shown columns only if a preference was set
if (prefs?.columns) {
columns.set(
$columns.map((column) => {
column.show = prefs.columns?.includes(column.id) ?? true;
return column;
})
);
}
}
columns.subscribe((ctx) => {
@@ -71,8 +75,11 @@
<div class="grid-header-col-4">
{#if !hideColumns && view === View.Table}
{#if $columns?.length}
<DropList bind:show={showSelectColumns} scrollable>
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
<Button
secondary
on:click={() => (showSelectColumns = !showSelectColumns)}
{fullWidthMobile}>
<span
class="icon-view-boards u-opacity-50"
aria-hidden="true"
+2 -1
View File
@@ -15,6 +15,7 @@
export let autocomplete = false;
export let maxlength: number = null;
export let isPopoverDefined = true;
export let fullWidth = false;
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
@@ -49,7 +50,7 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
+2 -1
View File
@@ -18,6 +18,7 @@
export let autocomplete = false;
export let tooltip: string = null;
export let isPopoverDefined = true;
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
@@ -58,7 +59,7 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
+2 -1
View File
@@ -17,6 +17,7 @@
export let minlength = 8;
export let maxlength: number = null;
export let isPopoverDefined = true;
export let fullWidth = false;
let element: HTMLInputElement;
let error: string;
@@ -49,7 +50,7 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show={showPopover} display="inline-block">
+2 -1
View File
@@ -15,6 +15,7 @@
export let autocomplete = false;
export let maxlength: number = null;
export let isPopoverDefined = true;
export let fullWidth = false;
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
@@ -48,7 +49,7 @@
}
</script>
<FormItem>
<FormItem {fullWidth}>
<Label {required} hide={!showLabel} for={id}>
{label}{#if $$slots.popover && isPopoverDefined}
<Drop bind:show display="inline-block">
+58 -4
View File
@@ -4,11 +4,11 @@ type TemplateRuntime = {
};
const TemplateRuntimes = {
NODE: { name: 'node', versions: ['20.0', '19.0', '18.0', '16.0', '14.5'] },
PHP: { name: 'php', versions: ['8.2', '8.1', '8.0'] },
RUBY: { name: 'ruby', versions: ['3.2', '3.1', '3.0'] },
NODE: { name: 'node', versions: ['21.0', '20.0', '19.0', '18.0', '16.0', '14.5'] },
PHP: { name: 'php', versions: ['8.3', '8.2', '8.1', '8.0'] },
RUBY: { name: 'ruby', versions: ['3.3', '3.2', '3.1', '3.0'] },
PYTHON: { name: 'python', versions: ['3.12', '3.11', '3.10', '3.9', '3.8'] },
DART: { name: 'dart', versions: ['3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
DART: { name: 'dart', versions: ['3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
BUN: { name: 'bun', versions: ['1.0'] }
};
@@ -515,6 +515,30 @@ export const marketplace: MarketplaceTemplate[] = [
'npm install',
'src/main.js',
'node/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.PYTHON,
'pip install -r requirements.txt',
'src/main.py',
'python/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.PHP,
'composer install',
'src/index.php',
'php/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.BUN,
'bun install',
'src/main.ts',
'bun/sync-with-meilisearch'
),
...getRuntimes(
TemplateRuntimes.RUBY,
'bundle install',
'lib/main.rb',
'ruby/sync-with-meilisearch'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-meilisearch">file</a>.`,
@@ -599,6 +623,36 @@ export const marketplace: MarketplaceTemplate[] = [
'npm install',
'src/main.js',
'node/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.PYTHON,
'pip install -r requirements.txt',
'src/main.py',
'python/whatsapp_with_vonage'
),
...getRuntimes(
TemplateRuntimes.PHP,
'composer install',
'src/index.php',
'php/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.DART,
'dart pub get',
'lib/main.dart',
'dart/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.RUBY,
'bundle install',
'lib/main.rb',
'ruby/whatsapp-with-vonage'
),
...getRuntimes(
TemplateRuntimes.BUN,
'bun install',
'src/main.ts',
'bun/whatsapp-with-vonage'
)
],
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/whatsapp-with-vonage">file</a>.`,
+1 -1
View File
@@ -8,7 +8,7 @@ export const load: LayoutLoad = async ({ depends }) => {
depends(Dependencies.FACTORS);
return {
factors: await sdk.forConsole.account.listFactors(),
factors: await sdk.forConsole.account.listMfaFactors(),
header: Header,
breadcrumbs: Breadcrumbs
};
+1 -1
View File
@@ -17,7 +17,7 @@
async function deleteProvider() {
try {
await sdk.forConsole.account.deleteAuthenticator(AuthenticatorType.Totp, code);
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp, code);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
+7 -5
View File
@@ -16,16 +16,18 @@
let code: string;
let type: Models.MfaType = null;
let step = 1;
let codes: Models.MfaRecoveryCodes = null;
async function addAuthenticator(): Promise<URL> {
type = await sdk.forConsole.account.addAuthenticator(AuthenticatorType.Totp);
type = await sdk.forConsole.account.createMfaAuthenticator(AuthenticatorType.Totp);
return sdk.forConsole.avatars.getQR(type.uri, 192 * 2);
}
async function verifyAuthenticator() {
try {
await sdk.forConsole.account.verifyAuthenticator(AuthenticatorType.Totp, code);
await sdk.forConsole.account.updateMfaAuthenticator(AuthenticatorType.Totp, code);
codes = await sdk.forConsole.account.createMfaRecoveryCodes();
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showSetup = false;
@@ -111,8 +113,8 @@
description="Learn more about multi-factor authentication in our documentation."
bind:show={showRecoveryCodes}
onSubmit={verifyAuthenticator}>
{#if type}
{@const formattedBackupCodes = type.backups.join('\n')}
{#if type && codes}
{@const formattedBackupCodes = codes.recoveryCodes.join('\n')}
<Alert type="info">
<span slot="title">
It is highly recommended to securely store your recovery codes
@@ -148,7 +150,7 @@
</div>
<Table noMargin noStyles>
<TableBody>
{#each type.backups as code}
{#each codes.recoveryCodes as code}
<TableRow>
<TableCell title="code">
<Output value={code} hideCopyIcon>{code}</Output>
@@ -13,7 +13,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
header: Header,
breadcrumbs: Breadcrumbs,
user: await sdk.forProject.users.get(params.user),
userFactors: await sdk.forProject.users.listFactors(params.user)
userFactors: await sdk.forProject.users.listMfaFactors(params.user)
};
} catch (e) {
error(e.code, e.message);
@@ -15,7 +15,7 @@
async function deleteProvider() {
try {
await sdk.forConsole.users.deleteAuthenticator($user.$id, AuthenticatorType.Totp);
await sdk.forConsole.users.deleteMfaAuthenticator($user.$id, AuthenticatorType.Totp);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
@@ -27,19 +27,21 @@
TableScroll
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { Container, ContainerHeader } from '$lib/layout';
import { MessagingProviderType } from '@appwrite.io/console';
import type { PageData } from './$types';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
import MessageStatusPill from './messageStatusPill.svelte';
import ProviderType from './providerType.svelte';
import { columns, showCreate } from './store';
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';
export let data: PageData;
let selected: string[] = [];
@@ -47,9 +49,17 @@
let deleting = false;
let showFailed = false;
let errors: string[] = [];
let showCreateDropdownMobile = false;
let showCreateDropdownDesktop = false;
let showCreateDropdownMobile = false;
let showCreateDropdownEmpty = false;
const columns = writable<Column[]>([
{ id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 },
{ id: 'message', title: 'Message', type: 'string', show: false, filter: false, width: 140 },
{ id: 'providerType', title: 'Type', type: 'string', show: true, width: 100 },
{ id: 'status', title: 'Status', type: 'string', show: true, width: 120 },
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', show: true, width: 120 },
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', show: false, width: 120 }
]);
const project = $page.params.project;
@@ -82,46 +92,44 @@
</script>
<Container>
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<Heading tag="h2" size="5">Messages</Heading>
<div class="is-only-mobile">
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
</div>
<ContainerHeader title="Messages">
<div class="is-only-mobile">
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
</div>
<!-- TODO: fix width of search input in mobile -->
</ContainerHeader>
<div class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-16">
<SearchQuery
fullWidth
search={data.search}
placeholder="Search by message ID, description, type, or status">
<div class="u-flex u-gap-16 is-not-mobile">
<!-- TODO: make this not database-specific -->
<Filters query={data.query} {columns} />
<ViewSelector
view={data.view}
{columns}
hideView
allowNoColumns
showColsTextMobile />
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
<div class="is-not-mobile u-width-full-line">
<div class="u-flex u-gap-16 u-main-end">
<Filters query={data.query} {columns} />
<ViewSelector
view={data.view}
{columns}
hideView
allowNoColumns
showColsTextMobile />
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
</div>
</div>
</SearchQuery>
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
<div class="is-only-mobile u-flex u-gap-16">
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<ViewSelector
view={data.view}
{columns}
hideView
allowNoColumns
showColsTextMobile />
showColsTextMobile
fullWidthMobile />
</div>
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<Filters query={data.query} {columns} />
<Filters query={data.query} {columns} fullWidthMobile />
</div>
</div>
</div>
{#if data.messages.total}
<TableScroll>
<TableHeader>
@@ -13,12 +13,13 @@
import { onMount } from 'svelte';
import CreateMember from '$routes/console/organization-[organization]/createMember.svelte';
export let inputs: ProviderInput[];
export let inputs: (ProviderInput | ProviderInput[])[];
export let providerType: MessagingProviderType;
export let provider: Providers;
export let providerId: string;
export let params: object;
let flattened: ProviderInput[] = [];
let formValues: object = {};
let originalfiles: Record<string, FileList> = {};
let files: Record<string, FileList> = {};
@@ -41,7 +42,15 @@
}
onMount(() => {
for (const input of inputs) {
flattened = [];
for (const i of inputs) {
if (Array.isArray(i)) {
flattened.push(...i);
} else {
flattened.push(i);
}
}
for (const input of flattened) {
const value = params[input.name];
formValues[input.name] = value;
if (input.type === 'file' && typeof value == 'string' && value.length > 0) {
@@ -198,7 +207,7 @@
}
$: updated =
inputs
flattened
.filter((input) => input.type !== 'file')
.some((input) => formValues[input.name] !== params[input.name]) ||
Object.keys(files).some((key) => files[key] !== originalfiles[key]);
@@ -134,16 +134,20 @@
</div>
<svelte:fragment slot="aside">
<div class="u-flex u-main-space-between">
<div data-private>
<div data-private class="u-flex-vertical u-gap-16">
<ul>
<InputSwitch
id="enabled"
label={enabled ? 'Enabled' : 'Disabled'}
bind:value={enabled} />
</ul>
<p class="title">Provider: <Provider noIcon provider={$providerData.provider} /></p>
<p class="title">Channel: <ProviderType noIcon type={$providerData.type} /></p>
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
<div>
<p class="title">
Provider: <Provider noIcon provider={$providerData.provider} />
</p>
<p class="title">Type: <ProviderType noIcon type={$providerData.type} /></p>
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
</div>
</div>
</div>
</svelte:fragment>
@@ -0,0 +1,147 @@
<script lang="ts">
import {
InputDomain,
InputEmail,
InputFile,
InputSelect,
InputPhone,
InputSwitch,
InputText,
InputPassword
} from '$lib/elements/forms';
import InputCheckbox from '$lib/elements/forms/inputCheckbox.svelte';
import PopoverContent from './popoverContent.svelte';
import type {
ProviderInput,
TwilioProviderParams,
Msg91ProviderParams,
TelesignProviderParams,
TextmagicProviderParams,
VonageProviderParams,
MailgunProviderParams,
SendgridProviderParams,
SMTPProviderParams,
FCMProviderParams,
APNSProviderParams
} from './store';
export let files: Record<string, FileList>;
export let input: ProviderInput;
export let params: Partial<
| TwilioProviderParams
| Msg91ProviderParams
| TelesignProviderParams
| TextmagicProviderParams
| VonageProviderParams
| MailgunProviderParams
| SendgridProviderParams
| SMTPProviderParams
| FCMProviderParams
| APNSProviderParams
>;
export let fullWidth = false;
let popoverImage = input.popoverImage;
</script>
{#if input.type === 'text'}
<InputText
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
tooltip={input.tooltip}
isPopoverDefined={input.popover !== undefined}
{fullWidth}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputText>
{:else if input.type === 'password'}
<InputPassword
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
showPasswordButton
isPopoverDefined={input.popover !== undefined}
{fullWidth}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPassword>
{:else if input.type === 'email'}
<InputEmail
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
tooltip={input.tooltip}
isPopoverDefined={input.popover !== undefined}
{fullWidth}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputEmail>
{:else if input.type === 'domain'}
<InputDomain
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
{fullWidth}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputDomain>
{:else if input.type === 'phone'}
<InputPhone
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
{fullWidth}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPhone>
{:else if input.type === 'file'}
<InputFile
label={input.label}
allowedFileExtensions={input.allowedFileExtensions}
required={!input.optional}
tooltip={input.tooltip}
isPopoverDefined={input.popover !== undefined}
bind:files={files[input.name]}>
{fullWidth}
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputFile>
{:else if input.type === 'switch'}
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
<svelte:fragment slot="description">
<p>{@html input.description}</p>
</svelte:fragment>
</InputSwitch>
{:else if input.type === 'checkbox'}
<InputCheckbox label={input.label} id={input.name} bind:checked={params[input.name]}>
<svelte:fragment slot="description">
{input.description}
</svelte:fragment>
</InputCheckbox>
{:else if input.type === 'select'}
<InputSelect
label={input.label}
id={input.name}
options={input.options}
required={!input.optional}
bind:value={params[input.name]} />
{/if}
@@ -1,17 +1,6 @@
<script lang="ts">
import {
FormList,
InputDomain,
InputEmail,
InputFile,
InputSelect,
InputPhone,
InputSwitch,
InputText,
InputPassword
} from '$lib/elements/forms';
import InputCheckbox from '$lib/elements/forms/inputCheckbox.svelte';
import PopoverContent from './popoverContent.svelte';
import { FormItem, FormList } from '$lib/elements/forms';
import SettingsFormInput from './settingsFormInput.svelte';
import type {
ProviderInput,
TwilioProviderParams,
@@ -27,7 +16,7 @@
} from './store';
export let files: Record<string, FileList>;
export let inputs: ProviderInput[];
export let inputs: (ProviderInput | ProviderInput[])[];
export let params: Partial<
| TwilioProviderParams
| Msg91ProviderParams
@@ -44,98 +33,16 @@
<FormList>
{#each inputs as input}
{@const popoverImage = input.popoverImage}
{#if input.type === 'text'}
<InputText
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputText>
{:else if input.type === 'password'}
<InputPassword
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
showPasswordButton
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPassword>
{:else if input.type === 'email'}
<InputEmail
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputEmail>
{:else if input.type === 'domain'}
<InputDomain
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputDomain>
{:else if input.type === 'phone'}
<InputPhone
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPhone>
{:else if input.type === 'file'}
<InputFile
label={input.label}
allowedFileExtensions={input.allowedFileExtensions}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:files={files[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputFile>
{:else if input.type === 'switch'}
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
<svelte:fragment slot="description">
<p>{@html input.description}</p>
</svelte:fragment>
</InputSwitch>
{:else if input.type === 'checkbox'}
<InputCheckbox label={input.label} id={input.name} bind:checked={params[input.name]}>
<svelte:fragment slot="description">
{input.description}
</svelte:fragment>
</InputCheckbox>
{:else if input.type === 'select'}
<InputSelect
label={input.label}
id={input.name}
options={input.options}
required={!input.optional}
bind:value={params[input.name]} />
{#if Array.isArray(input)}
<FormItem isMultiple>
{#each input as i}
<div class="u-flex u-flex-basis-50-percent">
<SettingsFormInput input={i} bind:params bind:files fullWidth />
</div>
{/each}
</FormItem>
{:else}
<SettingsFormInput {input} bind:params bind:files />
{/if}
{/each}
</FormList>
@@ -26,6 +26,7 @@ export type ProviderInput = {
| 'checkbox';
placeholder?: string;
description?: string;
tooltip?: string;
popover?: string[];
popoverImage?: { src: { light: string; dark: string }; alt: string };
allowedFileExtensions?: string[];
@@ -45,7 +46,7 @@ type ProvidersMap = {
title: string;
description: string;
needAHand?: string[];
configure: ProviderInput[];
configure: (ProviderInput | ProviderInput[])[];
};
};
};
@@ -218,33 +219,37 @@ export const providers: ProvidersMap = {
description:
'Enable the EU region setting if your domain is within the European Union.'
},
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
},
{
label: 'Reply to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Reply to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
[
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
],
[
{
label: 'Reply-to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Reply-to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
]
]
},
[Providers.Sendgrid]: {
@@ -263,66 +268,44 @@ export const providers: ProvidersMap = {
'Head to <b>Settings -> API Keys -> Create API key.</b>'
]
},
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
},
{
label: 'Reply to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Reply to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
[
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
],
[
{
label: 'Reply-to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Reply-to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
]
]
},
[Providers.SMTP]: {
classIcon: 'mail',
title: 'SMTP',
description: '',
description: 'Generic SMTP server.',
configure: [
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
},
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Reply to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
},
{
label: 'Reply to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Server host',
name: 'host',
@@ -367,16 +350,45 @@ export const providers: ProvidersMap = {
type: 'checkbox',
optional: true
},
[
{
label: 'Sender email',
name: 'fromEmail',
type: 'email',
placeholder: 'Enter email'
},
{
label: 'Sender name',
name: 'fromName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
],
[
{
label: 'Reply-to email',
name: 'replyToEmail',
type: 'email',
optional: true,
placeholder: 'Enter email'
},
{
label: 'Reply-to name',
name: 'replyToName',
type: 'text',
optional: true,
placeholder: 'Enter name'
}
],
{
label: 'Mailer',
label: 'X-Mailer header',
name: 'mailer',
type: 'text',
optional: true,
placeholder: 'Enter mailer',
popover: [
'<b>What is the Mailer?</b>',
'The value to use for the X-Mailer header.'
]
tooltip:
'The X-Mailer header is used to identify the software or client used for composing or sending the email. It typically includes information such as the name and version of the email program, aiding in troubleshooting and debugging.'
}
]
}
@@ -134,6 +134,7 @@
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Provider</svelte:fragment>
<div class="u-sep-block-start u-padding-block-end-32" />
<FormList gap={16}>
<InputText
id="name"
@@ -173,3 +174,15 @@
</div>
</FormList>
</WizardStep>
<style lang="scss">
.grid-box {
--grid-gap: 1rem;
grid-template-columns: 1fr 1fr;
}
:global(body:not(.theme-dark)) .grid-box :global(.card) {
--p-card-bg-color-default: var(--color-neutral-5);
--p-card-bg-color-hover: var(--color-neutral-10);
}
</style>
@@ -21,7 +21,15 @@
let open = false;
onMount(() => {
for (const input of inputs) {
const flattened = [];
for (const i of inputs) {
if (Array.isArray(i)) {
flattened.push(...i);
} else {
flattened.push(i);
}
}
for (const input of flattened) {
if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) {
const dataTransfer = new DataTransfer();
const f = new File(
@@ -1,17 +1,7 @@
import type { Column } from '$lib/helpers/types';
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
export const showCreate = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 },
{ id: 'message', title: 'Message', type: 'string', show: false, filter: false, width: 140 },
{ id: 'providerType', title: 'Type', type: 'string', show: true, width: 100 },
{ id: 'status', title: 'Status', type: 'string', show: true, width: 120 },
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', show: true, width: 120 },
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', show: false, width: 120 }
]);
export const targetsById = writable<Record<string, Models.Target>>({});
export const topicsById = writable<Record<string, Models.Topic>>({});
@@ -16,14 +16,33 @@
import { base } from '$app/paths';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { columns, showCreate } from './store';
import { showCreate } from './store';
import { View } from '$lib/helpers/load';
import { Filters, hasPageQueries } from '$lib/components/filters';
import Table from './table.svelte';
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export let data: PageData;
const project = $page.params.project;
const columns = writable<Column[]>([
{ id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 140 },
{ id: 'emailTotal', title: 'Email Subscribers', type: 'integer', show: false, width: 140 },
{ id: 'smsTotal', title: 'SMS Subscribers', type: 'integer', show: false, width: 140 },
{ id: 'pushTotal', title: 'Push Subscribers', type: 'integer', show: false, width: 140 },
{
id: 'total',
title: 'Subscribers',
type: 'integer',
show: true,
filter: false,
width: 140
},
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 }
]);
const topicCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
await goto(`${base}/console/project-${project}/messaging/topics/topic-${event.detail.$id}`);
};
@@ -73,7 +92,7 @@
</div>
</div>
{#if data.topics.total}
<Table {data} />
<Table columns={$columns} {data} />
<PaginationWithLimit
name="Topics"
@@ -1,14 +1,3 @@
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export const showCreate = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 140 },
{ id: 'emailTotal', title: 'Email Subscribers', type: 'integer', show: false, width: 140 },
{ id: 'smsTotal', title: 'SMS Subscribers', type: 'integer', show: false, width: 140 },
{ id: 'pushTotal', title: 'Push Subscribers', type: 'integer', show: false, width: 140 },
{ id: 'total', title: 'Subscribers', type: 'integer', show: true, filter: false, width: 140 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 }
]);
@@ -16,13 +16,14 @@
} from '$lib/elements/table';
import { addNotification } from '$lib/stores/notifications';
import type { PageData } from './$types';
import { columns } from './store';
import { project } from '$routes/console/project-[project]/store';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { toLocaleDateTime } from '$lib/helpers/date';
import type { Column } from '$lib/helpers/types';
export let columns: Column[];
export let data: PageData;
let selectedIds: string[] = [];
@@ -62,7 +63,7 @@
<TableCellHeadCheck
bind:selected={selectedIds}
pageItemsIds={data.topics.topics.map((d) => d.$id)} />
{#each $columns as column}
{#each columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
{/if}
@@ -74,10 +75,10 @@
href={`${base}/console/project-${$project.$id}/messaging/topics/topic-${topic.$id}`}>
<TableCellCheck bind:selectedIds id={topic.$id} />
{#each $columns as column (column.id)}
{#each columns as column (column.id)}
{#if column.show}
{#if column.id === '$id'}
{#key $columns}
{#key column.id}
<TableCell title={column.title} width={column.width}>
<Id value={topic.$id}>{topic.$id}</Id>
</TableCell>
@@ -1,14 +1,178 @@
<script lang="ts">
import { page } from '$app/stores';
import { Button } from '$lib/elements/forms';
import {
Empty,
EmptySearch,
SearchQuery,
PaginationWithLimit,
Heading,
ViewSelector,
EmptyFilter
} from '$lib/components';
import { Container } from '$lib/layout';
import DangerZone from './dangerZone.svelte';
import Details from './details.svelte';
import UpdateName from './updateName.svelte';
import UpdatePermissions from './updatePermissions.svelte';
import { ID, type Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import Table from './table.svelte';
import { targetsById } from '../../store';
import UserTargetsModal from '../../userTargetsModal.svelte';
import { onMount } from 'svelte';
import { Filters, hasPageQueries } from '$lib/components/filters';
import { View } from '$lib/helpers/load';
import { writable } from 'svelte/store';
import type { Column } from '$lib/helpers/types';
export let data: PageData;
let showAdd = false;
let subscribersByTargetId: Record<string, Models.Subscriber> = {};
const columns = writable<Column[]>([
{ id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 },
{ id: 'userName', title: 'Name', type: 'string', show: true, filter: false, width: 100 },
{ id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 },
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
{ id: 'type', title: 'Type', type: 'string', show: true, width: 80 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 100 }
]);
onMount(() => {
$targetsById = {};
for (const subscriber of $page.data.subscribers.subscribers) {
const { target } = subscriber;
$targetsById[target.$id] = target;
subscribersByTargetId[target.$id] = subscriber;
}
});
async function addTargets(event: CustomEvent<Record<string, Models.Target>>) {
showAdd = false;
$targetsById = event.detail;
const targetIds = Object.keys($targetsById).filter(
(targetId) => !(targetId in subscribersByTargetId)
);
const promises = targetIds.map(async (targetId) => {
const subscriber = await sdk.forProject.messaging.createSubscriber(
$page.params.topic,
ID.unique(),
targetId
);
subscribersByTargetId[targetId] = subscriber;
});
try {
await Promise.all(promises);
addNotification({
type: 'success',
message: `Added ${targetIds.length} subscriber${
targetIds.length > 1 ? 's' : ''
} to topic`
});
trackEvent(Submit.MessagingTopicSubscriberAdd);
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingTopicSubscriberAdd);
}
}
</script>
<Container>
<Details />
<UpdateName />
<UpdatePermissions />
<DangerZone />
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<Heading tag="h2" size="5">Subscribers</Heading>
<div class="is-only-mobile">
<Button on:click={() => (showAdd = true)} event="create_subscriber">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add subscriber</span>
</Button>
</div>
</div>
<!-- TODO: fix width of search input in mobile -->
<SearchQuery
search={data.search}
placeholder="Search by subscriber ID, target ID, user ID, or type">
<div class="u-flex u-gap-16 is-not-mobile">
<Filters query={data.query} {columns} />
<ViewSelector
view={View.Table}
{columns}
hideView
allowNoColumns
showColsTextMobile />
<Button on:click={() => (showAdd = true)} event="create_subscriber">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add subscriber</span>
</Button>
</div>
</SearchQuery>
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<ViewSelector
view={View.Table}
{columns}
hideView
allowNoColumns
showColsTextMobile />
</div>
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<Filters query={data.query} {columns} />
</div>
</div>
</div>
{#if data.subscribers.total}
<Table columns={$columns} {data} />
<PaginationWithLimit
name="Subscribers"
limit={data.limit}
offset={data.offset}
total={data.subscribers.total} />
{:else if $hasPageQueries}
<EmptyFilter resource="subscribers" />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no subscribers that match your search.</p>
</div>
<Button
secondary
href={`/console/project-${$page.params.project}/messaging/topics/topic-${$page.params.topic}/subscribers`}>
Clear Search
</Button>
</EmptySearch>
{:else}
<Empty
single
on:click={() => (showAdd = true)}
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="subscriber" />
{/if}
</Container>
<UserTargetsModal
title="Select subscribers"
bind:show={showAdd}
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
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>
</svelte:fragment>
</UserTargetsModal>
@@ -11,11 +11,6 @@
const tabs = [
{
href: path,
title: 'Overview',
event: 'overview'
},
{
href: `${path}/subscribers`,
title: 'Subscribers',
event: 'subscribers'
},
@@ -23,6 +18,11 @@
href: `${path}/activity`,
title: 'Activity',
event: 'activity'
},
{
href: `${path}/settings`,
title: 'Settings',
event: 'settings'
}
];
</script>
@@ -0,0 +1,14 @@
<script lang="ts">
import { Container } from '$lib/layout';
import DangerZone from './dangerZone.svelte';
import Details from './details.svelte';
import UpdateName from './updateName.svelte';
import UpdatePermissions from './updatePermissions.svelte';
</script>
<Container>
<Details />
<UpdateName />
<UpdatePermissions />
<DangerZone />
</Container>
@@ -1,20 +1,10 @@
<script lang="ts" context="module">
import { get } from 'svelte/store';
let showDelete = writable(false);
export const promptDeleteUser = (id: string) => {
showDelete.set(true);
goto(`/console/project-${get(project).$id}/auth/user-${id}`);
};
</script>
<script lang="ts">
import { CardGrid, BoxAvatar, Heading } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { writable } from 'svelte/store';
import { topic, topicTotal } from './store';
import { goto } from '$app/navigation';
import { project } from '$routes/console/project-[project]/store';
import DeleteTopic from './deleteTopic.svelte';
import { topic, topicTotal } from '../store';
import DeleteTopic from '../deleteTopic.svelte';
let showDelete = false;
</script>
<CardGrid danger>
@@ -37,9 +27,9 @@
</svelte:fragment>
<svelte:fragment slot="actions">
<Button secondary on:click={() => ($showDelete = true)} event="delete_messaging_provider"
<Button secondary on:click={() => (showDelete = true)} event="delete_messaging_topic"
>Delete</Button>
</svelte:fragment>
</CardGrid>
<DeleteTopic bind:showDelete={$showDelete} />
<DeleteTopic bind:showDelete />
@@ -1,7 +1,7 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { toLocaleDateTime } from '$lib/helpers/date';
import { topic, topicTotal } from './store';
import { topic, topicTotal } from '../store';
</script>
<CardGrid>
@@ -2,7 +2,7 @@
import { CardGrid, Heading } from '$lib/components';
import { Button, Form, InputText } from '$lib/elements/forms';
import { onMount } from 'svelte';
import { topic } from './store';
import { topic } from '../store';
import { invalidate } from '$app/navigation';
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
@@ -10,7 +10,7 @@
import { onMount } from 'svelte';
import { Roles } from '$lib/components/permissions';
import { symmetricDifference } from '$lib/helpers/array';
import { topic } from './store';
import { topic } from '../store';
const topicId = $page.params.topic;
@@ -1,169 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { Button } from '$lib/elements/forms';
import {
Empty,
EmptySearch,
SearchQuery,
PaginationWithLimit,
Heading,
ViewSelector,
EmptyFilter
} from '$lib/components';
import { Container } from '$lib/layout';
import { ID, type Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import Table from './table.svelte';
import { targetsById } from '../../../store';
import UserTargetsModal from '../../../userTargetsModal.svelte';
import { onMount } from 'svelte';
import { Filters, hasPageQueries } from '$lib/components/filters';
import { columns } from './store';
import { View } from '$lib/helpers/load';
export let data: PageData;
let showAdd = false;
let subscribersByTargetId: Record<string, Models.Subscriber> = {};
onMount(() => {
$targetsById = {};
for (const subscriber of $page.data.subscribers.subscribers) {
const { target } = subscriber;
$targetsById[target.$id] = target;
subscribersByTargetId[target.$id] = subscriber;
}
});
async function addTargets(event: CustomEvent<Record<string, Models.Target>>) {
showAdd = false;
$targetsById = event.detail;
const targetIds = Object.keys($targetsById).filter(
(targetId) => !(targetId in subscribersByTargetId)
);
const promises = targetIds.map(async (targetId) => {
const subscriber = await sdk.forProject.messaging.createSubscriber(
$page.params.topic,
ID.unique(),
targetId
);
subscribersByTargetId[targetId] = subscriber;
});
try {
await Promise.all(promises);
addNotification({
type: 'success',
message: `Added ${targetIds.length} subscriber${
targetIds.length > 1 ? 's' : ''
} to topic`
});
trackEvent(Submit.MessagingTopicSubscriberAdd);
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingTopicSubscriberAdd);
}
}
</script>
<Container>
<div class="u-flex u-flex-vertical">
<div class="u-flex u-main-space-between">
<Heading tag="h2" size="5">Subscribers</Heading>
<div class="is-only-mobile">
<Button on:click={() => (showAdd = true)} event="create_subscriber">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add subscriber</span>
</Button>
</div>
</div>
<!-- TODO: fix width of search input in mobile -->
<SearchQuery
search={data.search}
placeholder="Search by subscriber ID, target ID, user ID, or type">
<div class="u-flex u-gap-16 is-not-mobile">
<Filters query={data.query} {columns} />
<ViewSelector
view={View.Table}
{columns}
hideView
allowNoColumns
showColsTextMobile />
<Button on:click={() => (showAdd = true)} event="create_subscriber">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Add subscriber</span>
</Button>
</div>
</SearchQuery>
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<ViewSelector
view={View.Table}
{columns}
hideView
allowNoColumns
showColsTextMobile />
</div>
<div class="u-flex-basis-50-percent">
<!-- TODO: fix width -->
<Filters query={data.query} {columns} />
</div>
</div>
</div>
{#if data.subscribers.total}
<Table {data} />
<PaginationWithLimit
name="Subscribers"
limit={data.limit}
offset={data.offset}
total={data.subscribers.total} />
{:else if $hasPageQueries}
<EmptyFilter resource="subscribers" />
{:else if data.search}
<EmptySearch>
<div class="u-text-center">
<b>Sorry, we couldn't find '{data.search}'</b>
<p>There are no subscribers that match your search.</p>
</div>
<Button
secondary
href={`/console/project-${$page.params.project}/messaging/topics/topic-${$page.params.topic}/subscribers`}>
Clear Search
</Button>
</EmptySearch>
{:else}
<Empty
single
on:click={() => (showAdd = true)}
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="subscriber" />
{/if}
</Container>
<UserTargetsModal
title="Select subscribers"
bind:show={showAdd}
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
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>
</svelte:fragment>
</UserTargetsModal>
@@ -1,11 +0,0 @@
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 },
{ id: 'userName', title: 'Name', type: 'string', show: true, filter: false, width: 100 },
{ id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 },
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
{ id: 'type', title: 'Type', type: 'string', show: true, width: 80 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 100 }
]);
@@ -18,15 +18,16 @@
} from '$lib/elements/table';
import { addNotification } from '$lib/stores/notifications';
import type { PageData } from './$types';
import { columns } from './store';
import ProviderType from '../../../providerType.svelte';
import ProviderType from '../../providerType.svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import { project } from '$routes/console/project-[project]/store';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/stores';
import { targetsById } from '../../../store';
import { targetsById } from '../../store';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import type { Column } from '$lib/helpers/types';
export let columns: Column[];
export let data: PageData;
let subscribers: Record<string, Models.Subscriber> = {};
@@ -84,7 +85,7 @@
<TableCellHeadCheck
bind:selected={selectedIds}
pageItemsIds={data.subscribers.subscribers.map((d) => d.$id)} />
{#each $columns as column}
{#each columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
{/if}
@@ -97,10 +98,10 @@
href={`${base}/console/project-${$project.$id}/auth/user-${subscriber.target.userId}`}>
<TableCellCheck bind:selectedIds id={subscriber.$id} />
{#each $columns as column}
{#each columns as column}
{#if column.show}
{#if column.id === '$id'}
{#key $columns}
{#key column.id}
<TableCell title="Subscriber ID">
<Id value={subscriber.$id}>
{subscriber.$id}
@@ -49,6 +49,7 @@
<InputText
id="subject"
label="Subject"
required
placeholder="Enter subject"
bind:value={$messageParams[$providerType]['subject']}>
</InputText>
@@ -56,10 +57,10 @@
<InputTextarea
id="message"
label="Message"
required
placeholder="Type here..."
bind:value={$messageParams[$providerType]['content']}>
</InputTextarea>
<!-- TODO: Add support for draft messages -->
<!-- <div class="u-flex u-main-end">
<Button text on:click={() => (showTest = true)}>Send test message</Button>
</div> -->
@@ -91,6 +91,7 @@
<InputText
id="title"
label="Title"
required
placeholder="Enter title"
bind:value={$messageParams[MessagingProviderType.Push]['title']}>
</InputText>
@@ -99,6 +100,7 @@
id="message"
label="Message"
placeholder="Type here..."
required
maxlength={1000}
bind:value={$messageParams[MessagingProviderType.Push]['body']}>
</InputTextarea>
@@ -38,8 +38,9 @@
<InputTextarea
id="message"
label="Message"
placeholder="Type here..."
required
maxlength={900}
placeholder="Type here..."
bind:value={$messageParams[$providerType]['content']}>
</InputTextarea>
<!-- TODO: Add support for draft messages -->
@@ -41,13 +41,19 @@
$targetsById = rest;
}
async function beforeSubmit() {
if (topicIdsLength === 0 && targetIdsLength === 0) {
throw new Error('At least one topic or target is required.');
}
}
$: topicIdsLength = Object.keys($topicsById).length;
$: targetIdsLength = Object.keys($targetsById).length;
$: $messageParams[$providerType].targets = Object.keys($targetsById);
$: $messageParams[$providerType].topics = Object.keys($topicsById);
</script>
<WizardStep>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Targets</svelte:fragment>
<svelte:fragment slot="subtitle">
Select targets to whom this message should be directed.
@@ -7,8 +7,7 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Empty } from '$lib/components';
import { Pill } from '$lib/elements';
import { Empty, Id } from '$lib/components';
import {
TableBody,
TableRowLink,
@@ -18,14 +17,23 @@
TableHeader,
TableScroll
} from '$lib/elements/table';
import { Container, ContainerHeader } from '$lib/layout';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container, GridHeader } from '$lib/layout';
import { Button } from '$lib/elements/forms';
import { wizard } from '$lib/stores/wizard';
import type { PageData } from './$types';
import Create from './createWebhook.svelte';
import { columns } from './store';
import { updateCommandGroupRanks } from '$lib/commandCenter';
import { type Models } from '@appwrite.io/console';
import Create from './createWebhook.svelte';
import FailedModal from './failedModal.svelte';
import MessageStatusPill from './messageStatusPill.svelte';
export let data: PageData;
let showFailed = false;
let selectedWebhook: Models.Webhook;
function openWizard() {
wizard.start(Create);
}
@@ -40,34 +48,69 @@
</svelte:head>
<Container>
<ContainerHeader
title="Webhooks"
buttonText="Create webhook"
buttonMethod={openWizard}
buttonEvent="create_webhook"
total={data.webhooks.total} />
<GridHeader title="Webhooks" {columns} view={data.view} hideColumns={false} hideView={true}>
<Button on:click={openWizard} event="create_webhook">
<span class="icon-plus" aria-hidden="true" />
<span class="text">Create webhook</span>
</Button>
</GridHeader>
{#if data.webhooks.total}
<TableScroll>
<TableHeader>
<TableCellHead width={200}>Name</TableCellHead>
<TableCellHead width={180}>POST URL</TableCellHead>
<TableCellHead width={80}>Events</TableCellHead>
{#each $columns as column}
{#if column.show}
<TableCellHead width={column.width}>{column.title}</TableCellHead>
{/if}
{/each}
</TableHeader>
<TableBody service="webhooks" total={data.webhooks.total}>
{#each data.webhooks.webhooks as webhook}
<TableRowLink
href={`${base}/console/project-${projectId}/settings/webhooks/${webhook.$id}`}>
<TableCell title="Name">
<div class="u-flex u-main-space-between u-cross-center">
{webhook.name}
{#if webhook.security === false}
<Pill>SLL/TLS disabled</Pill>
{#each $columns as column (column.id)}
{#if column.show}
{#if column.id === '$id'}
{#key $columns}
<TableCell title={column.title} width={column.width}>
<Id value={webhook.$id}>{webhook.$id}</Id>
</TableCell>
{/key}
{:else if column.id === 'events'}
<TableCellText title={column.title} width={column.width}>
{webhook.events.length}
</TableCellText>
{:else if column.type === 'datetime'}
<TableCellText title={column.title} width={column.width}>
{#if !webhook[column.id]}
-
{:else}
{toLocaleDateTime(webhook[column.id])}
{/if}
</TableCellText>
{:else if column.id === 'enabled'}
<TableCellText title={column.title} width={column.width}>
<MessageStatusPill enabled={webhook.enabled} />
&nbsp;
{#if webhook.enabled === false}
<Button
link
on:click={(e) => {
e.preventDefault();
selectedWebhook = webhook;
showFailed = true;
}}>Details</Button>
{/if}
</TableCellText>
{:else}
<TableCellText title={column.title} width={column.width}>
{webhook[column.id]}
</TableCellText>
{/if}
</div>
</TableCell>
<TableCellText title="URL">{webhook.url}</TableCellText>
<TableCellText title="Scopes">{webhook.events.length} events</TableCellText>
{/if}
{/each}
</TableRowLink>
{/each}
</TableBody>
@@ -80,3 +123,5 @@
on:click={openWizard} />
{/if}
</Container>
<FailedModal bind:show={showFailed} bind:webhook={selectedWebhook} />
@@ -1,11 +1,17 @@
import { Dependencies } from '$lib/constants';
import { View, getPage, getView } from '$lib/helpers/load';
import { sdk } from '$lib/stores/sdk';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, depends }) => {
export const load: PageLoad = async ({ params, url, route, depends }) => {
depends(Dependencies.WEBHOOKS);
const page = getPage(url);
const view = getView(url, route, View.Grid);
return {
page,
view,
webhooks: await sdk.forConsole.projects.listWebhooks(params.project)
};
};
@@ -6,9 +6,11 @@
import UpdateSecurity from './updateSecurity.svelte';
import UpdateSignature from './updateSignature.svelte';
import UpdateUrl from './updateURL.svelte';
import Details from './details.svelte';
</script>
<Container>
<Details />
<UpdateSignature />
<UpdateName />
<UpdateUrl />
@@ -0,0 +1,89 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import Button from '$lib/elements/forms/button.svelte';
import { InputSwitch } from '$lib/elements/forms';
import { webhook } from './store';
import { toLocaleDateTime } from '$lib/helpers/date';
import FailedModal from '../failedModal.svelte';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import MessageStatusPill from '../messageStatusPill.svelte';
import FormList from '$lib/elements/forms/formList.svelte';
const projectId = $page.params.project;
let enabled: boolean;
onMount(async () => {
enabled = $webhook.enabled;
});
let showFailed = false;
async function setEnabled() {
try {
await sdk.forConsole.projects.updateWebhook(
projectId,
$webhook.$id,
$webhook.name,
$webhook.events,
$webhook.url,
$webhook.security,
enabled,
$webhook.httpUser || undefined,
$webhook.httpPass || undefined
);
await invalidate(Dependencies.WEBHOOK);
addNotification({
type: 'success',
message: 'Webhook has been ' + (enabled ? 'enabled' : 'disabled')
});
trackEvent(Submit.WebhookUpdateEnabled);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.WebhookUpdateEnabled);
}
}
</script>
<CardGrid>
<Heading tag="h2" size="7">{$webhook.url}</Heading>
<svelte:fragment slot="aside">
<div class="u-flex u-gap-16">
<ul class="u-stretch">
<FormList>
<InputSwitch
label={enabled ? 'Enabled' : 'Disabled'}
id="enable-switch"
bind:value={enabled} />
</FormList>
<li style="margin-top:16px">Created: {toLocaleDateTime($webhook.$createdAt)}</li>
<li>Last updated: {toLocaleDateTime($webhook.$updatedAt)}</li>
</ul>
<MessageStatusPill enabled={$webhook.enabled} />
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<div class="u-flex u-gap-16">
{#if $webhook.logs.length > 0}
<Button on:click={() => (showFailed = true)} secondary>View logs</Button>
{/if}
<Button on:click={setEnabled} disabled={enabled === $webhook.enabled} submit
>Update</Button>
</div>
</svelte:fragment>
</CardGrid>
<FailedModal bind:show={showFailed} webhook={$webhook} showUpdateButton={false} />
@@ -12,6 +12,6 @@
<CoverTitle href={`/console/project-${projectId}/settings/webhooks`}>
{$webhook?.name}
</CoverTitle>
<Id value={$webhook?.$id} event="webhook">Webhook ID</Id>
<Id value={$webhook?.$id} event="webhook">{$webhook?.$id}</Id>
</svelte:fragment>
</Cover>
@@ -0,0 +1,52 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import Code from '$lib/components/code.svelte';
import Modal from '$lib/components/modal.svelte';
import Button from '$lib/elements/forms/button.svelte';
import { type Models } from '@appwrite.io/console';
export let show: boolean;
export let webhook: Models.Webhook;
export let showUpdateButton: boolean = true;
const projectId = $page.params.project;
$: webhookPath = webhook
? `${base}/console/project-${projectId}/settings/webhooks/` + (webhook['$id'] ?? '')
: '';
let enabled = webhook ? webhook.enabled : false;
$: style = `color: ${enabled ? 'hsl(var(--color-success-100))' : 'hsl(var(--color-danger-100))'}`;
$: iconClass = enabled ? 'icon-check-circle' : 'icon-exclamation-circle';
</script>
<Modal title="Webhook error" headerDivider={false} bind:show size="big">
<div class="box u-flex-vertical u-gap-24">
<div class="u-inline-flex u-cross-center u-gap-8">
<p class="u-stretch">
<span class={`${iconClass} u-font-size-20`} aria-hidden="true" {style} />
<span>Webhook {webhook.enabled ? 'enabled' : 'disabled'}</span>
</p>
{#if showUpdateButton}
<Button secondary href={webhookPath} event="update_webhook">Update webhook</Button>
{/if}
</div>
<p>
The connection to {webhook.url} stopped after {webhook.attempts} consecutive failures. Please
refer to the logs below for more information or try to update the webhook to resolve this
issue.
</p>
<div style="max-inline-size: 524px">
<Code language="html" code={webhook.logs} noMargin noBoxPadding allowScroll />
</div>
</div>
<svelte:fragment slot="footer">
<Button secondary on:click={() => (show = false)}>Close</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Pill } from '$lib/elements';
export let enabled: boolean;
$: style = `color: ${enabled ? 'hsl(var(--color-success-120))' : 'hsl(var(--color-danger-120))'}`;
$: iconClass = (enabled ? 'icon-check-circle' : 'icon-exclamation-circle') + ' u-font-size-12';
</script>
<Pill success={enabled} danger={!enabled} on:click>
<span class="text u-font-size-12 u-trim">
<span class={iconClass} aria-hidden="true" {style} />
<span>{enabled ? 'enabled' : 'stopped'}</span>
</span>
</Pill>
@@ -0,0 +1,17 @@
// import { page } from '$app/stores';
import type { Column } from '$lib/helpers/types';
// import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
// export const database = derived(page, ($page) => $page.data.database as Models.Database);
// export const showCreate = writable(false);
export const columns = writable<Column[]>([
{ id: '$id', title: 'Webhook ID', type: 'string', show: true, width: 150 },
{ id: 'name', title: 'Name', type: 'string', show: true, width: 120 },
{ id: 'url', title: 'Post URL', type: 'string', show: true, width: 120 },
{ id: 'events', title: 'Events', type: 'string', show: true, width: 60 },
{ id: 'enabled', title: 'Status', type: 'string', show: true, width: 150 },
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 120 },
{ id: '$updatedAt', title: 'Updated', type: 'datetime', show: true, width: 120 }
]);
+2 -12
View File
@@ -16,10 +16,10 @@
async function verify() {
try {
disabled = true;
const challenge = await sdk.forConsole.account.createChallenge(
const challenge = await sdk.forConsole.account.createMfaChallenge(
AuthenticationFactor.Totp
);
await sdk.forConsole.account.updateChallenge(challenge.$id, code);
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountCreate);
if ($page.url.searchParams) {
@@ -61,14 +61,4 @@
</FormList>
</Form>
</svelte:fragment>
<svelte:fragment slot="links">
<li class="inline-links-item">
<a href={`${base}/recover`}><span class="text">Forgot Password?</span></a>
</li>
<li class="inline-links-item">
<a href={`${base}/register${$page?.url?.search ?? ''}`}>
<span class="text">Sign Up</span>
</a>
</li>
</svelte:fragment>
</Unauthenticated>