mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.5.x' into fix-mfa-screens
This commit is contained in:
Generated
+13
-13
@@ -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
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>.`,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+12
-3
@@ -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]);
|
||||
|
||||
+8
-4
@@ -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>
|
||||
|
||||
+14
@@ -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>
|
||||
+6
-16
@@ -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
-1
@@ -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>
|
||||
+1
-1
@@ -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';
|
||||
+1
-1
@@ -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;
|
||||
|
||||
-169
@@ -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>
|
||||
-11
@@ -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 }
|
||||
]);
|
||||
+7
-6
@@ -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} />
|
||||
|
||||
|
||||
|
||||
{#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 }
|
||||
]);
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user