From a3e5f7ba128179732a97a3d205e921f64c1aedfe Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:29:53 +0000 Subject: [PATCH 001/187] feat: add sessions scope --- src/lib/constants.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a2107d03c..377df669d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -60,6 +60,11 @@ export const scopes: { description: "Access to create, update, and delete your project's users", category: 'Auth' }, + { + scope: 'sessions', + description: "Access to create and read your project's user sessions", + category: 'Auth' + }, { scope: 'teams.read', description: "Access to read your project's teams", From 40ae318dd4bc31d47bb0ba9cbe1ff3c63a5d3a2d Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:02:09 +0000 Subject: [PATCH 002/187] feat: account scope --- src/lib/constants.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 377df669d..0a4d5372d 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -62,7 +62,12 @@ export const scopes: { }, { scope: 'sessions', - description: "Access to create and read your project's user sessions", + description: "Access to create sessions as one of your project's users", + category: 'Auth' + }, + { + scope: 'account', + description: "Access to perform actions as one of your project's users", category: 'Auth' }, { From 76d64cddc04ebf25ba033e211698ecba6ff09347 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Mon, 16 Oct 2023 15:02:20 -0700 Subject: [PATCH 003/187] Add messaging to navigation --- src/lib/layout/navigation.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib/layout/navigation.svelte b/src/lib/layout/navigation.svelte index 8d5eb201d..d0797059b 100644 --- a/src/lib/layout/navigation.svelte +++ b/src/lib/layout/navigation.svelte @@ -123,6 +123,23 @@ Functions +
  • + trackEvent('click_menu_messaging')} + href={`${projectPath}/messaging`} + use:tooltip={{ + content: 'Messaging', + placement: 'right', + disabled: !narrow + }}> + +
  • Date: Tue, 17 Oct 2023 17:16:39 -0700 Subject: [PATCH 004/187] Add new messaging provider icons --- static/icons/dark/color/firebase.svg | 6 ++++++ static/icons/dark/color/mailgun.svg | 3 +++ static/icons/dark/color/mqtt.svg | 3 +++ static/icons/dark/color/msg91.svg | 4 ++++ static/icons/dark/color/sendgrid.svg | 18 ++++++++++++++++++ static/icons/dark/color/telesign.svg | 5 +++++ static/icons/dark/color/textmagic.svg | 7 +++++++ static/icons/dark/color/twilio.svg | 3 +++ static/icons/dark/color/vonage.svg | 4 ++++ static/icons/dark/grayscale/firebase.svg | 3 +++ static/icons/dark/grayscale/mailgun.svg | 3 +++ static/icons/dark/grayscale/mqtt.svg | 3 +++ static/icons/dark/grayscale/msg91.svg | 3 +++ static/icons/dark/grayscale/sendgrid.svg | 18 ++++++++++++++++++ static/icons/dark/grayscale/telesign.svg | 5 +++++ static/icons/dark/grayscale/textmagic.svg | 3 +++ static/icons/dark/grayscale/twilio.svg | 3 +++ static/icons/dark/grayscale/vonage.svg | 4 ++++ static/icons/light/color/firebase.svg | 6 ++++++ static/icons/light/color/mailgun.svg | 3 +++ static/icons/light/color/mqtt.svg | 3 +++ static/icons/light/color/msg91.svg | 4 ++++ static/icons/light/color/sendgrid.svg | 18 ++++++++++++++++++ static/icons/light/color/telesign.svg | 5 +++++ static/icons/light/color/textmagic.svg | 7 +++++++ static/icons/light/color/twilio.svg | 3 +++ static/icons/light/color/vonage.svg | 4 ++++ static/icons/light/grayscale/firebase.svg | 3 +++ static/icons/light/grayscale/mailgun.svg | 3 +++ static/icons/light/grayscale/mqtt.svg | 3 +++ static/icons/light/grayscale/msg91.svg | 3 +++ static/icons/light/grayscale/sendgrid.svg | 18 ++++++++++++++++++ static/icons/light/grayscale/telesign.svg | 5 +++++ static/icons/light/grayscale/textmagic.svg | 3 +++ static/icons/light/grayscale/twilio.svg | 3 +++ static/icons/light/grayscale/vonage.svg | 4 ++++ 36 files changed, 196 insertions(+) create mode 100644 static/icons/dark/color/firebase.svg create mode 100644 static/icons/dark/color/mailgun.svg create mode 100644 static/icons/dark/color/mqtt.svg create mode 100644 static/icons/dark/color/msg91.svg create mode 100644 static/icons/dark/color/sendgrid.svg create mode 100644 static/icons/dark/color/telesign.svg create mode 100644 static/icons/dark/color/textmagic.svg create mode 100644 static/icons/dark/color/twilio.svg create mode 100644 static/icons/dark/color/vonage.svg create mode 100644 static/icons/dark/grayscale/firebase.svg create mode 100644 static/icons/dark/grayscale/mailgun.svg create mode 100644 static/icons/dark/grayscale/mqtt.svg create mode 100644 static/icons/dark/grayscale/msg91.svg create mode 100644 static/icons/dark/grayscale/sendgrid.svg create mode 100644 static/icons/dark/grayscale/telesign.svg create mode 100644 static/icons/dark/grayscale/textmagic.svg create mode 100644 static/icons/dark/grayscale/twilio.svg create mode 100644 static/icons/dark/grayscale/vonage.svg create mode 100644 static/icons/light/color/firebase.svg create mode 100644 static/icons/light/color/mailgun.svg create mode 100644 static/icons/light/color/mqtt.svg create mode 100644 static/icons/light/color/msg91.svg create mode 100644 static/icons/light/color/sendgrid.svg create mode 100644 static/icons/light/color/telesign.svg create mode 100644 static/icons/light/color/textmagic.svg create mode 100644 static/icons/light/color/twilio.svg create mode 100644 static/icons/light/color/vonage.svg create mode 100644 static/icons/light/grayscale/firebase.svg create mode 100644 static/icons/light/grayscale/mailgun.svg create mode 100644 static/icons/light/grayscale/mqtt.svg create mode 100644 static/icons/light/grayscale/msg91.svg create mode 100644 static/icons/light/grayscale/sendgrid.svg create mode 100644 static/icons/light/grayscale/telesign.svg create mode 100644 static/icons/light/grayscale/textmagic.svg create mode 100644 static/icons/light/grayscale/twilio.svg create mode 100644 static/icons/light/grayscale/vonage.svg diff --git a/static/icons/dark/color/firebase.svg b/static/icons/dark/color/firebase.svg new file mode 100644 index 000000000..2016d2483 --- /dev/null +++ b/static/icons/dark/color/firebase.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/icons/dark/color/mailgun.svg b/static/icons/dark/color/mailgun.svg new file mode 100644 index 000000000..eb456be0a --- /dev/null +++ b/static/icons/dark/color/mailgun.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/mqtt.svg b/static/icons/dark/color/mqtt.svg new file mode 100644 index 000000000..0d966304c --- /dev/null +++ b/static/icons/dark/color/mqtt.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/color/msg91.svg b/static/icons/dark/color/msg91.svg new file mode 100644 index 000000000..8e1743219 --- /dev/null +++ b/static/icons/dark/color/msg91.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/dark/color/sendgrid.svg b/static/icons/dark/color/sendgrid.svg new file mode 100644 index 000000000..ea185b767 --- /dev/null +++ b/static/icons/dark/color/sendgrid.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/static/icons/dark/color/telesign.svg b/static/icons/dark/color/telesign.svg new file mode 100644 index 000000000..af90bab97 --- /dev/null +++ b/static/icons/dark/color/telesign.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/dark/color/textmagic.svg b/static/icons/dark/color/textmagic.svg new file mode 100644 index 000000000..74def7e81 --- /dev/null +++ b/static/icons/dark/color/textmagic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/icons/dark/color/twilio.svg b/static/icons/dark/color/twilio.svg new file mode 100644 index 000000000..866c3aa77 --- /dev/null +++ b/static/icons/dark/color/twilio.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/dark/color/vonage.svg b/static/icons/dark/color/vonage.svg new file mode 100644 index 000000000..76c7c330b --- /dev/null +++ b/static/icons/dark/color/vonage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/dark/grayscale/firebase.svg b/static/icons/dark/grayscale/firebase.svg new file mode 100644 index 000000000..e1158a544 --- /dev/null +++ b/static/icons/dark/grayscale/firebase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/dark/grayscale/mailgun.svg b/static/icons/dark/grayscale/mailgun.svg new file mode 100644 index 000000000..7f7b7a5a1 --- /dev/null +++ b/static/icons/dark/grayscale/mailgun.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/grayscale/mqtt.svg b/static/icons/dark/grayscale/mqtt.svg new file mode 100644 index 000000000..d2189eeb4 --- /dev/null +++ b/static/icons/dark/grayscale/mqtt.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/dark/grayscale/msg91.svg b/static/icons/dark/grayscale/msg91.svg new file mode 100644 index 000000000..321b9e02d --- /dev/null +++ b/static/icons/dark/grayscale/msg91.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/grayscale/sendgrid.svg b/static/icons/dark/grayscale/sendgrid.svg new file mode 100644 index 000000000..cf1878874 --- /dev/null +++ b/static/icons/dark/grayscale/sendgrid.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/static/icons/dark/grayscale/telesign.svg b/static/icons/dark/grayscale/telesign.svg new file mode 100644 index 000000000..28816bd3c --- /dev/null +++ b/static/icons/dark/grayscale/telesign.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/dark/grayscale/textmagic.svg b/static/icons/dark/grayscale/textmagic.svg new file mode 100644 index 000000000..1b11720a7 --- /dev/null +++ b/static/icons/dark/grayscale/textmagic.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/grayscale/twilio.svg b/static/icons/dark/grayscale/twilio.svg new file mode 100644 index 000000000..055fcb079 --- /dev/null +++ b/static/icons/dark/grayscale/twilio.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/dark/grayscale/vonage.svg b/static/icons/dark/grayscale/vonage.svg new file mode 100644 index 000000000..4a790fce9 --- /dev/null +++ b/static/icons/dark/grayscale/vonage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/light/color/firebase.svg b/static/icons/light/color/firebase.svg new file mode 100644 index 000000000..d77c18648 --- /dev/null +++ b/static/icons/light/color/firebase.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/static/icons/light/color/mailgun.svg b/static/icons/light/color/mailgun.svg new file mode 100644 index 000000000..eb456be0a --- /dev/null +++ b/static/icons/light/color/mailgun.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/color/mqtt.svg b/static/icons/light/color/mqtt.svg new file mode 100644 index 000000000..f116bda13 --- /dev/null +++ b/static/icons/light/color/mqtt.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/light/color/msg91.svg b/static/icons/light/color/msg91.svg new file mode 100644 index 000000000..8b660c473 --- /dev/null +++ b/static/icons/light/color/msg91.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/light/color/sendgrid.svg b/static/icons/light/color/sendgrid.svg new file mode 100644 index 000000000..62fb7378b --- /dev/null +++ b/static/icons/light/color/sendgrid.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/static/icons/light/color/telesign.svg b/static/icons/light/color/telesign.svg new file mode 100644 index 000000000..86427534d --- /dev/null +++ b/static/icons/light/color/telesign.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/light/color/textmagic.svg b/static/icons/light/color/textmagic.svg new file mode 100644 index 000000000..6bfc246c5 --- /dev/null +++ b/static/icons/light/color/textmagic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/icons/light/color/twilio.svg b/static/icons/light/color/twilio.svg new file mode 100644 index 000000000..866c3aa77 --- /dev/null +++ b/static/icons/light/color/twilio.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/light/color/vonage.svg b/static/icons/light/color/vonage.svg new file mode 100644 index 000000000..12a89b738 --- /dev/null +++ b/static/icons/light/color/vonage.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/icons/light/grayscale/firebase.svg b/static/icons/light/grayscale/firebase.svg new file mode 100644 index 000000000..e1158a544 --- /dev/null +++ b/static/icons/light/grayscale/firebase.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/light/grayscale/mailgun.svg b/static/icons/light/grayscale/mailgun.svg new file mode 100644 index 000000000..7f7b7a5a1 --- /dev/null +++ b/static/icons/light/grayscale/mailgun.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/grayscale/mqtt.svg b/static/icons/light/grayscale/mqtt.svg new file mode 100644 index 000000000..d2189eeb4 --- /dev/null +++ b/static/icons/light/grayscale/mqtt.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/icons/light/grayscale/msg91.svg b/static/icons/light/grayscale/msg91.svg new file mode 100644 index 000000000..321b9e02d --- /dev/null +++ b/static/icons/light/grayscale/msg91.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/grayscale/sendgrid.svg b/static/icons/light/grayscale/sendgrid.svg new file mode 100644 index 000000000..189d454d0 --- /dev/null +++ b/static/icons/light/grayscale/sendgrid.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/static/icons/light/grayscale/telesign.svg b/static/icons/light/grayscale/telesign.svg new file mode 100644 index 000000000..4bd48f242 --- /dev/null +++ b/static/icons/light/grayscale/telesign.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/light/grayscale/textmagic.svg b/static/icons/light/grayscale/textmagic.svg new file mode 100644 index 000000000..cc3ac7245 --- /dev/null +++ b/static/icons/light/grayscale/textmagic.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/grayscale/twilio.svg b/static/icons/light/grayscale/twilio.svg new file mode 100644 index 000000000..055fcb079 --- /dev/null +++ b/static/icons/light/grayscale/twilio.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/light/grayscale/vonage.svg b/static/icons/light/grayscale/vonage.svg new file mode 100644 index 000000000..4379fc950 --- /dev/null +++ b/static/icons/light/grayscale/vonage.svg @@ -0,0 +1,4 @@ + + + + From 9f3c3b176f5ded80425e82a2cfe3f30cbd8e3812 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 20:04:39 -0800 Subject: [PATCH 005/187] Update oauth providers key since it was changed in the backend --- src/lib/stores/oauth-providers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index c5752e8ac..9e84290f1 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -24,7 +24,9 @@ export type Providers = { const setProviders = (project: Models.Project): Provider[] => { return ( - project?.providers.map((n) => { + // TODO: Remove @ts-expect-error when SDK is updated + // @ts-expect-error SDK needs to be updated + project?.oAuthProviders.map((n) => { const p = n as Models.Provider & { key: string }; let docs: Provider['docs']; let icon: Provider['icon'] = p.key.toLowerCase(); From ade620c9a5b30d12eef8e202e0564f3373e70f37 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 19 Dec 2023 16:13:47 -0800 Subject: [PATCH 006/187] Temporarily fix undefined billing limit Since the 1.5.x branch hasn't been updated with the billing changes, this change adds a fallback so that the console still functions. --- src/lib/layout/containerHeader.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/layout/containerHeader.svelte b/src/lib/layout/containerHeader.svelte index 08a7813f1..4c348b81a 100644 --- a/src/lib/layout/containerHeader.svelte +++ b/src/lib/layout/containerHeader.svelte @@ -34,8 +34,15 @@ let showDropdown = false; + // TODO: remove the default billing limits when backend is updated with billing code const { bandwidth, documents, storage, users, executions } = ($organization ?? {}) - .billingLimits; + .billingLimits ?? { + bandwidth: 1, + documents: 1, + storage: 1, + users: 1, + executions: 1 + }; const limitedServices = [ { name: 'bandwidth', value: bandwidth }, { name: 'documents', value: documents }, From a99082e4a5bb431a6d54a109f59a2cd80e50f5ee Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 20 Oct 2023 11:53:16 -0700 Subject: [PATCH 007/187] Add messaging overview route --- src/lib/commandCenter/commands.ts | 1 + .../messaging/+layout.svelte | 62 +++++ .../project-[project]/messaging/+layout.ts | 10 + .../project-[project]/messaging/+page.svelte | 229 +++++++++++++++++ .../project-[project]/messaging/+page.ts | 72 ++++++ .../messaging/breadcrumbs.svelte | 22 ++ .../project-[project]/messaging/create.svelte | 73 ++++++ .../messaging/failedModal.svelte | 21 ++ .../project-[project]/messaging/header.svelte | 44 ++++ .../messaging/messageStatusPill.svelte | 20 ++ .../messaging/provider.svelte | 96 +++++++ .../messaging/providerType.svelte | 59 +++++ .../project-[project]/messaging/store.ts | 240 ++++++++++++++++++ 13 files changed, 949 insertions(+) create mode 100644 src/routes/console/project-[project]/messaging/+layout.svelte create mode 100644 src/routes/console/project-[project]/messaging/+layout.ts create mode 100644 src/routes/console/project-[project]/messaging/+page.svelte create mode 100644 src/routes/console/project-[project]/messaging/+page.ts create mode 100644 src/routes/console/project-[project]/messaging/breadcrumbs.svelte create mode 100644 src/routes/console/project-[project]/messaging/create.svelte create mode 100644 src/routes/console/project-[project]/messaging/failedModal.svelte create mode 100644 src/routes/console/project-[project]/messaging/header.svelte create mode 100644 src/routes/console/project-[project]/messaging/messageStatusPill.svelte create mode 100644 src/routes/console/project-[project]/messaging/provider.svelte create mode 100644 src/routes/console/project-[project]/messaging/providerType.svelte create mode 100644 src/routes/console/project-[project]/messaging/store.ts diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts index f12bbe2ed..470a61012 100644 --- a/src/lib/commandCenter/commands.ts +++ b/src/lib/commandCenter/commands.ts @@ -17,6 +17,7 @@ const groups = [ 'platforms', 'databases', 'functions', + 'messaging', 'storage', 'domains', 'webhooks', diff --git a/src/routes/console/project-[project]/messaging/+layout.svelte b/src/routes/console/project-[project]/messaging/+layout.svelte new file mode 100644 index 000000000..2b8b30178 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/+layout.svelte @@ -0,0 +1,62 @@ + + + + Messaging - Appwrite + + + diff --git a/src/routes/console/project-[project]/messaging/+layout.ts b/src/routes/console/project-[project]/messaging/+layout.ts new file mode 100644 index 000000000..7db345058 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/+layout.ts @@ -0,0 +1,10 @@ +import Breadcrumbs from './breadcrumbs.svelte'; +import Header from './header.svelte'; +import type { LayoutLoad } from './$types'; + +export const load: LayoutLoad = async () => { + return { + header: Header, + breadcrumbs: Breadcrumbs + }; +}; diff --git a/src/routes/console/project-[project]/messaging/+page.svelte b/src/routes/console/project-[project]/messaging/+page.svelte new file mode 100644 index 000000000..f2b675613 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/+page.svelte @@ -0,0 +1,229 @@ + + + +
    +
    + Messages +
    + +
    +
    + + +
    + + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + {#if data.messages.total} + + + d.$id)} /> + {#each $columns as column} + {#if column.show} + {column.title} + {/if} + {/each} + + + {#each data.messages.messages as message} + + + + {#each $columns as column (column.id)} + {#if column.show} + {#if column.id === '$id'} + {#key $columns} + + {message.$id} + + {/key} + {:else if column.id === 'message'} + + {#if message.providerType === ProviderTypes.Push} + {message.data.title} + {:else if message.providerType === ProviderTypes.Sms} + {message.data.content} + {:else if message.providerType === ProviderTypes.Email} + {message.data.subject} + {:else} + Invalid provider + {/if} + + {:else if column.id === 'providerType'} + + + + {:else if column.id === 'status'} + + { + e.preventDefault(); + errors = message.deliveryErrors; + showFailed = true; + }} /> + + {:else if column.type === 'datetime'} + + {#if !message[column.id]} + - + {:else} + {toLocaleDateTime(message[column.id])} + {/if} + + {:else} + + {message[column.id]} + + {/if} + {/if} + {/each} + + {/each} + + + + 0}> +
    +
    + {selected.length} +

    + + {selected.length > 1 ? 'messages' : 'message'} + + selected +

    +
    + +
    + + + +
    +
    +
    + + + + {:else if data.search && data.search != 'empty'} + +
    + Sorry, we couldn't find '{data.search}' +

    There are no messages that match your search.

    +
    +
    + + + +
    +
    + {:else} + + ($showCreate = true)} /> + {/if} +
    + + + + + diff --git a/src/routes/console/project-[project]/messaging/+page.ts b/src/routes/console/project-[project]/messaging/+page.ts new file mode 100644 index 000000000..25c1e1cb8 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/+page.ts @@ -0,0 +1,72 @@ +import { + View, + getLimit, + getPage, + getQuery, + getSearch, + getView, + pageToOffset +} from '$lib/helpers/load'; +import { CARD_LIMIT } from '$lib/constants'; +import type { PageLoad } from './$types'; +import { messages as data, providersById } from './store'; +import type { Message } from './store'; +import { Query } from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; +import { queries, queryParamToMap } from '$lib/components/filters/store'; + +export const load: PageLoad = async ({ url, route }) => { + const page = getPage(url); + const search = getSearch(url); + const view = getView(url, route, View.Grid); + const limit = getLimit(url, route, CARD_LIMIT); + const offset = pageToOffset(page, limit); + const query = getQuery(url); + + const parsedQueries = queryParamToMap(query || '[]'); + queries.set(parsedQueries); + + // TODO: remove when the API is ready with data + // This allows us to mock w/ data and when search returns 0 results + let messages: { messages: Message[]; total: number } = { messages: [], total: 0 }; + if (search === 'demo') { + messages = data; + } else { + const params = { + queries: [ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc(''), + ...parsedQueries.values() + ] + }; + + if (search) { + params['search'] = search; + } + + const response = await sdk.forProject.client.call( + 'GET', + new URL(sdk.forProject.client.config.endpoint + '/messaging/messages'), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + }, + params + ); + + messages = response; + } + + return { + offset, + limit, + search, + query, + page, + view, + messages, + providersById + }; +}; diff --git a/src/routes/console/project-[project]/messaging/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/breadcrumbs.svelte new file mode 100644 index 000000000..9e28bee67 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/breadcrumbs.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/create.svelte b/src/routes/console/project-[project]/messaging/create.svelte new file mode 100644 index 000000000..49a81af91 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/create.svelte @@ -0,0 +1,73 @@ + + + + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)}> + +
    + {:else} + + {/if} +
    + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/failedModal.svelte b/src/routes/console/project-[project]/messaging/failedModal.svelte new file mode 100644 index 000000000..6ee183ffd --- /dev/null +++ b/src/routes/console/project-[project]/messaging/failedModal.svelte @@ -0,0 +1,21 @@ + + + +
    +

    Some messages failed to send.

    +
    + +
    +
    + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/header.svelte b/src/routes/console/project-[project]/messaging/header.svelte new file mode 100644 index 000000000..9534915ab --- /dev/null +++ b/src/routes/console/project-[project]/messaging/header.svelte @@ -0,0 +1,44 @@ + + + + + Messaging + + + {#each tabs as tab} + + {tab.title} + + {/each} + + diff --git a/src/routes/console/project-[project]/messaging/messageStatusPill.svelte b/src/routes/console/project-[project]/messaging/messageStatusPill.svelte new file mode 100644 index 000000000..46b309abc --- /dev/null +++ b/src/routes/console/project-[project]/messaging/messageStatusPill.svelte @@ -0,0 +1,20 @@ + + + + {#if status === 'sent'} + + {:else if status === 'scheduled'} + + {/if} + + {status} + + diff --git a/src/routes/console/project-[project]/messaging/provider.svelte b/src/routes/console/project-[project]/messaging/provider.svelte new file mode 100644 index 000000000..5e2da923a --- /dev/null +++ b/src/routes/console/project-[project]/messaging/provider.svelte @@ -0,0 +1,96 @@ + + + + +{#if icon === ''} + Invalid provider +{:else} +
    + {#if !noIcon} +
    + {displayName} +
    + {/if} + + {displayName} + +
    +{/if} diff --git a/src/routes/console/project-[project]/messaging/providerType.svelte b/src/routes/console/project-[project]/messaging/providerType.svelte new file mode 100644 index 000000000..b7db1514f --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providerType.svelte @@ -0,0 +1,59 @@ + + + + +{#if text === ''} + Invalid provider type +{:else} +
    + {#if !noIcon} +
    +
    + {/if} + + {text} + +
    +{/if} diff --git a/src/routes/console/project-[project]/messaging/store.ts b/src/routes/console/project-[project]/messaging/store.ts new file mode 100644 index 000000000..6a08966c0 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/store.ts @@ -0,0 +1,240 @@ +import type { Column } from '$lib/helpers/types'; +import { Providers } from './provider.svelte'; +import { ProviderTypes } from './providerType.svelte'; +import { writable } from 'svelte/store'; + +export const showCreate = writable(false); + +export const columns = writable([ + { id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 }, + { id: 'description', title: 'Description', type: 'string', show: true, width: 140 }, + { id: 'message', title: 'Message', type: 'string', show: 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 } +]); + +// TODO: remove this when the SDK and API are ready +export type Message = { + $id: string; + $createdAt: string; + $updatedAt: string; + topics: string[]; + users: string[]; + targets: string[]; + scheduledAt: string; + deliveredAt: string; + deliveryErrors: string[]; + deliveredTo: number; + status: string; + providerType: ProviderTypes; + description: string; + data: { + content?: string; + body?: string; + subject?: string; + title?: string; + }; +}; + +// TODO: remove this when the SDK and API are ready +export const messages: { messages: Message[]; total: number } = { + messages: [ + { + $id: '637a40ba7a703e3936e1', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + description: 'Welcome', + providerType: ProviderTypes.Push, + topics: [], + users: ['user-1', 'user-2'], + targets: [], + scheduledAt: '2021-03-01T00:00:00.000Z', + deliveredAt: '2021-03-01T00:00:00.000Z', + deliveryErrors: [], + deliveredTo: 2, + status: 'sent', + data: { + title: 'Welcome to the Cloud', + body: 'Detailed body' + } + }, + { + $id: '637a40ba7a703e3936e2', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + description: 'Public beta announcement', + providerType: ProviderTypes.Sms, + topics: [], + users: ['user-1', 'user-2'], + targets: [], + scheduledAt: '2021-03-01T00:00:00.000Z', + deliveredAt: '2021-03-01T00:00:00.000Z', + deliveryErrors: [], + deliveredTo: 2, + status: 'scheduled', + data: { + content: 'Cloud is live on public beta' + } + }, + { + $id: '637a40ba7a703e3936e3', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + description: 'Welcome', + providerType: ProviderTypes.Email, + topics: [], + users: ['user-1', 'user-2'], + targets: [], + scheduledAt: '', + deliveredAt: '', + deliveryErrors: [], + deliveredTo: 2, + status: 'draft', + data: { + subject: 'Welcome to the Cloud', + content: 'Detailed content' + } + } + ], + total: 3 +}; + +// TODO: remove this when the SDK and API are ready +export type Provider = { + $id: string; + $createdAt: string; + $updatedAt: string; + name: string; + provider: Providers; + default: boolean; + enabled: boolean; + type: ProviderTypes; + credentials: object; + options: object; +}; + +// TODO: remove this when the SDK and API are ready +export const providersById: { [providerId: string]: Provider } = { + '637a40ba7a703e3936e1': { + $id: '637a40ba7a703e3936e1', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Firebase', + provider: Providers.FCM, + type: ProviderTypes.Push, + default: false, + enabled: true, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e2': { + $id: '637a40ba7a703e3936e2', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My APNS', + provider: Providers.APNS, + type: ProviderTypes.Push, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e3': { + $id: '637a40ba7a703e3936e3', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My MQTT', + provider: Providers.MQTT, + type: ProviderTypes.Push, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e4': { + $id: '637a40ba7a703e3936e4', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Sendgrid', + provider: Providers.Sendgrid, + type: ProviderTypes.Email, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e5': { + $id: '637a40ba7a703e3936e5', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Mailgun', + provider: Providers.Mailgun, + type: ProviderTypes.Email, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e6': { + $id: '637a40ba7a703e3936e6', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Telesign', + provider: Providers.Telesign, + type: ProviderTypes.Sms, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e7': { + $id: '637a40ba7a703e3936e7', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Msg91', + provider: Providers.Msg91, + type: ProviderTypes.Sms, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e8': { + $id: '637a40ba7a703e3936e8', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Textmagic', + provider: Providers.Textmagic, + type: ProviderTypes.Sms, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936e9': { + $id: '637a40ba7a703e3936e9', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Vonage', + provider: Providers.Vonage, + type: ProviderTypes.Sms, + default: false, + enabled: false, + credentials: {}, + options: {} + }, + '637a40ba7a703e3936f0': { + $id: '637a40ba7a703e3936f0', + $createdAt: '2021-08-31T12:00:00.000Z', + $updatedAt: '2021-08-31T12:00:00.000Z', + name: 'My Twilio', + provider: Providers.Twilio, + type: ProviderTypes.Sms, + default: false, + enabled: false, + credentials: {}, + options: {} + } +}; From 7532b8e36f52ea3f39b1954ffed747eadca9a996 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Mon, 20 Nov 2023 13:51:23 -0800 Subject: [PATCH 008/187] Update input elments to support popovers --- src/lib/components/drop.svelte | 7 ++++++- src/lib/elements/forms/inputDomain.svelte | 23 +++++++++++++++++++++- src/lib/elements/forms/inputEmail.svelte | 23 +++++++++++++++++++++- src/lib/elements/forms/inputFile.svelte | 24 +++++++++++++++++++++-- src/lib/elements/forms/inputText.svelte | 23 +++++++++++++++++++++- 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/lib/components/drop.svelte b/src/lib/components/drop.svelte index d6c899df2..f61ff6a24 100644 --- a/src/lib/components/drop.svelte +++ b/src/lib/components/drop.svelte @@ -13,6 +13,7 @@ export let noStyle = false; export let fullWidth = false; export let fixed = false; + export let display = 'block'; const dispatch = createEventDispatcher<{ blur: undefined; @@ -100,7 +101,11 @@ -
    +
    diff --git a/src/lib/elements/forms/inputDomain.svelte b/src/lib/elements/forms/inputDomain.svelte index 58f066199..5445b2ccb 100644 --- a/src/lib/elements/forms/inputDomain.svelte +++ b/src/lib/elements/forms/inputDomain.svelte @@ -1,6 +1,7 @@ + + +
    +
    + Providers +
    + +
    +
    + + +
    + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + {#if data.providers.total} + + + d.$id)} /> + {#each $columns as column} + {#if column.show} + {column.title} + {/if} + {/each} + + + {#each data.providers.providers as provider (provider.$id)} + + + {#each $columns as column} + {#if column.show} + {#if column.id === '$id'} + {#key $columns} + + {provider.$id} + + {/key} + {:else if column.id === 'provider'} + + + + {:else if column.id === 'type'} + + + + {:else if column.id === 'enabled'} + + + {#if provider.enabled} + + {/if} + + {provider.enabled ? 'enabled' : 'disabled'} + + + + {:else} + + {provider[column.id]} + + {/if} + {/if} + {/each} + + {/each} + + + + + {:else if data.search} + +
    + Sorry, we couldn't find '{data.search}' +

    There are no providers that match your search.

    +
    + +
    + {:else} + + + {/if} +
    + + diff --git a/src/routes/console/project-[project]/messaging/providers/+page.ts b/src/routes/console/project-[project]/messaging/providers/+page.ts new file mode 100644 index 000000000..ac98e7a7c --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/+page.ts @@ -0,0 +1,75 @@ +import { Query } from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; +import { + View, + getLimit, + getPage, + getQuery, + getSearch, + getView, + pageToOffset +} from '$lib/helpers/load'; +import { PAGE_LIMIT } from '$lib/constants'; +import { providersById, type Provider } from '../store'; +import { queries, queryParamToMap } from '$lib/components/filters/store'; + +const providers = Object.values(providersById); + +let data: { providers: Provider[]; total: number } = { + providers: [...providers], + total: providers.length +}; + +export const load = async ({ url, route }) => { + const page = getPage(url); + const search = getSearch(url); + const view = getView(url, route, View.Grid); + const limit = getLimit(url, route, PAGE_LIMIT); + const offset = pageToOffset(page, limit); + const query = getQuery(url); + + const parsedQueries = queryParamToMap(query || '[]'); + queries.set(parsedQueries); + + // TODO: get rid of demo data + let providers: { providers: Provider[]; total: number } = { providers: [], total: 0 }; + if (search == 'demo') { + providers = data; + } else { + const params = { + queries: [ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc(''), + ...parsedQueries.values() + ] + }; + + if (search) { + params['search'] = search; + } + + const response = await sdk.forProject.client.call( + 'GET', + new URL(sdk.forProject.client.config.endpoint + '/messaging/providers'), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + }, + params + ); + + providers = response; + } + + return { + offset, + limit, + search, + query, + page, + view, + providers: providers + }; +}; diff --git a/src/routes/console/project-[project]/messaging/providers/create.svelte b/src/routes/console/project-[project]/messaging/providers/create.svelte new file mode 100644 index 000000000..65528f2c0 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/create.svelte @@ -0,0 +1,68 @@ + + + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)} + > +
    + {:else} + + {/if} +
    + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/providers/store.ts b/src/routes/console/project-[project]/messaging/providers/store.ts new file mode 100644 index 000000000..290483e9e --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/store.ts @@ -0,0 +1,12 @@ +import { writable } from 'svelte/store'; +import type { Column } from '$lib/helpers/types'; + +export let showCreate = writable(false); + +export const columns = writable([ + { id: '$id', title: 'Provider ID', type: 'string', show: true }, + { id: 'name', title: 'Name', type: 'string', show: true }, + { id: 'provider', title: 'Provider', type: 'string', show: true }, + { id: 'type', title: 'Type', type: 'string', show: true }, + { id: 'enabled', title: 'Status', type: 'boolean', show: true } +]); From 16cc91893a16255ae2b6f37d03a044f88a324573 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 25 Oct 2023 17:00:47 -0700 Subject: [PATCH 010/187] Add wizard for creating provider --- src/lib/actions/analytics.ts | 5 +- src/lib/components/labelCard.svelte | 10 + .../messaging/providers/+page.svelte | 59 +-- .../messaging/providers/+page.ts | 2 +- .../messaging/providers/create.svelte | 316 ++++++++++++--- .../providers/createProviderDropdown.svelte | 49 +++ .../messaging/providers/store.ts | 369 ++++++++++++++++++ .../providers/wizard/configure.svelte | 133 +++++++ .../providers/wizard/provider.svelte | 161 ++++++++ .../messaging/providers/wizard/store.ts | 103 +++++ static/images/apns-bundle-id.png | Bin 0 -> 42772 bytes 11 files changed, 1128 insertions(+), 79 deletions(-) create mode 100644 src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/wizard/store.ts create mode 100644 static/images/apns-bundle-id.png diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 8a15993fc..2b7145796 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -287,5 +287,8 @@ export enum Submit { SmsResetTemplate = 'submit_sms_reset_template', SmsUpdateInviteTemplate = 'submit_sms_update_invite_template', SmsUpdateLoginTemplate = 'submit_sms_update_login_template', - SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template' + SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template', + MessagingProviderCreate = 'submit_messaging_provider_create', + MessagingProviderDelete = 'submit_messaging_provider_delete', + MessagingProviderUpdate = 'submit_messaging_provider_update' } diff --git a/src/lib/components/labelCard.svelte b/src/lib/components/labelCard.svelte index 9d30f9b0c..461e5a53f 100644 --- a/src/lib/components/labelCard.svelte +++ b/src/lib/components/labelCard.svelte @@ -1,5 +1,7 @@ @@ -47,10 +46,7 @@
    Providers
    - +
    @@ -63,10 +59,7 @@ hideView allowNoColumns showColsTextMobile /> - +
    {#if data.providers.total} - - - d.$id)} /> - {#each $columns as column} - {#if column.show} - {column.title} - {/if} - {/each} - - - {#each data.providers.providers as provider (provider.$id)} - - - {#each $columns as column} - {#if column.show} - {#if column.id === '$id'} - {#key $columns} - - {provider.$id} - - {/key} - {:else if column.id === 'provider'} - - - - {:else if column.id === 'type'} - - - - {:else if column.id === 'enabled'} - - - {#if provider.enabled} - - {/if} - - {provider.enabled ? 'enabled' : 'disabled'} - - - - {:else} - - {provider[column.id]} - - {/if} - {/if} - {/each} - - {/each} - - + { +export const load = async ({ depends, url, route }) => { + depends(Dependencies.MESSAGING_PROVIDERS); + const page = getPage(url); const search = getSearch(url); const view = getView(url, route, View.Grid); diff --git a/src/routes/console/project-[project]/messaging/providers/table.svelte b/src/routes/console/project-[project]/messaging/providers/table.svelte new file mode 100644 index 000000000..2921c40ec --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/table.svelte @@ -0,0 +1,170 @@ + + + + + d.$id)} /> + {#each $columns as column} + {#if column.show} + {column.title} + {/if} + {/each} + + + {#each data.providers.providers as provider (provider.$id)} + + + {#each $columns as column} + {#if column.show} + {#if column.id === '$id'} + {#key $columns} + + {provider.$id} + + {/key} + {:else if column.id === 'provider'} + + + + {:else if column.id === 'type'} + + + + {:else if column.id === 'enabled'} + + + {#if provider.enabled} + + {/if} + + {provider.enabled ? 'enabled' : 'disabled'} + + + + {:else} + + {provider[column.id]} + + {/if} + {/if} + {/each} + + {/each} + + + + 0}> +
    +
    + {selectedIds.length} +

    + + {selectedIds.length > 1 ? 'providers' : 'provider'} + + selected +

    +
    + +
    + + +
    +
    +
    + + +

    + Are you sure you want to delete {selectedIds.length} + {selectedIds.length > 1 ? 'providers' : 'provider'}? +

    + + + + +
    From 37a5d11bdaf2721671bc040434318e3304c3be50 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 08:44:12 -0800 Subject: [PATCH 014/187] Update InputCheckbox to properly show label and description --- src/lib/elements/forms/inputCheckbox.svelte | 46 ++++++++++----------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/lib/elements/forms/inputCheckbox.svelte b/src/lib/elements/forms/inputCheckbox.svelte index 4aaf52c6b..0493d3f0b 100644 --- a/src/lib/elements/forms/inputCheckbox.svelte +++ b/src/lib/elements/forms/inputCheckbox.svelte @@ -1,13 +1,10 @@ - {#if label} - - {/if} - -
    - -
    + {#if error} {error} {/if} From cd7e82453eb5a7a3b87e5d38031d62ea3bcb0761 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 08:48:36 -0800 Subject: [PATCH 015/187] Update InputRadio to properly show label and description --- src/lib/elements/forms/inputRadio.svelte | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lib/elements/forms/inputRadio.svelte b/src/lib/elements/forms/inputRadio.svelte index cb3db08ce..a5803fa34 100644 --- a/src/lib/elements/forms/inputRadio.svelte +++ b/src/lib/elements/forms/inputRadio.svelte @@ -3,12 +3,14 @@ export let label: string = null; export let showLabel = true; + // export let label: string; export let id: string; export let group: string; export let value: string; export let name: string; export let required = false; export let disabled = false; + export let fullWidth = false; let element: HTMLInputElement; let error: string; @@ -47,6 +49,24 @@ {/if} + {#if error} {error} From 5ef5986be4cd86418077f6b811fa3537484fc2dc Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 16:42:09 -0800 Subject: [PATCH 016/187] Create InputDate for only dates --- src/lib/elements/forms/inputDate.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/elements/forms/inputDate.svelte b/src/lib/elements/forms/inputDate.svelte index a49aa3c4e..380abe279 100644 --- a/src/lib/elements/forms/inputDate.svelte +++ b/src/lib/elements/forms/inputDate.svelte @@ -10,6 +10,8 @@ export let value = ''; export let required = false; export let nullable = false; + export let min: string | number | undefined = undefined; + export let max: string | number | undefined = undefined; export let disabled = false; export let readonly = false; export let autofocus = false; @@ -65,6 +67,8 @@ {readonly} {required} step=".001" + {min} + {max} autocomplete={autocomplete ? 'on' : 'off'} type="date" class="input-text" From 26707526d8eac17798008fc13ba1c10ebb4b5082 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 16:42:28 -0800 Subject: [PATCH 017/187] Create InputTime for only times --- src/lib/elements/forms/index.ts | 1 + src/lib/elements/forms/inputTime.svelte | 67 +++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/lib/elements/forms/inputTime.svelte diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts index 22f3688a2..9858ae0c1 100644 --- a/src/lib/elements/forms/index.ts +++ b/src/lib/elements/forms/index.ts @@ -29,3 +29,4 @@ export { default as Label } from './label.svelte'; export { default as InputProjectId } from './inputProjectId.svelte'; export { default as InputDate } from './inputDate.svelte'; export { default as InputDateRange } from './inputDateRange.svelte'; +export { default as InputTime } from './inputTime.svelte'; diff --git a/src/lib/elements/forms/inputTime.svelte b/src/lib/elements/forms/inputTime.svelte new file mode 100644 index 000000000..bc30b8745 --- /dev/null +++ b/src/lib/elements/forms/inputTime.svelte @@ -0,0 +1,67 @@ + + + + + +
    + +
    + {#if error} + {error} + {/if} +
    From 8b953d4ccd68a840813d9421a78a58da27df1f7d Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 29 Nov 2023 22:12:20 -0800 Subject: [PATCH 018/187] Add a modal for selecting user targets --- .../messaging/userTargetsModal.svelte | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 src/routes/console/project-[project]/messaging/userTargetsModal.svelte diff --git a/src/routes/console/project-[project]/messaging/userTargetsModal.svelte b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte new file mode 100644 index 000000000..3d9b4311c --- /dev/null +++ b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte @@ -0,0 +1,260 @@ + + + + + +

    Grant access to any authenticated or anonymous user.

    + + {#if Object.keys(userResultsById).length > 0} + + {#each Object.entries(userResultsById) as [userId, user]} + {@const selectedCount = user.targets.filter( + (target) => selected[target.$id] + ).length} + + + 0 && + user.targets.every((target) => targetsById[target.$id])} + checked={selectedCount > 0 && selectedCount == user.targets.length} + on:change={(event) => onUserSelection(event, userId)} /> + + + + + {#if user.name} + {user.name} + {:else if user.email} + {user.email} + {:else if user.phone} + {user.phone} + {:else} + {userId} + {/if} + + + + + ({selectedCount} targets) + + + {#each user.targets as target} +
    + onTargetSelection(event, target)}> + +
    + + {#if target.providerType !== ProviderTypes.Push} + {target.identifier} + {:else} + + {target.name} + {/if} +
    +
    +
    +
    + {/each} +
    +
    + {/each} +
    +
    +

    Total results: {totalResults}

    + +
    + {:else if search} + +
    +
    + Sorry we couldn't find "{search}" +

    There are no Users that match your search.

    +
    +
    + + +
    +
    +
    + {:else} + +
    +
    +

    + You have no users. Create a user to see them here. +

    +

    + Need a hand? Learn more in our + documentation. +

    +
    +
    +
    + {/if} + + + + +
    From 96dc9612c87f565bbe5eda8fd9c01569b45cc892 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Tue, 28 Nov 2023 16:43:37 -0800 Subject: [PATCH 019/187] Create a wizard to send email messages --- src/lib/actions/analytics.ts | 3 +- .../project-[project]/messaging/+page.svelte | 58 ++++--- .../project-[project]/messaging/create.svelte | 158 +++++++++++------- .../messaging/createMessageDropdown.svelte | 67 ++++++++ .../messaging/userTargetsModal.svelte | 8 +- .../messaging/wizard/emailFormList.svelte | 145 ++++++++++++++++ .../messaging/wizard/step1.svelte | 20 +++ .../messaging/wizard/step2.svelte | 99 +++++++++++ .../messaging/wizard/step3.svelte | 101 +++++++++++ .../messaging/wizard/store.ts | 52 ++++++ 10 files changed, 626 insertions(+), 85 deletions(-) create mode 100644 src/routes/console/project-[project]/messaging/createMessageDropdown.svelte create mode 100644 src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte create mode 100644 src/routes/console/project-[project]/messaging/wizard/step1.svelte create mode 100644 src/routes/console/project-[project]/messaging/wizard/step2.svelte create mode 100644 src/routes/console/project-[project]/messaging/wizard/step3.svelte create mode 100644 src/routes/console/project-[project]/messaging/wizard/store.ts diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 2b7145796..d985c85b7 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -290,5 +290,6 @@ export enum Submit { SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template', MessagingProviderCreate = 'submit_messaging_provider_create', MessagingProviderDelete = 'submit_messaging_provider_delete', - MessagingProviderUpdate = 'submit_messaging_provider_update' + MessagingProviderUpdate = 'submit_messaging_provider_update', + MessagingMessageCreate = 'submit_messaging_message_create' } diff --git a/src/routes/console/project-[project]/messaging/+page.svelte b/src/routes/console/project-[project]/messaging/+page.svelte index f2b675613..cecbe8af5 100644 --- a/src/routes/console/project-[project]/messaging/+page.svelte +++ b/src/routes/console/project-[project]/messaging/+page.svelte @@ -1,5 +1,4 @@ @@ -54,10 +50,7 @@
    Messages
    - +
    @@ -71,10 +64,7 @@ hideView allowNoColumns showColsTextMobile /> - +
    @@ -219,11 +209,35 @@ single href="https://appwrite.io/docs" target="message" - on:click={() => ($showCreate = true)} /> + on:click={() => ($showCreate = true)}> +
    + + Create your first message to get started. + +

    + Need a hand? Learn more in our documentation. +

    +
    +
    + + + + +
    + {/if} - - - diff --git a/src/routes/console/project-[project]/messaging/create.svelte b/src/routes/console/project-[project]/messaging/create.svelte index 49a81af91..3c9924a97 100644 --- a/src/routes/console/project-[project]/messaging/create.svelte +++ b/src/routes/console/project-[project]/messaging/create.svelte @@ -1,73 +1,111 @@ - - - + onDestroy(() => { + console.log('destroy'); + }); + + const stepsComponents: WizardStepsType = new Map(); + stepsComponents.set(1, { + label: 'Message', + component: Step1 + }); + stepsComponents.set(2, { + label: 'Targets', + component: Step2 + }); + stepsComponents.set(3, { + label: 'Schedule', + component: Step3 + }); + - {#if !showCustomId} -
    - (showCustomId = !showCustomId)}> - -
    - {:else} - - {/if} -
    - - - - -
    + diff --git a/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte b/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte new file mode 100644 index 000000000..48d69abfd --- /dev/null +++ b/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte @@ -0,0 +1,67 @@ + + + + + + + + {#each Object.entries(providers) as [type, option]} + { + if ( + type !== ProviderTypes.Email && + type !== ProviderTypes.Sms && + type !== ProviderTypes.Push + ) + return; + $providerType = type; + $targetsById = {}; + const common = { + topics: [], + users: [], + targets: [] + }; + switch (type) { + case ProviderTypes.Email: + $messageParams[$providerType] = { + ...common, + subject: '', + content: '' + }; + break; + case ProviderTypes.Sms: + $messageParams[$providerType] = { + ...common, + content: '' + }; + break; + case ProviderTypes.Push: + $messageParams[$providerType] = { + ...common, + title: '', + body: '' + }; + break; + } + showCreateDropdown = false; + wizard.start(Create); + }}> + {option.name} + + {/each} + + diff --git a/src/routes/console/project-[project]/messaging/userTargetsModal.svelte b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte index 3d9b4311c..fad9a0022 100644 --- a/src/routes/console/project-[project]/messaging/userTargetsModal.svelte +++ b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte @@ -86,7 +86,9 @@ userResultsById = {}; response.users.forEach((user) => { if (providerType !== null) { - user.targets = user.targets.filter((target) => target.providerType === providerType); + user.targets = user.targets.filter( + (target) => target.providerType === providerType + ); } userResultsById = { ...userResultsById, @@ -200,7 +202,9 @@
    + > {#if target.providerType !== ProviderTypes.Push} {target.identifier} {:else} diff --git a/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte new file mode 100644 index 000000000..950c749f8 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/emailFormList.svelte @@ -0,0 +1,145 @@ + + + + + + + +
    + + + + + + + + + + Enter the email address to which te test message will be +
    (selected = 'other')} + on:keyup|self={clickOnEnter} + role="button" + tabindex="0"> + +
    +
    +
    + + + + + +
    +
    + + + Enable the HTML mode if your message contains HTML tags. + + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)} + > +
    + {:else} + + {/if} +
    diff --git a/src/routes/console/project-[project]/messaging/wizard/step1.svelte b/src/routes/console/project-[project]/messaging/wizard/step1.svelte new file mode 100644 index 000000000..01c704873 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/step1.svelte @@ -0,0 +1,20 @@ + + + + Message + + + Create an {providers[$providerType].text} that will be displayed to your subscribers. Learn more + in our documentation. + + + diff --git a/src/routes/console/project-[project]/messaging/wizard/step2.svelte b/src/routes/console/project-[project]/messaging/wizard/step2.svelte new file mode 100644 index 000000000..60f734edf --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/step2.svelte @@ -0,0 +1,99 @@ + + + + Targets + + Select users to whom this message should be directed. + {#if targetIdsLength == 0} + +
    + +
    + Select recipients to get started +
    +
    +
    + {:else} +
    +
    + + Target + + + + {#each Object.entries($targetsById) as [targetId, target] (targetId)} + + +
    + {target.name ? target.name : target.identifier} +
    +
    + + +
    + +
    +
    +
    + {/each} +
    +
    + + + {/if} + + + diff --git a/src/routes/console/project-[project]/messaging/wizard/step3.svelte b/src/routes/console/project-[project]/messaging/wizard/step3.svelte new file mode 100644 index 000000000..4a62c75ef --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/step3.svelte @@ -0,0 +1,101 @@ + + + + Schedule + + Schedule the time you want your users to receive this message. Learn more in our + documentation. + +
    + + + +
    +
    + + {#if when === 'now'} + The message will be sent immediately + {:else if !dateTime || isNaN(dateTime.getTime())} + The message will be sent later + {:else} + The message will be sent at {dateTime.toLocaleString('en', formatOptions)} + {/if} + +
    + + diff --git a/src/routes/console/project-[project]/messaging/wizard/store.ts b/src/routes/console/project-[project]/messaging/wizard/store.ts new file mode 100644 index 000000000..6ec774cc8 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/store.ts @@ -0,0 +1,52 @@ +import { ProviderTypes } from '../providerType.svelte'; +import type { Target } from '../userTargetsModal.svelte'; +import { writable } from 'svelte/store'; + +export enum MessageStatuses { + DRAFT = 'draft', + PROCESSING = 'processing' +} + +export type MessageParams = { + messageId: string; + topics: string[]; + users: string[]; + targets: string[]; + description: string; + status: MessageStatuses; + scheduledAt?: string; +}; + +export type EmailMessageParams = MessageParams & { + subject: string; + content: string; + html: boolean; +}; + +export type SMSMessageParams = MessageParams & { + content: string; +}; + +export type PushMessageParams = MessageParams & { + title: string; + body: string; + data: Record; + action?: string; + icon?: string; + sound?: string; + color?: string; + tag?: string; + badge?: string; +}; + +export const providerType = writable(null); +export const targetsById = writable>({}); +export const messageParams = writable<{ + [ProviderTypes.Email]: Partial; + [ProviderTypes.Sms]: Partial; + [ProviderTypes.Push]: Partial; +}>({ + [ProviderTypes.Email]: null, + [ProviderTypes.Sms]: null, + [ProviderTypes.Push]: null +}); From fdb7dd1e9cf9838991641cf69bf6332bf542fd4c Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 29 Nov 2023 16:58:40 -0800 Subject: [PATCH 020/187] Add a message detail page for email messages --- src/lib/actions/analytics.ts | 3 +- src/lib/constants.ts | 3 +- .../message-[message]/+layout.svelte | 5 ++ .../messaging/message-[message]/+layout.ts | 33 +++++++++++ .../messaging/message-[message]/+page.svelte | 16 ++++++ .../message-[message]/breadcrumbs.svelte | 24 ++++++++ .../messaging/message-[message]/delete.svelte | 38 +++++++++++++ .../message-[message]/deleteModal.svelte | 56 +++++++++++++++++++ .../message-[message]/emailPreview.svelte | 33 +++++++++++ .../messaging/message-[message]/header.svelte | 17 ++++++ .../message-[message]/overview.svelte | 52 +++++++++++++++++ .../messaging/message-[message]/store.ts | 5 ++ .../messaging/providerType.svelte | 2 +- 13 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/+layout.ts create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/+page.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/delete.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/header.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/overview.svelte create mode 100644 src/routes/console/project-[project]/messaging/message-[message]/store.ts diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index d985c85b7..e9c127f9c 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -291,5 +291,6 @@ export enum Submit { MessagingProviderCreate = 'submit_messaging_provider_create', MessagingProviderDelete = 'submit_messaging_provider_delete', MessagingProviderUpdate = 'submit_messaging_provider_update', - MessagingMessageCreate = 'submit_messaging_message_create' + MessagingMessageCreate = 'submit_messaging_message_create', + MessagingMessageDelete = 'submit_messaging_message_delete' } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 33a02a160..c9868fdcd 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -48,7 +48,8 @@ export enum Dependencies { RUNTIMES = 'dependency:runtimes', CONSOLE_VARIABLES = 'dependency:console_variables', MESSAGING_PROVIDERS = 'dependency:messaging_providers', - MESSAGING_PROVIDER = 'dependency:messaging_provider' + MESSAGING_PROVIDER = 'dependency:messaging_provider', + MESSAGING_MESSAGE = 'dependency:messaging_message' } export const scopes: { diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte b/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte new file mode 100644 index 000000000..9d2f6426e --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte @@ -0,0 +1,5 @@ + + Message - Appwrite + + + diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts b/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts new file mode 100644 index 000000000..21e6f6aad --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/+layout.ts @@ -0,0 +1,33 @@ +import type { LayoutLoad } from './$types'; +import Breadcrumbs from './breadcrumbs.svelte'; +import Header from './header.svelte'; +import { sdk } from '$lib/stores/sdk'; +import { Dependencies } from '$lib/constants'; +import { error } from '@sveltejs/kit'; +import type { Message } from '../store'; + +export const load: LayoutLoad = async ({ params, depends }) => { + depends(Dependencies.MESSAGING_MESSAGE); + + try { + const response: Message = await sdk.forProject.client.call( + 'GET', + new URL( + `${sdk.forProject.client.config.endpoint}/messaging/messages/${params.message}` + ), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + } + ); + + return { + header: Header, + breadcrumbs: Breadcrumbs, + message: response + }; + } catch (e) { + throw error(e.code, e.message); + } +}; diff --git a/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte b/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte new file mode 100644 index 000000000..1867a5dac --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte @@ -0,0 +1,16 @@ + + + + + {#if $message.providerType === ProviderTypes.Email} + + {/if} + + diff --git a/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte new file mode 100644 index 000000000..caf1d7778 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/breadcrumbs.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte b/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte new file mode 100644 index 000000000..7d7a2fb77 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/delete.svelte @@ -0,0 +1,38 @@ + + + + Delete message +

    + The message will be permanently deleted, including all data associated with this message. + This action is irreversible. +

    + + + +
    + {$message.data.title ?? + $message.data.subject ?? + $message.data.content ?? + 'Message'} +
    +
    +

    + Last updated: {toLocaleDateTime($message.$updatedAt)} +

    +
    +
    + + + + +
    + + diff --git a/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte b/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte new file mode 100644 index 000000000..41988ae73 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/deleteModal.svelte @@ -0,0 +1,56 @@ + + + +

    Are you sure you want to delete this message?

    + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte new file mode 100644 index 000000000..153a14def --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/emailPreview.svelte @@ -0,0 +1,33 @@ + + + +
    + Preview +
    + + + + + + + + + + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/message-[message]/header.svelte b/src/routes/console/project-[project]/messaging/message-[message]/header.svelte new file mode 100644 index 000000000..f97cc9e21 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/header.svelte @@ -0,0 +1,17 @@ + + + + + + {$message.data.title ?? $message.data.subject ?? $message.data.content ?? 'Message'} + + {$message.$id} + + diff --git a/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte new file mode 100644 index 000000000..9dfb7add3 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/overview.svelte @@ -0,0 +1,52 @@ + + + +
    + + {providerType} + +
    + +
    +
    +

    Created: {toLocaleDateTime($message.$createdAt)}

    +

    Scheduled at: {toLocaleDateTime(scheduledAt)}

    +
    +
    + +
    +
    +
    + + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/message-[message]/store.ts b/src/routes/console/project-[project]/messaging/message-[message]/store.ts new file mode 100644 index 000000000..97b3c1c5d --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/store.ts @@ -0,0 +1,5 @@ +import { derived } from 'svelte/store'; +import { page } from '$app/stores'; +import type { Message } from '../store'; + +export const message = derived(page, ($page) => $page.data.message as Message); diff --git a/src/routes/console/project-[project]/messaging/providerType.svelte b/src/routes/console/project-[project]/messaging/providerType.svelte index b7db1514f..d5e43f9f5 100644 --- a/src/routes/console/project-[project]/messaging/providerType.svelte +++ b/src/routes/console/project-[project]/messaging/providerType.svelte @@ -49,7 +49,7 @@ class="avatar" class:is-size-large={size === 'l'} class:is-size-small={size === 's'}> -
    @@ -147,7 +140,7 @@ limit={data.limit} offset={data.offset} total={data.providers.total} /> - {:else if data.search} + {:else if data.search && data.search != 'empty'}
    Sorry, we couldn't find '{data.search}' @@ -159,11 +152,33 @@ {:else} - + +
    + + Create your first provider to get started. + +

    + Need a hand? Learn more in our documentation. +

    +
    +
    + + + + +
    +
    {/if} - - diff --git a/src/routes/console/project-[project]/messaging/providers/+page.ts b/src/routes/console/project-[project]/messaging/providers/+page.ts index ac98e7a7c..b55c53ff0 100644 --- a/src/routes/console/project-[project]/messaging/providers/+page.ts +++ b/src/routes/console/project-[project]/messaging/providers/+page.ts @@ -70,6 +70,6 @@ export const load = async ({ url, route }) => { query, page, view, - providers: providers + providers }; }; diff --git a/src/routes/console/project-[project]/messaging/providers/create.svelte b/src/routes/console/project-[project]/messaging/providers/create.svelte index 65528f2c0..d828be10c 100644 --- a/src/routes/console/project-[project]/messaging/providers/create.svelte +++ b/src/routes/console/project-[project]/messaging/providers/create.svelte @@ -1,68 +1,274 @@ - - - - {#if !showCustomId} -
    - (showCustomId = !showCustomId)} - > -
    - {:else} - - {/if} -
    - - - - -
    + diff --git a/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte b/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte new file mode 100644 index 000000000..e829f4927 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/createProviderDropdown.svelte @@ -0,0 +1,49 @@ + + + + + + + + {#each Object.entries(providers) as [type, option]} + { + if ( + type !== ProviderTypes.Email && + type !== ProviderTypes.Sms && + type !== ProviderTypes.Push + ) + return; + $providerType = type; + const p = Object.keys(providers[type].providers).shift(); + if (p && isValueOfStringEnum(Providers, p)) { + $provider = p; + } + showCreateDropdown = false; + wizard.start(Create); + }}> + {option.name} + + {/each} + + diff --git a/src/routes/console/project-[project]/messaging/providers/store.ts b/src/routes/console/project-[project]/messaging/providers/store.ts index 290483e9e..714db9d9a 100644 --- a/src/routes/console/project-[project]/messaging/providers/store.ts +++ b/src/routes/console/project-[project]/messaging/providers/store.ts @@ -1,5 +1,7 @@ import { writable } from 'svelte/store'; import type { Column } from '$lib/helpers/types'; +import { Providers } from '../provider.svelte'; +import { ProviderTypes } from '../providerType.svelte'; export let showCreate = writable(false); @@ -10,3 +12,370 @@ export const columns = writable([ { id: 'type', title: 'Type', type: 'string', show: true }, { id: 'enabled', title: 'Status', type: 'boolean', show: true } ]); + +type ProvidersMap = { + [key in ProviderTypes]: { + name: string; + text: string; + icon: string; + providers: { + [key in Providers]?: { + imageIcon: string; + title: string; + description: string; + configure: { + label: string; + name: string; + type: 'text' | 'phone' | 'email' | 'domain' | 'file' | 'switch'; + placeholder?: string; + description?: string; + popover?: string[]; + allowedFileExtensions?: string[]; + }[]; + }; + }; + }; +}; + +export const providers: ProvidersMap = { + [ProviderTypes.Sms]: { + name: 'SMS', + text: 'SMS', + icon: 'annotation', + providers: { + [Providers.Twilio]: { + imageIcon: 'twilio', + title: 'Twilio', + description: '', + configure: [ + { + label: 'Account SID', + name: 'accountSid', + type: 'text', + placeholder: 'Enter Account SID', + popover: [ + 'How to get the Account SID?', + 'Head to Twilio console -> Account info -> Account SID.' + ] + }, + { + label: 'Auth token', + name: 'authToken', + type: 'text', + placeholder: 'Enter Auth token', + popover: [ + 'How to get the Auth token?', + 'Head to Twilio console -> Account info -> Auth Token.' + ] + }, + { + label: 'Sender number', + name: 'from', + type: 'phone', + placeholder: 'Enter phone', + popover: [ + 'How to get sender number?', + 'Head to Twilio console -> Account info -> My Twilio phone number.', + 'If you have multiple Twilio phone numbers, you can select one as the default number.' + ] + } + ] + }, + [Providers.Msg91]: { + imageIcon: 'msg91', + title: 'MSG91', + description: '', + configure: [ + { + label: 'Auth key', + name: 'authKey', + type: 'text', + placeholder: 'Enter auth key', + popover: [ + 'How to get the Auth key?', + 'Create an account in MSG91.', + 'Click to open the Username dropdown -> Authkey -> Verify your mobile number -> Create Authkey.' + ] + }, + { + label: 'Sender ID', + name: 'senderId', + type: 'text', + placeholder: 'Enter sender ID', + popover: [ + 'How to create a Sender ID?', + 'Head to MSG91 dashboard -> SMS -> Sender ID -> Create sender ID.' + ] + }, + { + label: 'Sender number', + name: 'from', + type: 'phone', + placeholder: 'Enter phone' + } + ] + }, + [Providers.Telesign]: { + imageIcon: 'telesign', + title: 'Telesign', + description: '', + configure: [ + { + label: 'Username', + name: 'username', + type: 'text', + placeholder: 'Enter username' + }, + { + label: 'Password', + name: 'password', + type: 'text', + placeholder: 'Enter password' + }, + { + label: 'Sender number', + name: 'from', + type: 'phone', + placeholder: 'Enter phone' + } + ] + }, + [Providers.Textmagic]: { + imageIcon: 'textmagic', + title: 'Textmagic', + description: '', + configure: [ + { + label: 'API key', + name: 'apiKey', + type: 'text', + placeholder: 'Enter API key', + popover: [ + 'How to get the API key?', + 'Create an account in Textmagic.', + 'Head to TextMagic dashboard -> API Settings -> Add new API key.' + ] + }, + { + label: 'Username', + name: 'username', + type: 'text', + placeholder: 'Enter username' + }, + { + label: 'Sender number', + name: 'from', + type: 'phone', + placeholder: 'Enter phone' + } + ] + }, + [Providers.Vonage]: { + imageIcon: 'vonage', + title: 'Vonage', + description: '', + configure: [ + { + label: 'API key', + name: 'apiKey', + type: 'text', + placeholder: 'Enter API key', + popover: [ + 'How to get the API key?', + 'Create an account in Vonage.', + 'Head to Vonage dashboard and copy the API key.' + ] + }, + { + label: 'API secret', + name: 'apiSecret', + type: 'text', + placeholder: 'Enter API secret', + popover: [ + 'How to get the API secret?', + 'Head to Vonage dashboard and copy the API secret.' + ] + }, + { + label: 'Sender number', + name: 'from', + type: 'phone', + placeholder: 'Enter phone' + } + ] + } + } + }, + [ProviderTypes.Email]: { + name: 'Email', + text: 'emails', + icon: 'mail', + providers: { + [Providers.Mailgun]: { + imageIcon: 'mailgun', + title: 'Mailgun', + description: '', + configure: [ + { + label: 'API key', + name: 'apiKey', + type: 'text', + placeholder: 'Enter API key', + popover: [ + 'How to get the API key?', + 'Create an account in Mailgun.', + 'Head to Profile -> API Security -> Add new key.' + ] + }, + { + label: 'Domain', + name: 'domain', + type: 'domain', + placeholder: 'Enter domain', + popover: [ + 'How to create a domain?', + 'Head to Sending -> Domains -> Add new domain.', + 'Follow Mailgun instructions to verify the domain name.' + ] + }, + { + label: 'EU region', + name: 'isEuRegion', + type: 'switch', + description: + 'Enable the EU region setting if your domain is within the European Union.' + }, + { + label: 'Sender email', + name: 'from', + type: 'email', + placeholder: 'Enter email' + } + ] + }, + [Providers.Sendgrid]: { + imageIcon: 'sendgrid', + title: 'Sendgrid', + description: '', + configure: [ + { + label: 'API key', + name: 'apiKey', + type: 'text', + placeholder: 'Enter API key', + popover: [ + 'How to get the API key?', + 'Create an account in Mailgun.', + 'Head to Profile -> API Security -> Add new key.' + ] + }, + { + label: 'Sender email', + name: 'from', + type: 'email', + placeholder: 'Enter email' + } + ] + } + } + }, + [ProviderTypes.Push]: { + name: 'Push notification', + text: 'notifications', + icon: 'device-mobile', + providers: { + [Providers.FCM]: { + imageIcon: 'firebase', + title: 'FCM', + description: 'Firebase Cloud Messaging', + configure: [ + { + label: 'Server key (.json file)', + name: 'serverKey', + type: 'file', + allowedFileExtensions: ['json'], + placeholder: 'Enter server key', + popover: [ + 'How to get the FCM server key?', + 'Head to Project settings -> Service accounts -> Generate new private key.', + 'Generating the new key will result in the download of a JSON file.' + ] + } + ] + }, + [Providers.APNS]: { + imageIcon: 'apple', + title: 'APNS', + description: 'Apple Push Notification Service', + configure: [ + { + label: 'Team ID', + name: 'teamId', + type: 'text', + placeholder: 'Enter team ID', + popover: [ + 'How to get the team ID?', + 'Head to Apple Developer Member Center -> Membership details -> Team ID.' + ] + }, + { + label: 'Bundle ID', + name: 'bundleId', + type: 'text', + placeholder: 'Enter bundle ID', + popover: [ + 'How to get the bundle ID?', + 'Head to Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Identifiers.', + `
    +
    + Screenshot of Bundle ID in Apple +
    +
    +
    +
    +
    +
    ` + ] + }, + { + label: 'Authentication key ID', + name: 'authKeyId', + type: 'text', + placeholder: 'Enter key ID', + popover: [ + 'How to get the auth key ID?', + 'Head to Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Keys.', + 'Click on your key to view details.' + ] + }, + { + label: 'Auth key (.p8 file)', + name: 'authKey', + type: 'file', + allowedFileExtensions: ['p8'], + popover: [ + 'How to get the authentication key?', + 'Head to Apple Developer Member Center (under Program resources) -> Certificates, Identifiers & Profiles -> Keys.', + 'Create a key and give it a name. Enable the Apple Push Notifications service (APNS), and register your key.' + ] + } + ] + } + // [Providers.MQTT]: { + // imageIcon: 'mqtt', + // title: 'MQTT', + // description: 'Message Queuing Telemtry Transport' + // } + } + } +}; diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte new file mode 100644 index 000000000..7f1fce5d0 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte @@ -0,0 +1,133 @@ + + + + Configure + + Set up the credentials below to enable {providers[$providerType].providers[$provider].title} + for sending + {providers[$providerType].text}. + + + {#each inputs as input} + {#if input.type === 'text'} + + + {@html input.popover?.join('

    ')} +
    +
    + {:else if input.type === 'email'} + + +

    + {@html input.popover?.join('

    ')} +

    +
    +
    + {:else if input.type === 'domain'} + + +

    + {@html input.popover?.join('

    ')} +

    +
    +
    + {:else if input.type === 'phone'} + + +

    + {@html input.popover?.join('

    ')} +

    +
    +
    + {:else if input.type === 'file'} + + +

    + {@html input.popover?.join('

    ')} +

    +
    +
    + {:else if input.type === 'switch'} + + + {input.description} + + + {/if} + {/each} +
    + +

    Need a hand?

    + +
    +
    +
    +
    + Read the full guide in the documentation +
    +
    + +
    +
    +
    +
    + Invite a team member to complete this step +
    +
    +
    diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte b/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte new file mode 100644 index 000000000..1f9e238a1 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/wizard/provider.svelte @@ -0,0 +1,161 @@ + + + + Provider + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)} + > +
    + {:else} + + {/if} +

    + Select a provider you would like to enable for sending {providers[$providerType].text}. +

    +
    + {#each Object.entries(providers[$providerType].providers) as [value, option]} + + {option.title} + {#if option.description} + {option.description} + {/if} + + {/each} +
    +
    +
    diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/store.ts b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts new file mode 100644 index 000000000..9f47f8a41 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts @@ -0,0 +1,103 @@ +import type { Providers } from '../../provider.svelte'; +import type { ProviderTypes } from '../../providerType.svelte'; +import { writable } from 'svelte/store'; + +type ProviderParams = { + providerId: string; + name: string; + default: boolean; + enabled: boolean; +}; + +/** + * SMS providers + */ + +export type TwilioProviderParams = ProviderParams & { + accountSid: string; + authToken: string; + from: string; +}; + +export type Msg91ProviderParams = ProviderParams & { + from: string; + senderId: string; + authKey: string; +}; + +export type TelesignProviderParams = ProviderParams & { + from: string; + username: string; + password: string; +}; + +export type TextmagicProviderParams = ProviderParams & { + from: string; + username: string; + apiKey: string; +}; + +export type VonageProviderParams = ProviderParams & { + from: string; + apiKey: string; + apiSecret: string; +}; + +/** + * Email providers + */ + +export type MailgunProviderParams = ProviderParams & { + isEuRegion: boolean; + from: string; + apiKey: string; + domain: string; +}; + +export type SendgridProviderParams = ProviderParams & { + from: string; + apiKey: string; +}; + +/** + * Push providers + */ + +export type FCMProviderParams = ProviderParams & { + serverKey: string; +}; + +export type APNSProviderParams = ProviderParams & { + authKey: string; + authKeyId: string; + teamId: string; + bundleId: string; +}; + +export type MQTTProviderParams = ProviderParams & { + serverKey: string; +}; + +export const providerType = writable(null); +export const provider = writable(null); +export const providerParams = writable<{ + twilio: Partial; + msg91: Partial; + telesign: Partial; + textmagic: Partial; + vonage: Partial; + mailgun: Partial; + sendgrid: Partial; + fcm: Partial; + apns: Partial; +}>({ + twilio: null, + msg91: null, + telesign: null, + textmagic: null, + vonage: null, + mailgun: null, + sendgrid: null, + fcm: null, + apns: null +}); diff --git a/static/images/apns-bundle-id.png b/static/images/apns-bundle-id.png new file mode 100644 index 0000000000000000000000000000000000000000..90fde1eff20bccdf9a34bf5555c55483e8d4c40f GIT binary patch literal 42772 zcmdSBXH=7Ezc-2?pg}=Lkq(ZGQ~^Z+0coRxO7FcBdXo|aqz@n>pwcAt-U%f1P((#~ zCkde|(wmfkBm~Zt@!4m;AJ2z(t$lXZav4HMuKOzg@+)_YmWIkj2s?y|it6IiCy#Wf zsLn1?QJtwfM+aUppCyfh|IT|pG4`gSVz@&2cZLd{!3thHxe z5*1ZV0>iO2EfrN!>C;C_dN0mw%+P+&!*F!%_^qZeo=>hlN5g=wrn$n*@#yiX4ZYC> zSLkb6x@!|>K71$<&wm}6F|Q}~tjjP&nc=3K8hN4I>? zJ=yqtfz-jz&++@^Q0jNd$pm$S>)pS9rn*|dOu2ZhOC#*TgJb%MB`#%5m(*vDZW@0tjmc)9PvBofbg^eN&WuTxQ705w^-4T_9g9o-XB) zrC(t*;*2SSLiMn6?;{V?9i8CT^7maHz_y}9RL(nU`sM~f^jleJFctII>#9trZqlA!$&m3q6L_rvSN>F#&DGc8_sEw5^%OS&QL2jG#d z24#AJq~+|8TMccbEm_&+4DU}C=%1e$DmAm^4KjAj``##=9*=D`Epw9&raV3eFEBq@ zo2bd3flawN370$0r8?Fo!0$QP$ZofwHj#azu(i_SYo+AmLEAmlkUYwLra5w;T_u5g zD}w=CTF%d2Z(RfP3NCO(u%4T#6RDQLY!U+e%`3yC=Fs)oR&?J50gWg|F7{@}KWK$5 zL&$`ZG3TknxsZ^+qs7$EqF-Bvz~ze=?mhliL}oqej;%CpSo)An&ECm1R~PO#7j%fv zjpladResaG!_gj04hTFYA3j{n5FKd~dzVn>f|~@N3Egfz%rfnwVR|r}_2T>aDI_$O z8MZ1fsk*jJ*{NFNi-NZgF@~1-=UrS53tb7C#JkNoFP1a?T*2qpoxj3oWGxXy^hwQ~ z4>1cOVD1!qj1{TJ<4hXdxqMBaDHRs2lkea4zGTSP>$`lDX|C?m=77|kkFoiQ;=4zZ zmio6hmeM_7#$#!DOF6x)awoqQ-1e3R3_d(}r3T;r){K5>y!|bFptL>RaaYNvbJB}aI)-0z6;0K5Z(J@kBRaz)J#V;gTaxT=oFsCv|bP0V9qi2 zjE{(5GBGzNW4W6*p;jDvg6>)%kW+YkEOxlTm-#9+^ogSTGaou*E7&9}4^(RR^lp?U zHHkxfBLoeWUfiM$a^>SS@AA+lx`CuX1&K(yyL9z0Erq||9ZY*`1|IKpEd+ySLv?e( zqydSD-7$fXHKPaYv}foUIalb?v<{*~xqU8AwD@~VK`e1q<_i|XC1V?4NRh2YYLFlc zwsY)H4V3<2mvEjKFV)?g4czvP&|CR_Ctlbl(9o5b3hdVBT(PdF9m&i77Ht;J%-2H%cv?GuOLG|ueI z(yb~Tm$&6;8IH$q3GR9~hZ0t(w_?TGUddQW4DCvdj7C}@_XxU05pW;DyLU{!M&m+K zk*ZhEVQQNlUn`T0&a^m<6+I5J%s;o5OZ6Hq(!Sd(y!B~3ams7Iz~wYnG~8F4b@Toe zE~d#@5G-azwmVrYeZrR1MqsPqrudj(ArId#r#<}Hjj<%XAIpbN@qb_8*YN4E2q{WJ z#w1DSM~5>)t_eGS6YQO<^GR|jYgZ4Z^;I)0)#)U76j8+s%EReq}uW%9a~#u}A-Yzg?!mfX?E_h3?WS`;2^0yL(IGPkFGv^5Y>T^XqtrY_7WkEKgb9G~eH|e6sXd z=CFNs8(V4G&(QV6Ii<)g#`Tk-7KqDCvc#2q@3V`?wL`Tb?^|yVZEq)r(+Qzo46r1- z`7b6L7ES*^-0mIPRagg&y-(w;k{H9rWKFY7AX>D$ZP@6Z(12Y4l4~(-M2% zS!mPaXGPLbacEVL-);{-x=kn1X|1$_-)D~(@6Hal~)Y}9r_=;%#Jnt zx~M+R(Cl0mXOA6ThV_zc!7Slsw-Eh+iuhzd^=p9;76xzeZc%2le5@a8ooP39mS znJb5-Txnu-7Qb04*`u9UV6=yM5_pmQ-Z{C$LtIkDa#rA=MBtkOf_~Je*UZ9ZY)4{3 znC#&?oW;7b!9IGetlaBt(7ooZKchLni*F>63yL&@ukfjPrrm3p3q}WKrFy9?pPcTG zX?okGmX>Bxewb8(-Q2r~ktl(d0Doo~u;)_Ud-1R-;`K zslb(0!tx3fvl4Djl6#L#PI_LzxKUUVjN3&$SRhOKLx>-+q4LDcp9x-N+y2?|L7&9? zVAIc4QmqS@V$&{dPA91)Zrg!9^m4pJTiOEGq?r}~Qm8ln^T|h4ukzvn`EV{8Ij~Ej z?4$plQj@T&GhTfnVQ>kTD7h_A?3c2`w*<;4d}W7zy-`w1TvE?>QPf@U4(SnFxv;Z~;Y5vXa-rQ5G#E5pbKQ(~_0 z`A+ZatX7Z$)t@)yx&lg$kHf<-C0%Ra#B!dqxP^8KXW0+?_Bnc4fOq@y1#IL1DyL0z zbE<*q_}4;$J9(c~+RH8n)Kbz#vLLZ~C#h=_s|ryMyh&3@F}5bzvg;M7yeb5DsyWQq zs^|U9+*SR9j&#~wmD{0Lo;((xOMQ}R5#SU*w7)XK>k6MvIyyeuqXE_N`Sdn}N|Ug* zAX3yoAaQ?n%ji{x-%n+|c;A5p+q%UFd!XEwry} zn1Pv2xHyX+x1W-S+?!X6s&Sa-4Kl=Q(BKmM`)nfX>wXiRu4CdG1#gXk+ZZ(=u{7@0 z*;CyjwezHm1F}1MGi5>I;-j3CspF(O-+kLGF5T-^{wq$V^rWn1*AQP;x<2L6?#3kx zQ@htkeIze9whT5`!)C*r{JzQU59>0Bga;|#B}PObyXXgrM{YtZwgH$jebI*+G`^4W zwY8I1SBrx?!=;{sI;O!2Im1=ZyS}VJgLyPM&m8lYkT>+Iie!fY%0Ob^Kv%LRI5t8` z;FE=6cJ^9CozLoMiqq7iCADX`Z3onqKB@^VXO~7>PkF`eWBPT% z&Pv?T<$*))XSJx1Xr|W$8NUw-A$V-3V%?9xuDA{MhfA*A(&G!5>D=WeZj6U-@klN&Ka_) zje1G&F1Al|PT_ePVc_`J&qseG``6AH&Q!coo}m+9{RI+Glm+N;BGxTMu!^#@f?$LE zS87Ei@d~U(@QHihKq4|)UbT2)N<4Z`UfbG=lx1YMNrfiWzHfp+-&fNoHJz*u2DD5#gqYKBBOSX1~f`Oft&RebHq$BakoAyp;a+rQ&`yb~b}wv^@&Mc^&25F;p+_6v$x z<&*tSj+Unaw_90ywe}7*`BuB17+7hLCvWEe9SOmCm(xxD?~;oe>lmqh6m7wUXym&PybW`~@d4^%&MU7}9SKs@XplR9VP z=eZS^p|-zDX|`I-&aP4$siV35f4t*lyLKd~aL^9dvv|4Fshu7pG+e^iPk6goKQ$YMi3#I?puz=)?1IxF-#=hsf)h(j zy-o@)^HdU;2cQ|;Xk3U#-Be9!JiK>3Mj3+5IfLG6E@xOFG20oQF-cM8REGsF{JWO4 zncvB6lCBfCyIj~m+(;{0J`$V(i}?GLL(n=YH=zBGUvuW(edp0m#>yZ4NUV-VijMzK zVPW#my%75T(PAOrqhSB@Q33+Ru_AI1aRL6Lx>E_04P8ySC!hq)aRxk%H9sTi+bCh1}av>q%H>5IC-5>CG zaN_*YS~=qVnu;>pHHVDn$`iEVaCB)l4=sDrW}WA^Ps4#Apd)6yhvZT!J`H!kltw#C zh=;olCH>h7bLMJgJ^E$i(TPY`d&}GPq{Ia|LOJt+H5=@4ob*h(@My8U> zy}|YqqVNVmg(v7<>BngoL~F`IRjUl+;R^NFkZ(Ejk19?^->O5G+CB9Sa+QbNM)Dqy zuQIn!u`*?~zMqI^uWWE#8P2uF zUvCc8<8x55cu?l#Y&U0S(hmCc+iUynmT-oOzs zd_h{=k_ge#5N({NX6YXiru|gaMsyZ-eAkW;QX$Z@l@Hw~@UysaM=Aw#wMUX0n8B3^ z^y@J83o@*qkhU9iHKcED5I~u4jIe0k0A)*jT4~|s6YdxDVVnpOjW>j^bnJP8a@jbq zNl{kG_iZNolAa{w_U=PFm5Inm(R$^0PPT)|y&(k;Cb@mBc&B7MD?Y4Qob?g;qF~N) z!<0MvajU1Ur@ry!rppM{5l`nHFvIJw@r%rP&x5V(3qoQeLvb7}taVt_~l6j}`l5F?MttyQ4ik>U(^xZq5K)d(Q1X%(*kpqdwnb z_?zR~$XWhNY%CK^AZVBwWa2g+pW!iYTqSp3X7e3c*k*dd^@@r8o`6qSG1Xh*#Q@MI zdHIMdc+M+=V6YDGqV+2DEv`IYN5p(S{0;_TIR{DH8wDu{}XxlVfwCTfQXkDX4V7tdJv^@|4_FrCj9V~>NkXIp1r{_773n=JAZ^uB1uLbCE3)^FTv=_=}f=|4Aw zY1+|G>QtIY;8flzRFAX;1xkI-wg87hTis3+y&*28IpcN;03R=(+f_O5MZ-sx*mzCE z;v`o_Xj740<1fo$o0Fo$Myw@>;#kqG;~G{?lWFzmmej25$;@x-O5|v8zIhW)6cEp)W~x$#*2qU8m4V_O0v;D z(212#4_TO+6?b$M9}OsB9+g+0Wq8Eq%3f;?Voxs4C%0m-k?=~Hjj=cDr-Qf8_N<(W zg2vii!QJoAGCPEG-U}$Erc?Tub^_LP2ALeRsO>${&6oeqr=v2~NCC=76#O+!X}|d0 z@>95;k|cKjHda4NBCI>aT#cg43BY~8oGjnriWTn&IM{GXkUKD%Shoy5Dhw?6+gPtx zM)zk`wOxFn(ByVG@!8n9{2gUB{X<5`Ef&62nP7(R_s@b$e-=I-y}7S__o;cs7k0br z*Ps@qY#Z7tT+DqniP}bOAiNY(!pq&vE}sXG@sb`NA>K@(k7fO%i5fD38@_~*q6(B9 z+>%9kcDuY?Vf;m{M^@mLbi+WTI_R>(Wf%c^8v0#SGfaAicq0Jxw{9S4$Fh}ZVPoND zWpi-Qji8+tw1-vHBgS7oGxoIz@eS^6(;7=ma z+h?JrWeK&8?FdLfrvFmf!I-83^OL(jr=Q_=OfPl)2^Lyw8cO?zuTJufi~BhvG8mQ= zWdl8L72gZ9CEEEV$)|Ru#!~>0bL&kKGyo}+<*(5P49)Km!PPz;CRgEhrKQQes*6Px zber@NO6oUhlm{~R{Wn$2KNP8!Eq$flEJo&Bm?avvIlcU# zxjIhL##zQG2(z3BA1*y=Ja{NX5=;5C8}zzZAP6UJoaHZFq?=35U7Chy1$O0!%S;aX za3(&E3$~P)cz4i5*QPaW780B1!`V*j%Sow_I@Kp>ycM4p@V$Vj1>ohIt@J-ul)xOB zz^|yN#KnfxjlNwru{d`BOncUp{M`F?u^FCFNOY9 zBDh{*jQV@%aKe@ej--4MRp^61|C<0(H~!5e0ir3f_sdDqOUgSye148AczUHz>>iYY zn1Y9RXi>%QXa8I1rT-ly*#A#P`OL@4y<<&8(xG#S6VWbd-_&}#T*i5-0!P~ASI#yL z*ff1Z`AY_^@~JkwyNjJ*^Ekc|(B?B^5cq4V6V@5+?M7a5gH5R3oo@cUopOE(jf6GP z4XM|B)jUpKASCc3>JdeVo}G|_6f)mb_0w{Z6qPnhS8qeN`WAN13yf|1>&R*xb=zrR zu*?uK!PK2A-OzWa0WbNR$yEGiU_;}bnCdES+S2j3u`Rm^3qfr=AW&lV$1I!=?23gX z3-%aumk}Cf*5r71EBLYwA4-*EsI@ z$JMP#b+%?>Tr%+C7h%V-x+j5~Rp)d0YIjlsXQxmbSqK&rOxG!FUBpt&+)ae2QD4Ck z(tsrwPfYl)U;I7(Rl^NxZ`1Iw61nVbXITSL7g?+Ti!WU9<3_2r>IU*aqSA#lDQUN_ zGgW)j%0E|e+T}{h6q@B8s%FM?G7-{Wc`SDO#kN%JMvqnXCdpl7V~E4AZlvQQo_9{f zYrAr)cA$+L)sIB>FC99&X>w|HHMhu*x%Hv=rD|0fOC5^0O{cKaBiJO*XJK^v`l_2o%i^Fp3KI|_gClaA;g$&Qn<;+dIjaZ5xdhie znn0sNn8fDN5DSIB2ai$21ntt`i#?w7y65Q^6+%>4R$edlIh7BDZ*w);LS2kySzF3Y zJEGK#to`qJ9z)Q=tiv+Chnp2{5s(97Y23iejyBowj_PGu$!Z!{i7R?s&_j$1-8v%& zI|{igVA;!kq0O@0nT=!_J!xM!(3W0MmN96>hpWW}4k3ZIAd^3edZuCXyTBnHPQ!d)4sk>vv)Ksdy6DIJAd# z-e)R}vN-F;X;B(4-Rut-((Caw9k@Y!e$*mXGtqGJvkHbuZm!b8A0-4mO}E1Ht;^V@ z_y&0#gdk{fN_Y>AOo#!1?lL37kS}=K_(VFq(vOf3ef!U1F4n~_A9PhQ1lwoNl$p*g z)HfW&39DGu@Ht3IDFLgS`!3taVrK){z>)qv5O6jdd7wv-1iEzH`&XB<$JqCY6Q?oAs(0 zd`i`n^I;Hb_+V9`r+#*7@EMftKi^zpBmu3|DoPS*Ybm^#Svwlgxx%qIP$LXBM1w^% z2Gw^^xytPu{0C z)Jm2Dc|iyLV@CSvKwz{pbThwc-E41EuMjy+Ti(zZ#$#VO;aVq@TpvGBPW!N-(AD(1 z2l-!$8E7wxZIbWs*9oWA_sE$T_o_U?eF8{6`D?_ z2HgxlyPx>SB)1;;rOy>t7T!o+EDJ$a6MAIRJxMGAFG$74 z;%qqnOUMDatCRQp)zAz7mEfuVXXP9Jw=!3#_;2mZPXt?^B%T4BryvEfJL%}bXSGqu z8`vP1slq^9ZKiXI>iwhr!VV=^=vlL6~4`4ZeGc3?iwOqTaye zgPP;mC3@pQ>rk$pAN6lSdV5|s#kDr&@YtFuYw}AoJqPoGVP zCI)W0gItjyz4_y2dWC-Wd3%lq_V;>iwd!j#lO|Zvrj8~XmJjPQ=!RB67iiCw8`b1( zR)1#1);j6R9!`6^0S!dGTKXN^s5+0i*;)?Rq!T}hfU^_jh2A)FUObH`eYyj#OMcACe^(OV0-Fd?$i*1=$0Y#!KkQx}EVQzg4Zi!Mu z4;iKZ3038^3?d{m#1mV0zH&U~yWId#q3VSoGLc}t-9?hWWzjP6HGyaQghICf*@!y~ zKu#tAefvGu{k?2q*GH+D7rrIQl~gpn(1SfOe=62sGt))UMO8daPmdD7)Qw$o!@|X(3*A5Gq_MSWeCBf3dfR%WwKJ@idH{`}%E`EyPetqUfuXnL1xqnOo4%~iJGl&_YUgS&$k}VuD9`L0b&@>oAAKq-$Q!+G`U3E*ahg5SS(-G zxX2R$e2T^;3GM@N$RudkIVCHJ0UVO99~hP@jcPstBpSLoS?3)zmMmf_xppk&KJDny zN=S)gyZe=vG=7+h{6?Yb0s$)iP@NCcU|y_|IeqHMY5QM4sGWr$b0pwJW=x;UE?*X_rVRA0J zopbyG5#WI^5(#kc)rj_!$$Gb`rQV+anri?WgQ88;>b%4hSSYut=B}MCo^3krG=-41 zK!jy`Ft*9t!DY6k>EBZwdaRpA?VWW+wc43t}e z1dO5i&!_4quo+)mwf|tc)DUfNQra>A%QekVqfGL@KF)@m2I0)~u@%NwApNEI9{_n~ zA@&2_mIG8wW=*5C`7Oa8AM{e`DW@XIQ5V>3(3|z?f#!WjU}&F<$ls!Qrt=?0Gg^l= zO}iF{`^`3EOLe8+yGL9%xb@UIz&;yLAA>Ih@&hsS)9+asm6E z+4|Su4glM&a;-B5dK^s?`E*NmDfbP!xZ&pRrGa$w+rQv0Rd@c~jc-Gq*?oRa>wUb2 zDQ6*CvYBYg5%dI%3a_UrSMGyt!vn(_J4A|CK3L-!q3~Nlr$#h2eD(V@QC|)KI>czH*_$rD7`M8$#{iM9JQ&UeZ?F$?zV09yZ|i&sP}g-x?(l*- z3&NuT&`9|B2fI&LZj-=<-30rHG0<``@fP$S!A1dR_6;bPkm7Q7cEe!JN>Fkp^=pW| z;Qbe0F01@AYuWa)sgGLi&|yrSrm)1gI{II6W~h&GV?Zpq#`EDyjgL7|h7y7uy@I!m5c%1_xRHR~daEc8QZNv4!6b-A)eDOUdH#%Hw4edf9 zm7UxhztK_h9x3GKd`GTT9PzLj1n-Aj3*{ca(~C5rd`bIlmnMyxz=U3t7nGvNFy9-{ z6J!|7K3^Q%JwBJQQ9Gtlm9_!k^iA#nQRk>t4>}=8OKl(?^(71ynS$K%@KjiUB5kBv^;|Y&s3dSLJWQCx zow_G;lcv6kJ!vHv7BCf8)be*Bs&ACrOOp{5WQzDhy`rvE=6}94sZO^t2>* z`MRADv9l4ubwa5%CX1~84b=kJpnk!JqR1hM1h`h2U@^5XH}tuGOUK1yph4v%o`ThV zJ|Hf^L*CX-96s2biVhmgrcJ&jVyQ0I700ohB|K4_ld@DepGJH$Mg@vCFd+r<-`+1) zmeYNpaIR*O^8iYo{>bPoxvu?lWNtY#n@->-A{xoXbbp>I)t|rjyJ}SZ&|C(d=gl5+ z`QG(c0-=5r*3q4rmOoDPskBMmE+*wCYr6}m)g{6rzXkR$%0v`&!k}(_VQNF%`^Q|EW5h?dNTW(UH){Cp_A2X1k1NL_~JS7s=KZ}2YkHq0Zk`up=v)I^Tc^~-6JmmB%fy6&IKklp}RXk zbQ8z^VgFJ5itrlKFhf=8(=y`PUP66({7_LMb>0b;cP;tfCb%gDYCxRhgLAyBVV*!!VF1DraFk>Sq#EEVY?I@!~kGT6nJ>!g*Bfx#TdgkYd4M@D$<4w+2t)v+BA zR@M9KnJnlBOBFuo^%1$TA*!=BnFH<$znf)arr7V<#uSv z;X*sXDE;GYdtELJ{z@q$7GCtsLy!4@vWEb$$us1EzhYe--SCCbYH^6yzhl9!_SxAbBypc63&j zU)EG3tbr!FAGrrBv7ym$b$obgWNp|YIU}=KpNT0q`5F*sWTiCsekh%B`%!0RS}8Vm(`yaBWgZ#!#|Ki$gd?A zdaoT|e=%cMTk%-$1o**Xq!=zbv@ii zRvMo0QEY|5i=QZ`0WSlz4oskQYUEBI`fnNX z;F_M*pn_49<6j0I*DoqSfBB~c32na!la?qXBrKH$?E>BPKUw9*B?`P^U_4;Fcv?t@IYI0e;r@Vg19~AND^+dvDAr z7&ccl(YKr0(brZwC2%*irrUc(*h|_xyWM^0ad+rq$9hRV{Xq3|2HaG*l~$EO;i$pY zd$UzEeor)F);2ZqQ1$c^T(CK>g^+1qep~AUAZ4xr zO-Xc+#Tj;5?R~ABuN-eWc#PgrR9nQSy4Oa*EIE|wTvw$LTFHGQ2_*_DqJV1I2yo+{ zu=)c&hCt(P$>Yls`=+E7Q6ZkS2K_EOZy~^lXnZhGwu(!fK2Q*7{HRW_a>v} zD;z^aFjSu3J7w5VCs5lkzh}}okvVPu@aau5ha1{Xp5qoVGkhKP__k;BuUXjE&Y-cn zYtLk*J*JQCy`hcx(xWrNai0Q!b`QPGK0Xf&2*@s;kZHMXAbMrM`g5JSeCavpQ9A0G=oj<^udH|wbriuvHwUS~d3>58R9BU?VvsPERlihCl zAGww#-CToutu?-eJ#hYK4%Ewol$E*Ot|y?|+gw}jJ`^m8Q&zKZt6WQIFr9-tdH8rZ zIn>S$V%->x>ayCKoMsjs2}nsVY)H=^>)}dePh+K;<#8EA=FP-G*~uMEvRkXXrPU#~ z`P}PpKYM!*-G-U+sZi;*byQ~AgbKd|@=Ej|5??MkwOSd0t8|8=C1iUFg7SM04)cDf z)DCRkBojrZDs1rc(S50~2xX%t9fK~0mvn~E*)oxXQ4iJ4DnENRFT|I)guqFsVv0rQ z?;(07Zcg@N#V|CbZI#|#U1nB z19!a<1GkAaYPrRTj^f8I^+veEWxEC^GVbQ4%aq}Q!2~Kl1;1aIzVXZOv__6z-A~=B z@GER_qtQUWa%fe3qwUavJ|f7a;>m}mue03kpSEP1!#3LuMWnpkrU2$W6bm=*lj&F= zp!o^RSPSG(}%&kD5ir7rK=xYGyT=(qeNbf~R*pKAKj~U@QQnN2? zSB_FdOs7jSvg8h@#|Z63MQ`4nfzTp>L{k#T5}w2#9snN@%lHcLBXHCz1w{yccAozj z1{-FWbedeqeIO4w zi1II7bmTz$ZEwMcg}n8G*eML9;$-ElP31Q0Hn&5Y%)lg0$RYa_k?g|gKOdjGx&Ls@ zxR|AA@}RZL^e#c$pviS5Uz4+;)z{VVvnW$=6@GKUwHH2cu+SvC*nM{0*GNv*YRCk; z6WJr_@OXrkZNGx0xJhx*FZEvC$B4j*iLC**gV*ZVwVw9yGqF~lUSHW|R|#p@pzO(O zjp#VJ;Fk~4#C-pQ8HDP4_^D~T2wf)rfoJqB%)`G_MJ?>n0=G@P3r4t%9mx!O! z8y#&|Hf7~M*k-GlK*s`Vs|OOctc$lT24sR{q;8eG{(_|T-&Cw%)g2MoQ)hz$q|z(w zjU(4i`DsnEuwnBl0r*P3IiVmcj%RhCh#2HBMdq#=scf4GB>f~{n6qtoV@F3jELnoL zvHL0N1-Gle%awGw2v5(;LgCVU{B?ookt?zg%ixyCnJ^ywN*?ejf z=5Sb}Hc<^t?+X{c!n^P+LeBM^e3^c?TD*21a)i`mP@mNI3Z*`TjdjmtYyDQ3-9RKM3s2b;#da||$^s%*(20EPY0 z`(78NMNn9#T`UM99i#|B#UC@m-(GNg*pibWr7k{|#@K75r(oZyKLD2yY1rL0ri@Ww zP3SL2=h6=DmNPuBxoBIL_*PQo4L*vp`S;jNAw_mb{zi50<3F0QJL;)QTqFZXbHwiZG z|DyMyd@Ti9eaz$SRCHNI;poQ|Tmqb!shjQQd?R4=DSuPjH?93r|6PC^{ynq~V6f}i z6jnU+IHgB^34(sPX zUTrl#81C-08#IO$W%T|{dEaufE=PJYzl-S2j$N-sgYY{-6r%?f)w#49*TmM_0ckUb zdw-ys)+-*l81=Z%G}N0n`>9Wr+Bf}^sA8+18;ZD? z2tRMS=J4Ov(Hz0wpGx&V;V11ix+4xOZvSC~M@EoPRpC;A`Txgzo1-|AL=ho^$NxSv z?EhyrMUhR|-hL>{4M674PaU98V+v33@@O~rP=UC@YYHGHidQy;L)fsY*7|c49+WGa zpgySsu>tsv1=z6&OAm@k7RY6d0aAafy`-upfZ*&hS`ZVsg~(14F~!!;1qTZ23!RWh z9{IwjUsZIeWj(-C{EdL*NYDpvBrwBT)vwL1wg61r02;^0tn$%7Amg3{&B4aWDX6I2 zW?TH9(k@Zd#*#HDbgZH3x7trLuI_aqtYl^Pxot}^>62Y7#IEW50|@Y4@U>T4 zY}h{NUb!rBM?WzE6fooZAN5P)mVlF?UevsK0wDdaQZqh@lqj`DpwzFxhP$?ON%$20Usz-Z_OY{%~0Q}u4%h<4ClyR(5| zA_L8ncT~9jzW@YXzF`_5=o1w8Gyt-5xB%|DCE5o60Aka=_I2063~3n9b)q+pYHYir zW;vw23NpQV1tsnKSt;f*S%o=ZIj#p{$%0wUC~9j8zCU1;0Hm+z#S~LBZ+(oEO^ji& zcF=c#>C^)^@|cXz%DlTk+13uli*>YK2L#Jl%$>hX-U}aTm}-H6Yw}CXl|;BaQX`-E$<(8Dk=hk9D&aaYj8U}+ z+?2pwH~@Z2Z^9*spkzk^E5`6%+}bU$bkTKAV_#?Pk0bREPLs8z=$&(vM4^34_UYKn zPmi{vuY|O`BA&HSW4|WeI#>KC0d=End~M4M=x)t#>iZR};}F|I{bgTbg({k-D$&?k z7iWz;d7EZXA!P}_^`o>lMM7Zt3j)=Zq@bfmrFrGDb9uqrwcK)n2R@my<=;=obu|G) z|AdJa4!jsBt#&WqwDFNzyjnXNcSpNEZE(3cjc;#x$d7Oyisau5F4HS^1eA|Jz6ku0 zFFSN@1{g1Lm&;NG)(6TX%se}2-8oo9%~p|moy31tH*fsHG6GOn@wv;D82-V~yA>#6AlaTZG11GuITgXW76Gd_amNdQCxb{xsYeO*lvO+b zFc<<3HZvgbP&NX2hgLH}j)IM2fjI8a?2G^zfg^`W({6%g$sRt1ygZ6%lrbBD@FmUX%O+*w!+A7irlr4-BN(F-!>Jm~Q(E$}o7qFj8;LlwOmt9?4+> z;m>)3)a$HU+;7oon&$aV??}IR0J2885kS>>aLWUI^HWfD_ykK7_PhO_8+Z0L+@{@PS0zsi9e*#ZUOfos6wC@5)@`fAFKOd z{APhnf14P~@!&;u9Ea2(M)O&!Xy(s7=chS*Xhh5I>&(EdrUP&q8Y#Dn*j0&P>~|WD zRyg_fIbNRVh-+D>Kn+6TdWC?mE@^#ev1M=2o3|){T=%OqtqWk_8}YJRo@87}T8z+9 zGg{4C4~Pi2LAdfvbCN-jR*w-KoD|i!vV8KGhU@_0VX;*vyt1rnK6QosLa%uEq10JS zY9(@H+#wT}{8*;M$4MwJ-SdnpjTq3i*@bfLp$A2l$%M zPmyn=K`Q+2G(H-0h5uh>x6a8a?~wAl>Hn5#C<^{AS*Wz;|8L#615^P~!I>@x9amA` zPy@6z1?DuN!;K&F9t&<&UcB?Av!O_6pnft4W6V?p--Ze>v_;W+#e zTqULGeaCzbX;W~Duy6hPLBtY@x9+M4nHx56zf-O~u#qL#U*+hb7{(^$ol zs6FePF4B~OGfha* zTBm~A8`5ij)C#3vu#p&@u@?n)Z^dwL0aj6g!^6K^W{Tu2gJ%?*1qB+OFDRE=wzuh*>Ggoj9|KIr4wQlh+{)of1`Xt$%~)3k z93iAD>aI0-5+o)ZXcF`l@fvRHLoyh@&Y#| zSP5w@I=mNGgmDmm%u)sFui_xUl0Q#a2Ht$UrULng#uk;s+KPk|tZx6Cv{$L^9gQIUu6;$QtKA*c5EkO7m35UH~Z=Y54gaJAVUs6swb3YhxC z*+BHwxz1P?)1fT6O^SWF^$=k6lOVh}12M!!k-U)86VaRo>(6u~kcHg<2PoS8T|)oO zGQhR!!Ch-iGi7}L&?vV`Ordy<(FAQpFqy%kZzqb>(}#UPBt;RnW~S`>KYSyKQxpeo zU;*ku@nNd;1ey+_zFfIZ;cJTQMSD(6Xo_SH+2mEo5CHwom=c~X zvy>`gYEKEzKs4UfMqCq?oe!m%HkTM_HV0LiyF&RJfC>4se3AX}_Tls)9TkMvmrc`MFq5VGrClMelqJ8|J8Cye_>gjY0X$~h?Ep0H_IlDVW!dQfZiehA(Nid66(YS&by zEp8&Nl|STZ2ZhVVo4bS6@m#V|7P?j|A*ZD5)}y6#(TTjAzMtPdn}RS}%k_$FrZGEQm_P)wpk6~46dS(v+L)*@7)~nu_s%I4ErH~@NNSG& zG6=SQLzW=NxBx4~gx?}1JSw$-H{gCB#ogEHR)cYEYnl^yXUTa-)KVSNa{N?;ZZagS zsp3@GmEf~EiGiB_D+&oeR+82}wc^k43};}a_*z6`Rh2G)8+35V_*5TkUh%V_n6L4m zT>L&7lkDU-rY_Y))dh+oX82c^HasY_Sym{@h=*r{O0{ysNoD|s{#!|?b(=|9F!UEl zW|5V)_aGInzzGZkH@iVE_v(!8fzo|Wj^c@{1SNTuS}nP^KJw|9u1qa)S_66t7l0djK}!$s{ayOwP?F`JIv1=7$k&Nk z7GrC7gAL7nlc&TWRKhDIIW^S5;6T<3Q)B{D8uCYWQk!6}EXG?y>z#69K*c;nxl_-f zMN3dF5_@Onx{cigrKuQvu&0JoNDR+~M;|n0$4}WvM*Y@m(&kO<-(eYu8ZgZ};pwB~ z#5qCTqDH>T5rOX9LnydR?+^sQSyBayPnsSDMcvE`?V$)y!owg|rEK}gj$?9wV5rx8 z7=5!m_UctYfN5V+RcC7b_=;K(*tydfY6V~LLd z6+SC&V|78xDpvITZ7ElZ)Q2ZOsjI0l?c9eGf7wDdDz=&Sdl-aeX6$pJbn8AaQD_h zQUBlnD59W(Sil0Vg6SwJjw>29#7T^bf- zso#0MKi|)tJ9FpG{APZC+?mUN$SCV8&hwl{9j4{nnf#!$OgFwX*WitdZ|AV?l(tDX zBg9mH|4?SEB^gfcxPY^8I54q%PT3>Mwq@}3J^fwM+b(H=1Uo2>Qf`{lEY>N96a%h{ zzAYg}1y2wff}AXBTo2Y2)_*7NupE~EYc1vZgzd8=$36!0a^de?Wwckxj2xZCnPNuh z{-JXF+9{@&f}Xkct86)}5)^__A!wO4b(5FE`aL(#4&L#HQN(qR9@exfq$*yF>YQRJI~q!V_1^#%l3+(xV4Tc-dC+-5M|jk=P^|IU>kpB@ys zsGW@dA@C1F{Mr4tI%r;O(@TXMd}>yWhgYWUvG` z_wG&@2!xjHU`$+VSJ24=SdW131q$ZS!Q76#?BUE@%GE36v(HesH^fHR45zT# zaL;V5jHV^dWNF*;-FDk1cR^fKO~QZ6Q|jttRTFbNgIsws@wz%{)r&fw*?ChU=c(AV zvR{SZH!-+kCzCs0W>_XaU?K{nOl2RF=sjuJh*uU*j4!m?25>>w=!_w&g|N~>u-M+4 z+NYy=JU_8WJuR6^)9;Ikznz6oskqi4xLEG|MI(Wm_)Sf@+txJThZsAT`T%#nqTfDw zFrwd5toPi!(Nq>Tr&i;sr|^Q5!^W^fv?)zXnY$2ce%s1->Div^kBY4uZ}dOAUa~+5 zsGAJX-_qGLU&Gh+ z{FL1GE^HuCR&j_Pt^f7YrUrj}|IF7u&~WO~g=nH)yTfztP&F41j0=cTi zWu63b1x_~|Oz*bh-=3%>%??*O89&r{%*;R_2F5W8>FwHb?Q2Yba7AK!1-?_!&wnL` zle=k1(vhAy-+wPbdSiXRtvKD&_&50bxvD09+yXg4vdx9H6Usie==@ zD^~{>z8PcNo~j7#c-d=?4|Nvqb7?1#tdIE*IpU6a54xf33%yD{4|8zoV-@=WC=nK~ z3u@+ijoqv3*=jE_{em%e(RSSaLpab5Ty$*vNvGte-mf_P_+aEh%yzlm;NM=ezPaYd zw@Ql=>>ja~^BMbbYP81URQ^T8#{F^Xvg@=@<8>X5b(+!vkt3%B}VZ}^{gDutOE zQX{V2==563NJfIvq zJ_%*CC3C{A@Ny$v52|@gS~O0xI9+%!&KbJb2ES~F70t(+p$0Op_X4tY zgJ(2vCfBmxdOz15G;NJFctuxl5fKamcVRa_eU2iMD@>i=n6ba4%OPDN_Y~aVmfsQ^4nv!Ut z-H-BHTGPX%NfL!?0&lWyhR8RRCQ4ZOD`{DTqedD{Vd08);u&JJFA(;{YYEWZjeXCkV)^UW$niU5;6SqSy! z%fZSEobds&5yKfJ-kEKN{}^U{)Y1HRwJ&!1?Sm_X*-?~lcP=k)dv4wa zHSpuX(tAyEB2OA?uU|?zK^K^I+H>#sZS6_l;cA!I59{N*KXwjtOUka&wl z0)9MxG5YC>O-t2>TQ7JL?^s&Yx*us(YKQ~!<%o|}a<{W=#0phr=`&hsg=uX1pzr!< z_^kzsoUmvovCM%#X+RJ#;rOEda(`2mG6&A9i_z~W6P@nUrC#Mw9QVT<$4;#S75hF= zeDkgiArVKTe4eZlM~RK>2gaT<$z`}{ zpFIX%W*_KOPaYWabzK+*FwW5nKr_|TCxi}tb_Nt>KO?Zp-lvFO+|-l#)I(#dOj`h+ zruGL(HPFMgyh%rQwlz^k>P<3nk#8+@)BrY(5w zS;z2 zXwrWOL4 zI@_#1+ZA>)y~OgWy9ek6jFIrT&24md05ZK<@cy*xWJsA7hws+|6c90pXBpNCTQLjmPK6GC(l3?g9 zF*x|@zV!i#7rjKbhiGeIPSfW#`im7<${D*cF8enx!4p6HUn#6_oWAuRBtqTkY|IxCHg|78Es z7g2^h4kbQ*)nl~?d;@PyBVI&W-19BtgEtoLgI}n^`ToY+7BYXH*SudmVwbt|iv8uj zz>xvCK^6)zv{3^o1tyYL`yMvoeUr=jig#5rZ}|P5m~a%Oqq}Kxnq*7y-+3j$@Sd%T zOL}!QW3T1TPH#^?p5BM_0Nvo~&oo4x_cMA}KQ$q()M}@qhbAyO4Xs-gAYq~z|9 zf90B2YqYraQg&X+>nG>N7xsw`ewYM{g?U5$_xMYjY;%xQ5HWFW#fZVg6=B7UC>&igu%0v-WV(R;$mg z)<}JLBDTDxC@!&!q0=06>ZG?O-N-X758?J3-sN?723^mW_-f)n}z1B~d+HyDW3V)BJO#h3RX*`AB zF}VHx+5To?gmLey?r~nT5;kizh`?zxQ6-FqrwgNhZpVdRj~dLrHPsuQ z(9}#qUl%r9G2xV6^{ceBbB5>08;Fnn=gX7iyj{bu(G!w`g5$#*2JsbGHd(EhAPmIPI=oSeXxrr!r8sh-lA8vWyDoT^~c1v)4J?c(D%6)M|+>G0F$C=f5+F3&v zL%MIw+yBUe=!7w4`Yb}+YV32HIr^I3imBehYvRhWK|263O7|P8oqV^l-kz26CO+2t zv9PE>f4<2Tj=&XNiZEK+jQs${%8|)gkl7I$&%TCTm)*lTKX4Vo&aKJ|^_%WeAA%<3 zY&+~7@a?Ma-zmpLxQAE7Te+QxZ|`uoU7Om1IF+f1B{4FaUN`M!pBbgQ za^Z@jUu0{{N{%m^nteClBd?2lb)J|CUd^umEl$_N8?g1WL{&WHNq^?c_#*swGJ@8^ z?5@^)+W5NxuRG%EZ^mXN1uR~mihBJ0`2ryh2NCEp@c0?S(whtoXP=lR&GO~8S3f9M zX6slMIGt&e$yDLhthe_5)F7wRZYSMQ)A}GMm^zA>TLiC9wW{tB*?n1%o#-CITsG>z zX(3|G$8?fh>TeoWDb%W}Sh!~|?ZxZqc%faQENQEoj>FVx8{2ImN2GSz=NhCS=>)(g zve|A!-(|j{$57vMFM-h;gzxj3Hpm?mc^^@)jGYx929^OVD2ueR;|X1(G-(|9XX> z!GSka9uw{cnR#*cN+|qs^CR~WDAJ<+wwe1+d0KZRYxeM4++rdHm(UQp0dV1eANs%> zzY+Hr0_#1>R6HFen0@Rv3=jXJ#VH*ffrq`>j1U_JQrhTF$80_L%9OGwi2D~ff~dFJ=h>2=Nc<{*dW6U22O&}Y|^bIWD2r5Hd%AaX85|6hRQH>Q2?2iGr4 zdd!<6bh5)}O)rA8!iP#R2X_(6^`HNd?80yVM)&_zH~jyZeEF*TnC@D&dcVGGR!=H%8nxNpd~O$F)?$abQhV=6>BS-L zuDcNCD0#o@JRCrH<`2MdHP_X<&KPhMLC#e>h$BN*nM^mgncs+UDS=AOWrdG3Py9V$JVu+cyP| za>4W+$j@N&vE#dX=j);Mch_99kcWIZGM;Y?TG9%eR<89eAn(gM(^bvzF``zEz!xyd ztvfYAc=7|lYhsM->hW6DRf^X(*X#0Uk$E2Y&SRzuyXOkmAFGC)oC(5+L2g@u7*6A3 zyRAY1prM8*gpG?n@SO9h93OTc#BqQ=!(2dspJr7!Bd7f@B;fjL=IRoX*e7gBygDnv zvx4Mr?rt`BSXFfTLKPNyQh1sONvMpFo`i-VfDQdyt)XBr?4+$PI8{gYw?j4g7jH!X z)Ag$}8IiL;o*bzFcFe?I;-qnzZ3GLa*k=Z{KVVsOX^h6y0c8HK(l2SV@;4Gn=eIW4 z1fe@|=MF?NQ3a>s3u*Sx@bV5j>pu%MqDe_>Pfk2422tTBNT?FwuwGJF<~o2#8yx0G zW^Gf%zQ2u2&gv{fI7pLQsn&V%1^6S?%0&Oktv#j+JVpq-GhmOzuge&2v7PV-xyZ=w zj@W9fgE-R&VzXlk3JL_~P5g`F;(qLnEq0Dk2U!TGn?=Mc(r#p9Ma1kxuxgN>nc?+Y z)gK0Ea~9Ca_TH&Ibk|)WeYIfp;U|`PqBO{(5pn!nPI_1gxL7}jyoKbLQ6#(!L?(%^ zc@@SW2F((5Y`H-tZ#?e1F1 zeEsf@&$`hbv%;NCK+uQx>_7Qqr@eg}Zk7RBD4_)Zkp_B^8gai-#rMz|<`p@Du!Sie z)dtEDbXPZ|&&L)xM-Riz4II#5*sBePkUQr+AioZdw$uV#FbCN35$$z=9+3cOZaZ2w z?J2!AM5et#F7dALCB+txuuOcW0+Y45)TiK3HbSD|GG=`9`XPODr(=~#ZTe)vhsd^0U3lADs3#DC43Hus&AW&$ zrCHzlQYG7=e6Xl9D`z(UOME0BVt59=VQU#8&G%G-XRh|2a|&CFPkgi+uk>QE3jYLf(q_I^)I9M59o8IV_Lf3Z@_#*7%KkXh`K$Qo*iSSFeAxdAcX+Vj#fCNBQSal=fgYP;6` zFiQ&sg`tKR0~6bIbWAv{WC#>8XqqoL73aWY`-sYfW1iU}a0f7X3;gH@ZmnNgFHWUq zgCjJEyXKk)qHwo)Nji#_j0`faPUU_}dPj0laPi5@OmE1m>uOARR@h#uFzN8{2&hSJ z(C_;5F*4X;Z8Z=Fd0H8_XpCX1KFCimc0gXvI6Oam0n+TGrUM6s>1aEL~mt2g_Po?#siX!>%tCq_&9au6M4?mPI|`;99MwsVVL&LeMrh?#BwoMyPc zn05)hJpsXv?t7h-8An||b?V}Bq;+JOj0LM^vbof^&FlAtmnIs`=onogep!%SqC1_~tV7B|WM+PrD^VT^3l# ztTqHWN9?tlh4%nC$T4TdCW%aWsKYx<#q&0l)a$WEXZl8K5Uv%g!8U8~ArbBjhvc)2xz4#E|s?^#% ztv`BwP|+@EW+~Z>Rev(JZDvRQcJGKNms^_^ENO@p%$UEl75*(ywWMcDZ#!f{vgna> zsI?cSbg>hR>*DrIl#33SPoa%Ip9+eb2yl!OqB|#=3vK1 z7MK@zO}52FJ~V}1hLzzYmVXOEUd^|4o68_>uF}HL{GV&m3ntSHDjh96q&g^0 zDm|~hSVib6M{{GHx7t019V#>A^JFsZbSPH!DAl+V4*vjDY6DHA$calh5?g=yQ?X9B zPg-t!M_-{`3~*@|KdHqjd^aeS?>*BaSRa1~A|;;1w@<5f zY1_E5Tyg&Mtz(r-qQBOQ1)e7ZXeqqe4J9u3^4!3hZnGN}-pFTqH7`PLBcOiP)MndU zz#jk=N8SA4s*<0WC3akYZZaE7QA)nF1hXE_IC>LUMZR)Jre8BFFs;X%?Z+PNT_UJz z>qjeQ&sKetNjI&heeI?@UUM$@n7Kqybo?(EdH=HAU8J`WHZ6NNb(RUmeMz%nuxLVmpUd^E`Lm zi`|f+9&?KtA4o2EPj83s2?4TmLf3;1+o*R*C+@ztHakn0I!{oJOQ)g&53tOxCtu&%PA8 zw93CberTsvl6!l>_&C?Jq8}-D!_M)_#Boj#Lp3KE@CKqdq8rGMz8h?7xLpqL;4RLX z2vT4c>SlY|&0^w~`>Xt;o_n&XvcT0C0knuhj(q;1TI0rFo)ZM`JVpymL)18+3L)u` zprd({$W)Bt#z$dbQ4;wc`-yHID(3Ird(bq=6vwmYd}J;B{uuQLU&fvlasiMC$G>}# z%~luGgHJ?D{NuKouPV+I|AmR9j#sQ!3!b0mBkw=Y>Y)pG$?}t$BkJx?VR=~KuMFjT zE$rg2B{Q=)k$i3VDSV~e{jbY{?(X$qM-oI$gh-A0o0r*y5(bJ|`y7Yt5-OY(j??Lh zMPE$BMK?!Rkl8g%Mo~DSBTu~cs+|)<(ADCyYbTy=`Ye65zV4U+px7=(#E9fxfF2{o z@Ja8#%cbN^5A3;WB@h3xsJ(QjQHi{6N$0iqf0ISP68J%@QU4Zq6nb*xje^ktxMs^LfjC!@o@@G3HxgMj(~|EMGE1}%?glqFG>$qP z02!o)EW;FeTz<^I#^&C-`TD||gZX#5yE?_>JB}bJ_*dGN#PO!vt2l$HcCN~iSrHJi zpplJBo|dZ}O()-Y!SBpIv?s`!j_&UMDOXD*tyRzSzgCt1O;ZWm8AK5iuEipNA!yOUq!4SOgaz}|`&ZX`~p7s6jakpKR-Tpg$*<>2qBeGS~x zZk#OdVXEJeZ-R!59S_2trgR~mESYLA+@JNj8l#BT4|LoOc4OSkiTRQ$&Ug0*?&s2srlUST7gX@kFTb#Ng_^SA1ew3ehFa3?V>V4KdvE zUL&4atq(dKucC;DiS0vO;q^DFkKg!jCo%8DPK+6i!>TunpO*di*v`)kQA4Z)FL)-AcU13VnWa`HHyx+ZPzun8(ZkW>ByHgT% z>t0Sfru>9-`E-Z1cG2Y%p+evgf74F?fZ&A5l68QwJO*S(4&ndyBQSekXDL{K*W&}s3ZdkG zfHsF`02cYtL(rO=HN*i`k5yp$+3DWMe^25FO;6r2;r1TDDeQO7bD0u(472^^RbRNk zK!kW+{R_t4#vq2rfapkH3fKwi*aV;s7816~HB81+Mpu)-TD8A@Ch=p;qhG<1(O_yiJ+Ph;y&k&_Y+ ze_WgoAb9*)Miem}dHsoxZ844!!}z(S3Vq7aYYM_fg)^mNHMB~8XKslA?cY`&PI0D& zu>rpQaqCBf`T9bw)Z7O+10Wl%{U(dL6%R6LFa%6?!3esA5APZMJC) zA^6N8d-XW)AsTPUS{eYAy)7)M$wflb5V(MuaS0qXc6w;F$#Er!`PR1LesDm>Fz5exIkK4Zs*ZTJ51F~Gi@;2o`< zL!24ey2<8FW{z;51GSnR+p78w%Rb!guqOq=9vg~9%pfQyrsD&=wNKpK&8sFube~e8 z;BZ^}`1#I+d*UQ>1K96ixFOiw5;@vae1AaN)}Qt&ws(>c3!6Srx7yFm)G@MfS_|Z8kBEuy=I0AhV3A}Ar^V5WT}a9uk|3Dq)@LH}vH}W$DVUe$;KeLeW^;Wn z>s7-1=4QHE6}(U*-Z8NJj$x@R$ujU#uS0Wl4L#kP`w@y|cLf$FBph0pMU5k`yfGS- zYK^~|9KLNSYo{?-?ZUxvwcGvg6J@W=)9)tD-00iCaA8O*b#xCeiwUG@s> zX-tzvJTp9QJ^KGH%LHg^PUR6hLz^qV>ELaqAz`pJ?CfkhaVW0{H0Q%ho;n3|#RIs%>L;dtp1yCtu$mx-#v!imt6ThPF;y+59CIXh4f7|2hjNYl##_D~P(M9?j~ z#4a9PT>pJcKgj)tkT5`)8BGU>)IkgY)PiJtrrS5Wq$OZin*@P+3W?1_k~6Q-J9L)W z_qP=Qco!SZkqf8Gyc@&8USFf#&3<8cOen}>BFJPaV4T{vQummF?IrBLRJFLIbS{`& z*zns=RB!4?OU6fqqQ~#uP5Gsc237hpFt8gh+ipX`9k==X#cO?MDj)xYOBQ^`cCBHg z>%(zN_gF_q+7r9MVb;NY5^wJKlw7IW89w!zj}jfcgED5f|7@xdojo}vs>|~Aa=^|F zqp)9CH`q68zrCrDaOad=43~Y%L4wksn)_Z{8E1Hi)|pbf#*}v<-|lDyv;za55zh$t zJn>>Nr%Nyq&b5N&zn@uk2N*catr41lSwrx2b|4mfu1ei_r*fjfb0qQbV-VI`TifxR z;H4a)3bbyk-vvYiUIyzru6j1PJ)ACQVE(5{{x^nmJtzutW1C3?*L)Ch$ECH1Nbt;# zY;5W2{eid6JpilQu{(AkJIy9;Y_mGGC{K-yjKDs5UUE8ARx)l8Ngsje~ zv&|}$ln?1rM+ga7>Q>eTA!e!$KKX81(In-~?^b-$U%#d7rArCi7ZTz6iiCpJfVs@B z7gYpFM;)t(+hWt|T>2^34n@aR!wo>gMh|2)o$|@|k@uJ%xemnGr^$6VK=y&+zon=j zd`_(Y)|=hNAIaPV@e{Mmb-ag24CMYo-UDL?-P*U)OBgR!IX>-1N~3t)Wa=>tY>vRa zW;^tO0qlT0T+wdO%sMI$%yehhPQjGL;!5cg)`OJN>qQ@9@*FhXEcVdNe9Fo9;*IW2 zKc={S?v_fI&(fH#n@O&S!r*;_R1gctVs0j=<}WXYdvn?QoK=0(+A8h65@yu5G?~yM z){XTqj!B;1m}-*U!>03!#Kb0YLGq5y6D~wNI4gmBb6;=YYjdtcpR0HJGGHsGV+TF; zng{4StB$F|xd{^^_Q`43Q7VV5UNyDcxA$I!^MnTCs4;&YSF$2byI;mv0Dj>|>G4TG z6*%(r2pJWE`oSfJX*9yn%7^QfVvjEsG4eu&P_;Z2TvJnGd!uj0n$4IRfiq8i(0NJv zkFF8#?XeOA3;&YtjI$QQYz~QU<2=NMBZu8yNiE!MmX2^e#o8H@+~${PwF0wSFiueV z-j}EG8p?;2-&pmm4@1LeQ6yN%*3)g9&Gh!Z2k;MP+1Rx1Lg(fX5gzmuAiKm& zv_Ih@-RYVjS&t!GDm=$K59Dj7)+#DE~#z7(j4UX#>a)t>%Q0s zjsMvhhY)(D9rIhd+x0CE#=PM@Q68#Kv#7hLE#p^7Hmq=U$B*bpkWA|E<}!N$9-wy_ zih|Q=7*a)n$aeb|SI#xKkcZ8SL5#H}8n)c*y^cHPRgHZQhHOA{l8u_s7<8nfI^MH? zE4s~&q)aagpywC9a61S$!pQy+bC_`g5DodLYx_^T{Hy%Aty%sa0&xaHScZf4)DSA} zU+g^vA0H!f;%T0FK^eACk3=>URLVN3r}lpnDx5L?&lUYYNUi=d1evf8AlmX)A&4*= z5e92*2(*Vt#K7|rvl`c2WDX*m!FK$>R+TfM*K)EUV64oDP}; zQ?S?{Z9Y^&gsXlM2V!bNR`ia)9qCvw5NlA7o`f~ZZ+pZ|`LW)6&Lg(+S~%!FS#n># z{d5%8Sl((l0-L__kbS(*N4gifU~%%D8Rc!^-9Jq`8X4zjbs1#A-$c#G-UZZr#7#?4 zR6$|=8My;69@6>*@LqawkC8+NAQuP0Wy!rpL+q~*01Js%xG&Knd|=z)zj*(xT<28Q z$5J2zkk#VC-3EjacFCE72YmJrWQwdf9)v9ATaf=6g_!(c71th!EHTws*?__@!mtf4 zkXeym=#4G#QaZrsD{1|KZWvnRk6I@QR(B1d&BYWX`_ppws3FPJB1Q9syjt%rHG*x} z0T$UFs#`-?XvigS6foDh`g0#roeq6bQXfdJ@rwC=sQ7Noc-l?oT9CU7Ddf!KDQ`jJ z-y=6AbBJy!y7+bmcq99Y@2JMc_(6%N?&s5vzniWaUIB|kzlD^-98d)TW3Uo6nhjBR zK#p}+h)kig8-OodZ&#p?8wA0z9b(3Ue&ak_R5HZn)PY)}Ak7o-Uw#QuiBCu-R6dyh zB|(~j#6&^BfQ`DJ`_9(Jd{&^?afBue+6%xz-5vpB94V?8QCl&L!RR^N7Ax&^Dqw3# z5_~SmV!}~Z?_F>My6o-(`!4CZXdN3(7>|HHykq-y#&|qDur)Q$=>tfA3}cG??N`k( z<#t#PeQ%g>s%I)4hHVfTM6*Gc6B?G`%t5CBM<2d=9hs;+&}wnMruT#^L2o@@3Ra!O zM37~y!QHIQU%JwE^;4@ACHKQ`WMfoJG`Vcd8Bv=!o<-=SzD!MyJaF?dTl)hyZrI@O zhd4k9Q12lC@q(NU&HG!-~EFe;4q#-}TW3Ng#lGs2<7RWPR!Ng?rO~JY3-tY;| zo_k@`&Z+WI+Au84mH~t;jnSq~tV6zwMxE~bgv2oC;Ic~kO3JjMM|_mr--`CDzt^7h zDEzf;)7vn0+^PFMuJhMcTCQ05Nk!l35$)XRk{$P5(e~J}>qvPl^PDKwNdsj-a-Zrr zH;+-xK}pyfU+IKp9CDod1o%s9v<3937TcJn>WmVF?c>_qP61iVelIJ-&)ytXke;$1 zpIMOTH~-zI-?Rj{nIqT^vD2l9XAv16-9cybqV-Djgz9G7^bo)E27V>ykdDlpc)zPj zgD_K6Ztx7C4ati>1!8CuBxQbX9QNFO8LjGBgJwVMS|8%Za~U?Q_uV+TDQn;uc85;D zQDdBwkY(_6Wy30J1=KZb@6HrYcfg_QioWuj?^L0Cgp>Zw;*kj zke%}}RqKTI3h83u0H}`hp;o1aBu#{ESAmc(hd+eX#X`v`6jfmg1nV1(O7G(!_>P$p{DK>{h|5y7e+&DyF&N!NJX zXAy1eR15Z3O8kM`_t-T&o97-@hhaq5p)&#w6*vABpx#ApnOuB=_|S@AIb(GQA`$sR z(xIf`@_dzt-8vNaKbwqesqd-Ro6y~Jov@A!W);JEfjP%Bw)0eG$BgnfYZ$5-XjXD}s+T|@kbM%7)<;w#Wi8Cd~{#%Q&s3JT}@i&yl& zXtH1P)7doPU@l&BD%rf$xqhyX=7THTbvu({Lm(+*0yQSM1TEuG6rBnDx+{law7Iat z+sVRhFL0q%F%j!kQL29X-42n@DpS&&^}YDgek;Yr4KYT;6=2`FYujMEK`KFCL!CAT zx!AkzBfRn>hh-N(ba=>*iPzYouSJgTJ+Io3PDbBP_FH?@c(wQ7>To`@m&FH)AH%~a z#k2?Ekhjoe@ePy-8}p38{;KH~Cn>)$4MsJ8dsk@$FNDTgs1^C>*4D8w1f>0xzso95 z-$Dib4indm6q@75m{YkwrCh7mVl^S(B|O$GZQN%26oMVkuWL`aZx^^NjEyFuspcFT zrY|sAz1sUN9@?93tB`Hp_yW{!};T0 zy;7_G=N&X*%lnNf{cTH%gm&Jzdg2wgJ6I_btVn!#n-ZGGBu~nv!Hpk1;FeJ&dzvgh~qm-IVvKkW|Sti4}VC@-6XNB62jUtbXm-HScLTo;%VzSOs!Ns#1^lYm#EqtDtAU#;5$N z<-+!)R0+C2QJB7A9Br5Q3LD%Ne3M^MKjjAdh0!lP;?nnX#85S6{|s71qF&W3zRrkz zspBf0D+eZ1kxLmLUQec`yg&(){uo4F@w40>c&YP|DJM7R;ke)WAhK4RUP3w|l>P{l ze{P8j909c{?{s?7NN?%y;~(0_(ekFeEj;pE4SMZKhcM5?-&Sa|pAXj=sS!AqIKZAm zoXJWU@rFV09$`=7im|oVAW} zinnCHQg|?9$8Hp}fo#$JH9vlL{Z`t7YCd&+`knqwy=Ya2tyP|2KFc^{xnn^ZVLQVC zovC2TYtQs?<|Ev$A(7Mh3-9LtP&U&-GEgByk^w8_o>6k?&K)(B+e*<^*avmLPoH8? zDHox=_jf5fwKVNB$&kpZoxV`JKFoOf^5~lmFSXUp4C}{^uVy@qr3~2ip-*d258%S4 z%`=2}w`~Lhv&Ib2Zq3^*8}0k!gDKhT>eEJeQKT#+ry6>pn<~ zVl%bsWGJ^>alABr!(!^6L|xZA+LT;suamuFLY|;nT1_y?q!|{snzb0x4SAr9qLWFQ zQme|;li;O)m@zj17~zenRyBO61#E3_9JfurSe5P)(zelQYlW}RTt5CL=yfAhd-!6DyE@0X>(KbMEG41z_$9B<#9n__a zemUB!dVbP-^ak065XPVA;#S|iu7-*2_1C+kj>}4UPY%kRZO3G3+XV2f_^H|OdflGm z^-~}wgqw0e%^L08F-SD+aUwY8;77I$Opv#%Y{Dn%a<;Vkmk@TB7SX~t1d7xPs4?Xh;)>h1B}ohhA3yxw$1 zYT#poa&&6sON0En_FVcbnHr`Cw)IfWoC_&W32YBX1+h3CVq}tzyY$SAT;eL``?120 zXYdbmHWL0`)J$t&DwuW?y=l49bMPv5hqYFxPrBF#v(%$*z^>a_bMZ&6(B1~yXx0F< zB7-|yk^b`(R8h^R+(@F%*PX}`TdyHeX~Cw8_6mhJCiS0bJr>16DhH32cmT4^Jb9jk zZF-H$!SAz^#_Gv>kLS61dwi=SmGk$}yx46SDv2qtg0AD0RSgS1=amVb(_+7f9U*EY z1gZSO5|7V~W7cj7nI;=k7CMV(d6a6@46b=`p7viNVIE(y_+iUKl?#oY%?qWsxN7U; z8j)w6N>VdnwY=HBxx=4E_r8PyT6C&I4PK^$9EL8Fj-j@i#bx-q! zt4{GQd^8)B*%7jDHEL+88BC#jvAeFudAa7%TU2x%JwJZkwz|eEx8`|3q8yE7y)d+7 z@utZF6KeXW{vbI^OB`>XbLt0!WSMpZl{tLvnL?xpJkKZepI5e>A(0J!s8TYhp&k!j z?izQ=qwBCzaXqFts7exNdahlD$6ES%N$Hj=F^|)pxSi=m3)j}yWD6M2wk)-C^4i%> zzcVP4Rd$Nx3--bi87sU{n#7$b6|R^O=?ux|UtAM;71Q)I4ugr%*!G5zpP}qrd(LE40@ot@aklaET_cDDrBX zoeAsDenJyl782@l+_2+1zwPM3&_nrZ@L+z%KRk~%ArUzGrmv3j~Id$iEJ znl`Q$>ZVcG$0c4H&4d|cF$LwCl+$i>Zn3=&Gt9-(={6gA* zv-_G##dfpsq%+B*R`eS9Nj6%BWv9=>BJ^=3+$5QPL}nZq{tr)s{;})hqm5lWx}YF> zK{HOL#w6>N&32z^7m>YVIq1=t_A!e;WVz9Oi(}mjasGGG-Et#zOz&-f%^K1rkc~+i z1~FB!gu)+aA?fzNs*Sx)xvGky+Xa-=)4B2TWZ#VZyt^}uZ~AUFP%Zs*ry^~M^L(z6 zs#1%Xdn;=JV#lN7#1=vHB^66_!KU3g$HhQ(E!Av#X=*L7X=V{{URovk46nM>%C9-m z)+^3|=L#3fFf)_sWNst#N6#RR_vhQ>xNf(Er4v=?m z4F8f7wGRKrQOwM#0S~n&#M@87@7E@tZ?)>Y57bUG^x%L0la(f6B5)s+SXz?`{HQ)|#;_ZmIN#}Eraiz`wCFgQtDfXoI@|v<<=W`SiNp=sp1#jVNtGJ||TnH$jMW!)VtqZT4PWwot(KB-!RE{Y%oHHeJ&K z|BLMJ*>K!>ns1bQwDhGa%aTm35{G+LO@&Bj4U04;a_ZHw_m;^A$z&x(X1RVz$}xh% zuqQ^UQ{FJEwCxCqW~A`2bNXbaOKw~4kSbZJhw}9-XD~^NbBc-Hhx5VP>4BFScMPUZ zGH|&?s!yp#2h%rKUsGX)X9GQvH5nrhV*heC3wv71vhQHvNavR#|4+n~a8GS@YEnN&pR=6I zftEVhlbz}4FF+BrWW5~j&)vh3(PmF}GMr+wc;nOP>-M&g8%?gn?qw6WKJ~hWP;hd! zHeKZMY7+hPc%!mL$mJdEta)RoP-I#lQqKz7h8$6$(bS-ldE8|XE-aNNwF_}30tGYdLZzFPATvt@L z2gt@Nj-M|`OPDPQ0#c5Z4_IH29$=wYh549{K&!dbfP9 zGsX(@^*wq2_pql)W?rMoNtiW#;&wVMsbr$JMsd&Wa2l6k;yKf=0r=+d^~B)vt&uJk z28l+jlwwVFo``iCOU|Ue-*?Knt8Z&g>Nhmj*r>anv>&bWkHG3;kC((l%=c$%l?D%Ks0<~L3>KMPIHjuP8L zgHq|l1MGkF^^~+3ql?q)*?TS*(IoYowoUmN?r!`Mv1a&TKKZc;qFAf zy26kMfq@j8Ov@_-#Bd|`MpnCBkC5o;oX1BnVPkLRQ9hZA zK6}n4dX_s_;#Alzu|>b4%X5N%0@Of3HSKgtW3L%{Q9-Hc5RQ6|f3Msu{Zb-;L9c8M zzt0nMtK&;Tybed{S~`4C-QSM#0*iu(^EuW2&_5`NnuI}GPHmhd9TfV5>~A*cUAZg8S$ zvrSnWF?gZW?Zk)%P^Py+j zHJ=GZhcY!_*4)yl-D~!iohZFdlu<5*3tc@BT&^8l&%4y8z`jHhk6Z35&d4etwe?uo zV7bcYE%iEeo3iNpEx&4rqhz#yFQj)xYTUdGZ6Z^VBiElSD_%N*@2-)$oc$y%gX99z zYJZ>_DT+}{y>;5H)KbbMbm#*kO($2AZSvBlLs)0WbN-c&0I%SAyBUeQ&RQvNR9YWVJ75qJENnQ|M|J9w*MF$#hm9;s;|f z#nO?nTe|K$of##CtOP6Kohw6$y_mjh|DK}>$Ndz#qDPMvYDEe<@o18_j34U(6Dyq4 zQenH-M4(ys+?TKE&WM9uKKVg_%Xh83AF1=oK7NE33`!$*2vGDfW>L{z8aHuf@T zyJh4R)Sz;Y4KQ<;hgE^m%YwHSpFVAq#n>6@an8^pRQYgG3fOoWcKEGHt}GhQ7GhWV@dvNes8m}?1LvQ$ILea z+TQBbcvbmPKBcYAZQ)LP*La;>n(BJ7%-F4u$KVpAtYjr{32=ZhYOvD(_W)NE+?bBR z8A@ybB99C~{KKjP`-J_u0et@~tD*yVV$d4ahy_-%v#{&!NmOdm3p=c>C(Wr;!0;`t&OH#kMsRGq^zC zx|0G5&-qjY^;F)-BG8l>1PnEIdbCoD6~a$OfuiJ9ye9)1*pb=gJGqfgb|SdXMm-|% zg`4I6T!2xC=DRqCDlI4Nw=Q}0F`@%0lPLSo!g#Ynsn)~9?x_*!MVi}@`zzmgz!|hD zJD?~32mq5Rh$B$Cex={K<5UehxWAk>KxHFC{q87Y*)riVWPoPE0cZNMW1!q0^Kn+( z&TSnAC+4YiaTUbytl_@J8krWSW!r`edR_uOOo;@>Jt6yakCp|fg|}RO3Pp8I>8ehw zhY;L(_hB#%-Zp8vTE%zcrd+8*-D1nhF`f+>KgFt07$mI^t)u%yotDw_GN8<`J9 zF?`_~bPiSWlsyzz%OT0sI`2D8NW6en7 zSBd|?K=N}!u43Z?b*h$DJ6Yub)0oqo@X9C!>{}N{Z;VWXrIQQh(k=G&N`(FP`Z+vo z3On5nf~{oVFY+~MHCXVmo7U*BXh`h7_9;haP zO=2-3tH;yIAcJiuD0Rk6s$g$>>pK6r;7| znUb_Z=@Lw?Up7?xFDl0%%`d}Fx+c#5>}D{X1>N$#e{{gquuBCOlj}{k72j?iz8e#@ z6PqGYVuSQr9OchGjjykaaw)p?^e!ng2U7;ZA0r_>HaNj!=#xIfKrrz3L6Xyc{t`b& z9Wnz*5bhMXWs@RBXgRc--4KLH2T@^Blezy@eDhb{%yR%;YIV#O+=vprsz(zDh%I_^ zMe!3REQBc*SWkCu>J*HOGqe(F4Yn0zSz3V2t_*DmFN!l8Q@F1FS9O~z3YUPb=~x0S zUDeG2`f5Z!)ebfGJ^suwryM#|wx(?u-LN=gTyI?9dY8AlVg+Yk~ zAHy8;r>~y*Xd*Uua>$TLCe#CbrT*+SuW=HXiR1Og^#spcgNX)N<&lTI;XjHG6fK~} z3yMT1Fiahs`FYcEMPf+-!Jb#QbFd3|3_lhqtX zvg4a)XET~Tt1~PmnA+xc0XTDQx}tv`MS{kuAFi#58L>WCSES=!61|R8KjVI!HIBR? zzYr1?4UEiIdo4e$4NbUIAANP2ZntBVmm<7Mnunw&!i$aU+V>g?+nZX}XtvtVE+ zvRq?V_8>3*UGlbTdyauH^Zd8(QE%Ox!r%mqt}5}Ob(-5b4uX2r5fTaKIPwDn*8IL^Lk;_ z?143p#2#+VB17~?N?-VX1wR2nsy5pxh0C&(J_2?ocHxmb8*%=oSJzN&`kU=a@L5`C z&VF{w{pP}Sk6m1E%OrC4FLjy*hyi}mkrVN8f17SU@=7ZyKL0GYxNn`;@$dHrPt8L$ zrRhKO?bK$17oJcAICc{31%pf((q>#nVfbT9&Ae9-V>|k}T+huColQ#~DX&7Q7u$y~ zcf=u0i^Pgi zVg&<>)bw+#ciOXY-t<<)8WG-+t`_|O@m3{CI>=Xpko?kVecDq(w+Ms+KN~acWPNwaAaoc zLZ*$2aS&|`i%;2?o<#?7iAO@UjFy69TVazr}=x%_+R^+H8VrO|u zmwz@xXoM29mR(Ftw5Xh z3KUC>hG|@yfu+M-)tYD=@>EY=JtbwFLVd!zo~jf>^)-{=Eq6es4>tA)c{Jri#xd(o z@0LWN4*W;O6falAq&cL7U+F!~o~TP+bUgXnzTb%VNAvDS>&h5eUhp-VlZ6!B|*34*b|Z9N!Fagh^n=X$c3X*hBgJ0ileT) zLEWO|BLE$sUsqwZMeRCz7iByfx}{vBmQDj_{bOtHAp`jTW7Yk~)H}q}LVtL|IkUa} z;T2Sp&%*CCE((tUmYKqQj%T^dW0u=_p!yT-WpuTXe9ZgeyZj~L*||jF2k3xEug-^u zmSW;FBVlm6*|uXNlr@BZ)O{H#%;=C^m0L@GpD*LU!|rJzPqi55wWulx>*Tbtf3BmU zn|ed`4(A2jw6mFx!V(n?TLFtbyR_J1jn$okhlzg;*|%G-9?o+b(SJ>V3&r7PYH(7}qF| z?W8t8Je*KRm(kAZpdPCKyJUybxud;mY`z@J^MZeMb;Y)~&N{kI|J#kNh7g^>|M`ZB z6DCVj%^eIbq1oNlwOpRi2s~~{BsPbzkY`G&cQqLucR82PV#jIs6odP$)5jsz(V`&O zVeWlR^2Wz|eKS`}I|;q|zOxCGz}0G;)-?qszR~X=I092=P2Y0igE#=yO;nj5d7Fuk z8uBwR-!4nt&i|IZXLVDxV^lR$nsG@oe5BgBWik0xcg|`r3qieDv`q6);x1)0R%Zck zF(rg4Ah4b9^<8oyFz08d=gl=C>oQFA^^|>)PyVSgnC`rtBDq(8c0m=k9Tm~Y77N*K z;rE7DUjDW3;*(8D{Pn~Cdsp`BBgirUIkWIL7I++xV?$=-1&SQ!?MHY1`YrDN&mZLO zR@5n9``*{zk4+KQ30&p) nuTMUEq~+G|p-jWpbN6?m-kR-()~gO=BaXYE|3|5|Rmgt;9qU{G literal 0 HcmV?d00001 From c8ef5158759ec417de111c19698bdab2e9886d53 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Thu, 30 Nov 2023 17:55:07 -0800 Subject: [PATCH 011/187] Add wizard for updating provider --- .../messaging/providers/update.svelte | 272 ++++++++++++++++++ .../providers/wizard/configure.svelte | 15 + 2 files changed, 287 insertions(+) create mode 100644 src/routes/console/project-[project]/messaging/providers/update.svelte diff --git a/src/routes/console/project-[project]/messaging/providers/update.svelte b/src/routes/console/project-[project]/messaging/providers/update.svelte new file mode 100644 index 000000000..7c75c8bd5 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/update.svelte @@ -0,0 +1,272 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte index 7f1fce5d0..9a6dc02b1 100644 --- a/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte +++ b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte @@ -9,12 +9,27 @@ } from '$lib/elements/forms'; import InputPhone from '$lib/elements/forms/inputPhone.svelte'; import { WizardStep } from '$lib/layout'; + import { onMount } from 'svelte'; import { providers } from '../store'; import { providerType, provider, providerParams } from './store'; let files: Record = {}; const inputs = providers[$providerType].providers[$provider].configure; + onMount(() => { + for (const input of inputs) { + if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) { + const dataTransfer = new DataTransfer(); + const f = new File( + [$providerParams[$provider][input.name]], + `${input.name}.${input.allowedFileExtensions}` + ); + dataTransfer.items.add(f); + files[input.name] = dataTransfer.files; + } + } + }); + async function beforeSubmit() { const promises = []; for (const [key, value] of Object.entries(files)) { From bb705cbf8a12344b3cc731391b287163e82e7701 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 25 Oct 2023 16:52:35 -0700 Subject: [PATCH 012/187] Add provider details route --- src/lib/constants.ts | 3 +- .../provider-[provider]/+layout.svelte | 5 + .../providers/provider-[provider]/+layout.ts | 32 ++ .../provider-[provider]/+page.svelte | 12 + .../provider-[provider]/breadcrumbs.svelte | 27 ++ .../provider-[provider]/dangerZone.svelte | 45 +++ .../provider-[provider]/deleteProvider.svelte | 59 +++ .../provider-[provider]/header.svelte | 17 + .../providers/provider-[provider]/store.ts | 9 + .../provider-[provider]/updateName.svelte | 51 +++ .../provider-[provider]/updateStatus.svelte | 367 ++++++++++++++++++ .../messaging/providers/wizard/store.ts.bak | 161 ++++++++ 12 files changed, 787 insertions(+), 1 deletion(-) create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte create mode 100644 src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 52d2b999b..5bc06db41 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -46,7 +46,8 @@ export enum Dependencies { MIGRATIONS = 'dependency:migrations', COLLECTIONS = 'dependency:collections', RUNTIMES = 'dependency:runtimes', - CONSOLE_VARIABLES = 'dependency:console_variables' + CONSOLE_VARIABLES = 'dependency:console_variables', + MESSAGING_PROVIDER = 'dependency:messaging_provider' } export const scopes: { diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte new file mode 100644 index 000000000..944197a93 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte @@ -0,0 +1,5 @@ + + Provider - Appwrite + + + diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts new file mode 100644 index 000000000..afc8f83ab --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts @@ -0,0 +1,32 @@ +import type { LayoutLoad } from './$types'; +import Breadcrumbs from './breadcrumbs.svelte'; +import Header from './header.svelte'; +import { sdk } from '$lib/stores/sdk'; +import { Dependencies } from '$lib/constants'; +import { error } from '@sveltejs/kit'; + +export const load: LayoutLoad = async ({ params, depends }) => { + depends(Dependencies.MESSAGING_PROVIDER); + + const response = await sdk.forProject.client.call( + 'GET', + new URL(sdk.forProject.client.config.endpoint + '/messaging/providers/' + params.provider), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + } + ); + + console.log(response); + + try { + return { + header: Header, + breadcrumbs: Breadcrumbs, + provider: response + }; + } catch (e) { + throw error(e.code, e.message); + } +}; diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte new file mode 100644 index 000000000..2262f62ac --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+page.svelte @@ -0,0 +1,12 @@ + + + + + + + diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte new file mode 100644 index 000000000..5a3f26f90 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/breadcrumbs.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte new file mode 100644 index 000000000..7befc15c0 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/dangerZone.svelte @@ -0,0 +1,45 @@ + + + + + +
    + Delete provider +
    +

    The provider's instance will be permanently deleted. This action is irreversible.

    + + + +
    {$provider.name}
    +
    +

    + Last updated: {toLocaleDateTime($provider.$updatedAt)} +

    +
    +
    + + + + +
    + + diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte new file mode 100644 index 000000000..e32cbf6aa --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/deleteProvider.svelte @@ -0,0 +1,59 @@ + + + +

    + Are you sure you want to delete {$provider.name} from '{$project.name}'? +

    + + + + +
    diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte new file mode 100644 index 000000000..d78a0f0d4 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/header.svelte @@ -0,0 +1,17 @@ + + + + + + {$provider?.name ? $provider?.name : '-'} + + {$provider?.$id} + + diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts new file mode 100644 index 000000000..a69272a70 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/store.ts @@ -0,0 +1,9 @@ +import { derived } from 'svelte/store'; +import { page } from '$app/stores'; +import type { Provider } from '../../store'; + +export const provider = derived( + page, + // TODO: Set actual type + ($page) => $page.data.provider as Provider +); diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte new file mode 100644 index 000000000..f77b626ca --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateName.svelte @@ -0,0 +1,51 @@ + + +
    + + Name + + +
      + +
    +
    + + + + +
    +
    diff --git a/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte new file mode 100644 index 000000000..f4fddcc74 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/updateStatus.svelte @@ -0,0 +1,367 @@ + + + +
    + + {$provider.name} + +
    + +
    +
    +
      + +
    +

    Provider:

    +

    Channel:

    +

    Created: {toLocaleDateTime($provider.$createdAt)}

    +
    +
    +
    + + +
    + + +
    +
    +
    diff --git a/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak new file mode 100644 index 000000000..0ee3ba46c --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/wizard/store.ts.bak @@ -0,0 +1,161 @@ +import { writable } from 'svelte/store'; +import type { Column } from '$lib/components/viewSelector.svelte'; + +export let showCreate = writable(false); + +export const columns = writable([ + { id: '$id', title: 'Provider ID', show: true }, + { id: 'name', title: 'Name', show: true }, + { id: 'provider', title: 'Provider', show: true }, + { id: 'channel', title: 'Channel', show: true }, + { id: 'status', title: 'Status', show: true } +]); + +export type Instruction = { + text: string; + input: { + label: string; + name: string; + type: 'text' | 'domain' | 'email'; + placeholder: string; + }; +}; + +export const providers = { + sms: { + name: 'SMS', + text: 'SMS', + icon: 'annotation', + providers: { + twilio: { + imageIcon: 'twilio', + title: 'Twilio', + description: '' + }, + msg91: { + imageIcon: 'msg91', + title: 'MSG91', + description: '' + }, + telesign: { + imageIcon: 'telesign', + title: 'Telesign', + description: '' + }, + textmagic: { + imageIcon: 'textmagic', + title: 'Textmagic', + description: '' + }, + vonage: { + imageIcon: 'vonage', + title: 'Vonage', + description: '' + } + } + }, + email: { + name: 'Email', + text: 'emails', + icon: 'mail', + providers: { + mailgun: { + imageIcon: 'mailgun', + title: 'Mailgun', + description: '', + initialize: [ + { + text: 'Before you can create a Mailgun provider, you need to first create a Mailgun account.' + }, + { + text: 'Head to your Profile > API Security.' + }, + { + text: 'Generate a key and give it a name. Copy and paste it in the field below.', + input: { + label: 'API key', + name: 'apiKey', + type: 'text', + placeholder: 'Enter API key' + } + }, + { + // TODO: Update link to domain verification + text: 'Head to Sending > Domains and click on \'Add New Domain\'. Verify your domain by following the instructions.', + input: { + label: 'Base URL', + name: 'baseUrl', + type: 'text', + placeholder: 'Enter base URL' + } + } + ], + configure: [ + { + text: 'Provide a display name your recipient will see when they receive your emails.', + input: { + label: 'From', + name: 'from', + type: 'text', + placeholder: 'Enter name' + } + }, + { + text: 'Provide an email address that will be visible to the recipient as the senders email address for this message.', + input: { + label: 'From email address', + name: 'email', + type: 'email', + placeholder: 'Enter email' + } + }, + { + text: 'Provide an email address for users to use when replying to your emails.', + input: { + label: 'Reply to', + name: 'replyTo', + type: 'email', + placeholder: 'Enter email' + } + }, + { + text: 'Provide the domain as it is registered on Mailgun.', + input: { + label: 'Domain', + name: 'domain', + type: 'domain', + placeholder: 'Enter domain' + } + } + ] + }, + sendgrid: { + imageIcon: 'sendgrid', + title: 'Sendgrid', + description: '' + } + } + }, + push: { + name: 'Push notification', + text: 'notifications', + icon: 'device-mobile', + providers: { + fcm: { + imageIcon: 'firebase', + title: 'FCM', + description: 'Firebase Cloud Messaging' + }, + apns: { + imageIcon: 'apple', + title: 'APNS', + description: 'Apple Push Notification Service' + }, + mqtt: { + imageIcon: 'mqtt', + title: 'MQTT', + description: 'Message Queuing Telemtry Transport' + } + } + } +}; From ae8da6a091140bfa608beb249b7f1554c94c9673 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 13 Dec 2023 17:59:33 -0800 Subject: [PATCH 013/187] Refactor providers table and add bulk deletion --- src/lib/constants.ts | 1 + .../messaging/providers/+page.svelte | 76 +------- .../messaging/providers/+page.ts | 6 +- .../messaging/providers/table.svelte | 170 ++++++++++++++++++ 4 files changed, 177 insertions(+), 76 deletions(-) create mode 100644 src/routes/console/project-[project]/messaging/providers/table.svelte diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 5bc06db41..33a02a160 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -47,6 +47,7 @@ export enum Dependencies { COLLECTIONS = 'dependency:collections', RUNTIMES = 'dependency:runtimes', CONSOLE_VARIABLES = 'dependency:console_variables', + MESSAGING_PROVIDERS = 'dependency:messaging_providers', MESSAGING_PROVIDER = 'dependency:messaging_provider' } diff --git a/src/routes/console/project-[project]/messaging/providers/+page.svelte b/src/routes/console/project-[project]/messaging/providers/+page.svelte index 203bf85c8..9ede84994 100644 --- a/src/routes/console/project-[project]/messaging/providers/+page.svelte +++ b/src/routes/console/project-[project]/messaging/providers/+page.svelte @@ -1,16 +1,5 @@ @@ -79,61 +61,7 @@