diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aaac701734..8ebe53c4f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,8 +18,8 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 - # - name: Audit dependencies - # run: npm audit --audit-level low + - name: Audit dependencies + run: npm audit --audit-level low - name: Install dependencies run: npm ci - name: Svelte Diagnostics diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 8a15993fcf..39551514b2 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -287,5 +287,16 @@ 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', + MessagingMessageCreate = 'submit_messaging_message_create', + MessagingMessageDelete = 'submit_messaging_message_delete', + MessagingTopicCreate = 'submit_messaging_topic_create', + MessagingTopicDelete = 'submit_messaging_topic_delete', + MessagingTopicUpdateName = 'submit_messaging_topic_update_name', + MessagingTopicUpdateDescription = 'submit_messaging_topic_update_description', + MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add', + MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete' } diff --git a/src/lib/commandCenter/commands.ts b/src/lib/commandCenter/commands.ts index f12bbe2ed8..470a610129 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/lib/components/drop.svelte b/src/lib/components/drop.svelte index d6c899df21..f61ff6a246 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/components/emptyFilter.svelte b/src/lib/components/emptyFilter.svelte new file mode 100644 index 0000000000..5515fbdd10 --- /dev/null +++ b/src/lib/components/emptyFilter.svelte @@ -0,0 +1,26 @@ + + + +
+
+ Sorry, we couldn't find any {resource}. +

There are no {resource} that match your filters.

+
+
+ +
+
+
diff --git a/src/lib/components/filters/content.svelte b/src/lib/components/filters/content.svelte index 6da14dcc69..1f6717663c 100644 --- a/src/lib/components/filters/content.svelte +++ b/src/lib/components/filters/content.svelte @@ -118,10 +118,12 @@
({ - label: c.title, - value: c.id - }))} + options={$columns + .filter((c) => c.filter !== false) + .map((c) => ({ + label: c.title, + value: c.id + }))} placeholder="Select column" bind:value={columnId} /> import { tooltip } from '$lib/actions/tooltip'; + import { app } from '$lib/stores/app'; + import { base } from '$app/paths'; export let name: string; export let group: string; @@ -7,6 +9,7 @@ export let disabled = false; export let padding = 1; export let icon: string = null; + export let imageIcon: string = null; export let fullHeight = true; export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small'; export let backgroundColor: string = null; @@ -58,5 +61,12 @@
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 52d2b999bd..da342ce92c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -46,7 +46,13 @@ export enum Dependencies { MIGRATIONS = 'dependency:migrations', COLLECTIONS = 'dependency:collections', RUNTIMES = 'dependency:runtimes', - CONSOLE_VARIABLES = 'dependency:console_variables' + CONSOLE_VARIABLES = 'dependency:console_variables', + MESSAGING_PROVIDERS = 'dependency:messaging_providers', + MESSAGING_PROVIDER = 'dependency:messaging_provider', + MESSAGING_MESSAGE = 'dependency:messaging_message', + MESSAGING_TOPICS = 'dependency:messaging_topics', + MESSAGING_TOPIC = 'dependency:messaging_topic', + MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers' } export const scopes: { diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts index 22f3688a2d..9858ae0c17 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/inputCheckbox.svelte b/src/lib/elements/forms/inputCheckbox.svelte index 4aaf52c6be..0493d3f0b3 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} diff --git a/src/lib/elements/forms/inputDate.svelte b/src/lib/elements/forms/inputDate.svelte index a49aa3c4ee..380abe2793 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" diff --git a/src/lib/elements/forms/inputDomain.svelte b/src/lib/elements/forms/inputDomain.svelte index 58f0661999..5445b2ccbc 100644 --- a/src/lib/elements/forms/inputDomain.svelte +++ b/src/lib/elements/forms/inputDomain.svelte @@ -1,6 +1,7 @@ + + + + +
+ +
+ {#if error} + {error} + {/if} +
diff --git a/src/lib/helpers/types.ts b/src/lib/helpers/types.ts index b6d02e7a17..35e8214c9b 100644 --- a/src/lib/helpers/types.ts +++ b/src/lib/helpers/types.ts @@ -19,6 +19,13 @@ export type Column = { id: string; title: string; type: ColumnType; + /** + * Set to false to hide by default + */ show: boolean; width?: number; + /** + * Set to false to disable filtering for this column + */ + filter?: boolean; }; diff --git a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte index c622114491..e2948ef64b 100644 --- a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte +++ b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/+page.svelte @@ -1,8 +1,7 @@ + + + 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 0000000000..7db345058e --- /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 0000000000..35558ebe46 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/+page.svelte @@ -0,0 +1,246 @@ + + + +
+
+ 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 $hasPageQueries} + + + {: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)}> +
+ + 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/+page.ts b/src/routes/console/project-[project]/messaging/+page.ts new file mode 100644 index 0000000000..85eb3722cf --- /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'; + +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/actions.svelte b/src/routes/console/project-[project]/messaging/actions.svelte new file mode 100644 index 0000000000..bf2bb5b7bf --- /dev/null +++ b/src/routes/console/project-[project]/messaging/actions.svelte @@ -0,0 +1,44 @@ + + + + + + (showTopics = true)}>Select topics + (showUserTargets = true)}>Select targets + + + + { + showTopics = false; + dispatch('addTopics', e.detail); + }} /> + { + showUserTargets = false; + dispatch('addTargets', e.detail); + }} /> 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 0000000000..9e28bee679 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/breadcrumbs.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg b/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg new file mode 100644 index 0000000000..b5e7c0ba72 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/bubble-tail-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/bubble-tail-light.svg b/src/routes/console/project-[project]/messaging/bubble-tail-light.svg new file mode 100644 index 0000000000..9dcbbe34f4 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/bubble-tail-light.svg @@ -0,0 +1,3 @@ + + + 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 0000000000..52e96a0292 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/create.svelte @@ -0,0 +1,114 @@ + + + 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 0000000000..7a596496af --- /dev/null +++ b/src/routes/console/project-[project]/messaging/createMessageDropdown.svelte @@ -0,0 +1,69 @@ + + + + + + + + {#each Object.entries(providers) as [type, option]} + { + if ( + type !== ProviderTypes.Email && + type !== ProviderTypes.Sms && + type !== ProviderTypes.Push + ) + return; + $providerType = type; + $topicsById = {}; + $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/failedModal.svelte b/src/routes/console/project-[project]/messaging/failedModal.svelte new file mode 100644 index 0000000000..6ee183ffd1 --- /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 0000000000..9534915abb --- /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/message-[message]/+layout.svelte b/src/routes/console/project-[project]/messaging/message-[message]/+layout.svelte new file mode 100644 index 0000000000..9d2f6426ef --- /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 0000000000..21e6f6aad6 --- /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 0000000000..c949eba855 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/+page.svelte @@ -0,0 +1,22 @@ + + + + + {#if $message.providerType === ProviderTypes.Email} + + {:else if $message.providerType === ProviderTypes.Sms} + + {:else if $message.providerType === ProviderTypes.Push} + + {/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 0000000000..caf1d7778f --- /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 0000000000..7d7a2fb777 --- /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 0000000000..41988ae730 --- /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 0000000000..153a14def8 --- /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 0000000000..f97cc9e213 --- /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 0000000000..9dfb7add38 --- /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]/pushPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/pushPreview.svelte new file mode 100644 index 0000000000..b5695ed3fb --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/pushPreview.svelte @@ -0,0 +1,33 @@ + + + +
+ Preview +
+ +
+
+ + + + + + + + + + + + + +
diff --git a/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte b/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte new file mode 100644 index 0000000000..5648c9bf23 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/message-[message]/smsPreview.svelte @@ -0,0 +1,29 @@ + + + +
+ Preview + +
+ + + + + + + + + + + +
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 0000000000..97b3c1c5d6 --- /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/messageStatusPill.svelte b/src/routes/console/project-[project]/messaging/messageStatusPill.svelte new file mode 100644 index 0000000000..46b309abc7 --- /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 0000000000..5e2da923a0 --- /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 0000000000..d5e43f9f53 --- /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/providers/+page.svelte b/src/routes/console/project-[project]/messaging/providers/+page.svelte new file mode 100644 index 0000000000..83acdb53a9 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/+page.svelte @@ -0,0 +1,115 @@ + + + +
+
+ Providers +
+ +
+
+ + +
+ + + +
+
+
+
+ + +
+
+ + +
+
+
+ {#if data.providers.total} + + + + {:else if $hasPageQueries} + + {:else if data.search && data.search != 'empty'} + +
+ Sorry, we couldn't find '{data.search}' +

There are no providers that match your search.

+
+ +
+ {: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 new file mode 100644 index 0000000000..c8f65c5bea --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/+page.ts @@ -0,0 +1,77 @@ +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 { Dependencies, PAGE_LIMIT } from '$lib/constants'; +import { providersById, type Provider } from '../store'; +import { queries, queryParamToMap } from '$lib/components/filters'; + +const providers = Object.values(providersById); + +let data: { providers: Provider[]; total: number } = { + providers: [...providers], + total: providers.length +}; + +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); + 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 + }; +}; 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 0000000000..d828be10c8 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/create.svelte @@ -0,0 +1,274 @@ + + + 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 0000000000..d027d35941 --- /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/provider-[provider]/+layout.svelte b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.svelte new file mode 100644 index 0000000000..944197a93a --- /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 0000000000..3527cd7373 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/provider-[provider]/+layout.ts @@ -0,0 +1,30 @@ +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' + } + ); + + 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 0000000000..2262f62ac1 --- /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 0000000000..5a3f26f90d --- /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 0000000000..7befc15c0e --- /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 0000000000..e32cbf6aae --- /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 0000000000..d78a0f0d43 --- /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 0000000000..a69272a70c --- /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 0000000000..f77b626ca1 --- /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 0000000000..f4fddcc744 --- /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/store.ts b/src/routes/console/project-[project]/messaging/providers/store.ts new file mode 100644 index 0000000000..68e518b744 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/store.ts @@ -0,0 +1,382 @@ +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); + +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 } +]); + +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.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' + // } + } + }, + [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.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' + } + ] + } + } + } +}; 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 0000000000..2921c40ecf --- /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'}? +

+ + + + +
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 0000000000..4f3c14c44f --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/update.svelte @@ -0,0 +1,268 @@ + + + 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 0000000000..404b4f69ad --- /dev/null +++ b/src/routes/console/project-[project]/messaging/providers/wizard/configure.svelte @@ -0,0 +1,147 @@ + + + + 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 0000000000..1f9e238a1a --- /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 0000000000..9f47f8a41a --- /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/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 0000000000..0ee3ba46ca --- /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' + } + } + } +}; diff --git a/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg b/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg new file mode 100644 index 0000000000..777dec2679 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/push-notification-preview-dark.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg b/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg new file mode 100644 index 0000000000..7fd66545dd --- /dev/null +++ b/src/routes/console/project-[project]/messaging/push-notification-preview-light.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/project-[project]/messaging/pushPhone.svelte b/src/routes/console/project-[project]/messaging/pushPhone.svelte new file mode 100644 index 0000000000..a6fb893f77 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/pushPhone.svelte @@ -0,0 +1,73 @@ + + +
+
+
+
+ {$project.name} +
+
now
+
+
+

{title || 'Message Title'}

+

+ {body || 'Enter your message in the input field on the left to see it here'} +

+
+
+
+ + diff --git a/src/routes/console/project-[project]/messaging/sms-preview-dark.svg b/src/routes/console/project-[project]/messaging/sms-preview-dark.svg new file mode 100644 index 0000000000..cafc7a33bc --- /dev/null +++ b/src/routes/console/project-[project]/messaging/sms-preview-dark.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/project-[project]/messaging/sms-preview-light.svg b/src/routes/console/project-[project]/messaging/sms-preview-light.svg new file mode 100644 index 0000000000..9e9099038a --- /dev/null +++ b/src/routes/console/project-[project]/messaging/sms-preview-light.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/project-[project]/messaging/smsPhone.svelte b/src/routes/console/project-[project]/messaging/smsPhone.svelte new file mode 100644 index 0000000000..dfa2f82615 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/smsPhone.svelte @@ -0,0 +1,124 @@ + + +
+
+ {getIntials($project.name)} +
+
{$project.name}
+
+
+ Today {getTime()} +
+
+
+ {content || 'Enter your message in the input field on the left to see it here'} +
+
+
+
+ + 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 0000000000..d30c67b808 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/store.ts @@ -0,0 +1,265 @@ +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 } +]); + +export const targetsById = writable>({}); +export const topicsById = writable>({}); + +// 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: {} + } +}; + +// TODO: remove when sdk has the model +export type Topic = { + $id: string; + $createdAt: string; + $updatedAt: string; + providerId: string; + name: string; + total: number; + description: string; +}; + +export type Target = { + $id: string; + $createdAt: string; + $updatedAt: string; + name: string; + userId: string; + providerId: string; + providerType: ProviderTypes; + identifier: string; +}; diff --git a/src/routes/console/project-[project]/messaging/topics/+page.svelte b/src/routes/console/project-[project]/messaging/topics/+page.svelte new file mode 100644 index 0000000000..2b86aee240 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/+page.svelte @@ -0,0 +1,106 @@ + + + +
+
+ Topics +
+ +
+
+ + +
+ + + +
+
+
+
+ + +
+
+ + +
+
+
+ {#if data.topics.total} +
+ + + {:else if $hasPageQueries} + + + {:else if data.search && data.search != 'empty'} + +
+ Sorry, we couldn't find '{data.search}' +

There are no topics that match your search.

+
+ +
+ {:else} + + ($showCreate = true)} + href="https://appwrite.io/docs/references/cloud/client-web/teams" + target="topic" /> + {/if} + + + diff --git a/src/routes/console/project-[project]/messaging/topics/+page.ts b/src/routes/console/project-[project]/messaging/topics/+page.ts new file mode 100644 index 0000000000..013a089ad5 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/+page.ts @@ -0,0 +1,53 @@ +import { Query } from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; +import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load'; +import { Dependencies, PAGE_LIMIT } from '$lib/constants'; +import { queryParamToMap, queries } from '$lib/components/filters'; +import type { Topic } from '../store'; + +export const load = async ({ depends, url, route }) => { + depends(Dependencies.MESSAGING_TOPICS); + const page = getPage(url); + const search = getSearch(url); + const limit = getLimit(url, route, PAGE_LIMIT); + const offset = pageToOffset(page, limit); + const query = getQuery(url); + + const parsedQueries = queryParamToMap(query || '[]'); + queries.set(parsedQueries); + + const payload = { + queries: [ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc(''), + ...parsedQueries.values() + ] + }; + + if (search) { + payload['search'] = search; + } + + // TODO: remove when the API is ready with data + // This allows us to mock w/ data and when search returns 0 results + const topics: { topics: Topic[]; total: number } = await sdk.forProject.client.call( + 'GET', + new URL(sdk.forProject.client.config.endpoint + '/messaging/topics'), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + }, + payload + ); + + return { + offset, + limit, + search, + query, + page, + topics + }; +}; diff --git a/src/routes/console/project-[project]/messaging/topics/create.svelte b/src/routes/console/project-[project]/messaging/topics/create.svelte new file mode 100644 index 0000000000..2bb52245e2 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/create.svelte @@ -0,0 +1,88 @@ + + + + + + + + {#if !showCustomId} +
+ (showCustomId = !showCustomId)} + > +
+ {:else} + + {/if} +
+ + + + +
diff --git a/src/routes/console/project-[project]/messaging/topics/store.ts b/src/routes/console/project-[project]/messaging/topics/store.ts new file mode 100644 index 0000000000..0c950c385b --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/store.ts @@ -0,0 +1,12 @@ +import type { Column } from '$lib/helpers/types'; +import { writable } from 'svelte/store'; + +export let showCreate = writable(false); + +export const columns = writable([ + { id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 }, + { id: 'name', title: 'Name', type: 'string', show: true, width: 140 }, + { id: 'description', title: 'Description', type: 'string', show: true, width: 140 }, + { id: 'total', title: 'Subscribers', type: 'integer', show: true, width: 140 }, + { id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 } +]); diff --git a/src/routes/console/project-[project]/messaging/topics/table.svelte b/src/routes/console/project-[project]/messaging/topics/table.svelte new file mode 100644 index 0000000000..c72533d667 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/table.svelte @@ -0,0 +1,154 @@ + + + + + d.$id)} /> + {#each $columns as column} + {#if column.show} + {column.title} + {/if} + {/each} + + + {#each data.topics.topics as topic (topic.$id)} + + + + {#each $columns as column (column.id)} + {#if column.show} + {#if column.id === '$id'} + {#key $columns} + + {topic.$id} + + {/key} + {:else if column.type === 'datetime'} + + {#if !topic[column.id]} + - + {:else} + {toLocaleDateTime(topic[column.id])} + {/if} + + {:else} + + {topic[column.id]} + + {/if} + {/if} + {/each} + + {/each} + + + + 0}> +
+
+ {selectedIds.length} +

+ + {selectedIds.length > 1 ? 'topics' : 'topic'} + + selected +

+
+ +
+ + +
+
+
+ + +

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

+ + + + +
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte new file mode 100644 index 0000000000..525eedb90f --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.svelte @@ -0,0 +1,5 @@ + + Topic - Appwrite + + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts new file mode 100644 index 0000000000..594acc96ba --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+layout.ts @@ -0,0 +1,30 @@ +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_TOPIC); + + const response = await sdk.forProject.client.call( + 'GET', + new URL(sdk.forProject.client.config.endpoint + '/messaging/topics/' + params.topic), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + } + ); + + try { + return { + header: Header, + breadcrumbs: Breadcrumbs, + topic: response + }; + } catch (e) { + throw error(e.code, e.message); + } +}; diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte new file mode 100644 index 0000000000..dd88db1906 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/+page.svelte @@ -0,0 +1,14 @@ + + + +
+ + + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte new file mode 100644 index 0000000000..f805a79f04 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts new file mode 100644 index 0000000000..97f3d29aeb --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/activity/+page.ts @@ -0,0 +1,34 @@ +import { Query, type Models } from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; +import { getLimit, getPage, pageToOffset } from '$lib/helpers/load'; +import { PAGE_LIMIT } from '$lib/constants'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ params, url, route }) => { + const page = getPage(url); + const limit = getLimit(url, route, PAGE_LIMIT); + const offset = pageToOffset(page, limit); + + const payload = { + queries: [Query.limit(limit), Query.offset(offset)] + }; + + // TODO: remove when the API is ready with data + // This allows us to mock w/ data and when search returns 0 results + const logs: Models.LogList = await sdk.forProject.client.call( + 'GET', + new URL(`${sdk.forProject.client.config.endpoint}/messaging/topics/${params.topic}/logs`), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + }, + payload + ); + + return { + offset, + limit, + logs + }; +}; diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte new file mode 100644 index 0000000000..c41df553b9 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/breadcrumbs.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte new file mode 100644 index 0000000000..113c2550cd --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/dangerZone.svelte @@ -0,0 +1,45 @@ + + + + + +
+ Delete topic +
+

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

+ + + +
{$topic.name}
+
+

+ {$topic.total} subscriber{$topic.total === 1 ? '' : 's'} +

+
+
+ + + + +
+ + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte new file mode 100644 index 0000000000..a3e4226e33 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/deleteTopic.svelte @@ -0,0 +1,56 @@ + + + +

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

+ + + + +
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte new file mode 100644 index 0000000000..1e81e5044e --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/details.svelte @@ -0,0 +1,19 @@ + + + +
+ Details +
+ +
+
+

{$topic.total} subscriber{$topic.total === 1 ? '' : 's'}

+

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

+
+
+
+
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte new file mode 100644 index 0000000000..0848e48db2 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/header.svelte @@ -0,0 +1,48 @@ + + + + + + {$topic.name} + + {$topic.$id} + + + + {#each tabs as tab} + + {tab.title} + + {/each} + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts new file mode 100644 index 0000000000..a2379dadd2 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/store.ts @@ -0,0 +1,9 @@ +import { derived } from 'svelte/store'; +import { page } from '$app/stores'; +import type { Topic } from '../../store'; + +export const topic = derived( + page, + // TODO: Set actual type + ($page) => $page.data.topic as Topic +); diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte new file mode 100644 index 0000000000..4a05081d43 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.svelte @@ -0,0 +1,166 @@ + + + +
+
+ Subscribers +
+ +
+
+ + +
+ + + +
+
+
+
+ + +
+
+ + +
+
+
+ {#if data.subscribers.total} +
+ + + {:else if $hasPageQueries} + + + {:else if data.search && data.search != 'empty'} + +
+ Sorry, we couldn't find '{data.search}' +

There are no subscribers that match your search.

+
+ +
+ {:else} + + (showAdd = true)} + href="https://appwrite.io/docs/references/cloud/client-web/teams" + target="subscriber" /> + {/if} + + + diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts new file mode 100644 index 0000000000..2011433bf3 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/+page.ts @@ -0,0 +1,69 @@ +import { Query } from '@appwrite.io/console'; +import { sdk } from '$lib/stores/sdk'; +import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load'; +import { Dependencies, PAGE_LIMIT } from '$lib/constants'; +import type { PageLoad } from './$types'; +import type { Target } from '../../../store'; +import { queryParamToMap, queries } from '$lib/components/filters'; + +export type Subscriber = { + $id: string; + $createdAt: string; + $updatedAt: string; + targetId: string; + target: Target; + userName: string; + topicId: string; +}; + +export const load: PageLoad = async ({ params, url, route, depends, parent }) => { + depends(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS); + const page = getPage(url); + const limit = getLimit(url, route, PAGE_LIMIT); + const offset = pageToOffset(page, limit); + const search = getSearch(url); + const query = getQuery(url); + + const parsedQueries = queryParamToMap(query || '[]'); + queries.set(parsedQueries); + + const payload = { + queries: [ + Query.limit(limit), + Query.offset(offset), + Query.orderDesc(''), + ...parsedQueries.values() + ] + }; + + if (search) { + payload['search'] = search; + } + + const { topic } = await parent(); + + // TODO: remove when the API is ready with data + // This allows us to mock w/ data and when search returns 0 results + const subscribers: { subscribers: Subscriber[]; total: number } = + await sdk.forProject.client.call( + 'GET', + new URL( + `${sdk.forProject.client.config.endpoint}/messaging/topics/${params.topic}/subscribers` + ), + { + 'X-Appwrite-Project': sdk.forProject.client.config.project, + 'content-type': 'application/json', + 'X-Appwrite-Mode': 'admin' + }, + payload + ); + + return { + offset, + limit, + search, + query, + topic, + subscribers + }; +}; diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts new file mode 100644 index 0000000000..ca6538023a --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/store.ts @@ -0,0 +1,11 @@ +import type { Column } from '$lib/helpers/types'; +import { writable } from 'svelte/store'; + +export const columns = writable([ + { id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 }, + { id: 'userName', title: 'Name', type: 'string', show: true, width: 100 }, + { id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 }, + { id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 }, + { id: 'type', title: 'Type', type: 'string', show: true, filter: false, width: 80 }, + { id: '$createdAt', title: 'Created', type: 'string', show: true, width: 100 } +]); diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte new file mode 100644 index 0000000000..13fbbf7f0a --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/subscribers/table.svelte @@ -0,0 +1,191 @@ + + + + + d.$id)} /> + {#each $columns as column} + {#if column.show} + {column.title} + {/if} + {/each} + + + {#each data.subscribers.subscribers as subscriber (subscriber.$id)} + {@const target = subscriber.target} + + + + {#each $columns as column} + {#if column.show} + {#if column.id === '$id'} + {#key $columns} + + + {subscriber.$id} + + + {/key} + {:else if column.id === 'targetId'} + + + {subscriber[column.id]} + + + {:else if column.id === 'target'} + + {#if target.providerType === ProviderTypes.Push} + {target.name} + {:else} + {target.identifier} + {/if} + + {:else if column.id === 'type'} + + + + {:else if column.id === '$createdAt'} + + {toLocaleDateTime(subscriber[column.id])} + + {:else} + + {subscriber[column.id]} + + {/if} + {/if} + {/each} + + {/each} + + + + 0}> +
+
+ {selectedIds.length} +

+ + {selectedIds.length > 1 ? 'subscribers' : 'subscriber'} + + selected +

+
+ +
+ + +
+
+
+ + +

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

+ + + + +
diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte new file mode 100644 index 0000000000..137e7944c1 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateDescription.svelte @@ -0,0 +1,65 @@ + + +
+ + Description + + +
    + +
+
+ + + + +
+ diff --git a/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte new file mode 100644 index 0000000000..dfc6dd1af4 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topics/topic-[topic]/updateName.svelte @@ -0,0 +1,66 @@ + + +
+ + Name + + +
    + +
+
+ + + + +
+ diff --git a/src/routes/console/project-[project]/messaging/topicsModal.svelte b/src/routes/console/project-[project]/messaging/topicsModal.svelte new file mode 100644 index 0000000000..f1141b26e0 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/topicsModal.svelte @@ -0,0 +1,167 @@ + + + +

Select existing topics you want to send this message to its recipients.

+ + {#if Object.keys(topicResultsById).length > 0} + {#each Object.entries(topicResultsById) as [topicId, topic]} + onTopicSelection(event, topic)}> + + + + {topic.name} + ({topic.total} subscribers) + + + + {/each} +
+

Total results: {totalResults}

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

There are no topics that match your search.

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

+ You have no topics. Create a topic to see them here. +

+ +

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

+
+
+
+ {/if} + + + + +
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 0000000000..878e77a277 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/userTargetsModal.svelte @@ -0,0 +1,252 @@ + + + +

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} + + + + +
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 0000000000..950c749f8e --- /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/pushFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/pushFormList.svelte new file mode 100644 index 0000000000..9329272b46 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/pushFormList.svelte @@ -0,0 +1,250 @@ + + + + +
+ + + +
+ + + + + + + + + + Enter the phone number to which the test message will be +
(selected = 'other')} + on:keyup|self={clickOnEnter} + role="button" + tabindex="0"> + +
+
+
+ + + + + +
+
+
+ +
+
    + {#each customData || [] as _, rowIndex} + + + + + + + + + {/each} +
+ {#if dataError} + {dataError} + {/if} + +
+ + + + {#if !showCustomId} +
+ (showCustomId = !showCustomId)} + > +
+ {:else} + + {/if} +
+ +
+
+ +
diff --git a/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte b/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte new file mode 100644 index 0000000000..7f784b4d22 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/smsFormList.svelte @@ -0,0 +1,139 @@ + + + + +
+ +
+ + + + + + + + + + Enter the phone number to which the test message will be +
(selected = 'other')} + on:keyup|self={clickOnEnter} + role="button" + tabindex="0"> + +
+
+
+ + + + + +
+
+ + + {#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 0000000000..4fffcc771d --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/step1.svelte @@ -0,0 +1,30 @@ + + + + Message + + + Create an {providers[$providerType].text} that will be displayed to your subscribers. Learn more + in our documentation. + + {#if $providerType === ProviderTypes.Email} + + {:else if $providerType === ProviderTypes.Sms} + + {:else if $providerType === ProviderTypes.Push} + + {/if} + 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 0000000000..cb456bc95e --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/step2.svelte @@ -0,0 +1,149 @@ + + + + Targets + + Select users to whom this message should be directed. + {#if targetIdsLength === 0 && topicIdsLength === 0} + +
+ + + +
+ Select recipients to get started +
+
+
+ {:else} +
+
+ + Target + + + + {#each Object.entries($topicsById) as [topicId, topic] (topicId)} + + +
+ + + {topic.name} + ({topic.total} subscribers) + +
+
+ +
+ +
+
+
+ {/each} + {#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 0000000000..4a62c75ef2 --- /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 0000000000..3b02644524 --- /dev/null +++ b/src/routes/console/project-[project]/messaging/wizard/store.ts @@ -0,0 +1,52 @@ +import { ProviderTypes } from '../providerType.svelte'; +import { writable } from 'svelte/store'; +import type { Target } from '../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: [string, string][]; + 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 +}); diff --git a/static/images/apns-bundle-id.png b/static/images/apns-bundle-id.png new file mode 100644 index 0000000000..90fde1eff2 Binary files /dev/null and b/static/images/apns-bundle-id.png differ