mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.5.x' of https://github.com/appwrite/console into fix-command-center
This commit is contained in:
Generated
+13
-13
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.0-rc.13",
|
||||
"@appwrite.io/console": "^0.6.0-rc.14",
|
||||
"@appwrite.io/pink": "0.4.0",
|
||||
"@appwrite.io/pink-icons": "0.4.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
@@ -158,9 +158,9 @@
|
||||
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
|
||||
},
|
||||
"node_modules/@appwrite.io/console": {
|
||||
"version": "0.6.0-rc.13",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.13.tgz",
|
||||
"integrity": "sha512-/c+HGkqdqWjjlzVgOmC6vb4t2jsHgbmX/aaDLoQHXFmaakEDWzK1cgVYRHoU3o14xIEpBupteh3KzOoqdLvvsQ==",
|
||||
"version": "0.6.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.14.tgz",
|
||||
"integrity": "sha512-7Od0RuGRF01YUCWGTAvlTvgUG7sUCVHG9ig11OKz9jjBxE91BVcWsmqGWiXw+xb4HhF2jiyHd9ZU3sKAo0IC3w==",
|
||||
"dependencies": {
|
||||
"cross-fetch": "3.1.5",
|
||||
"isomorphic-form-data": "2.0.0"
|
||||
@@ -2336,9 +2336,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz",
|
||||
"integrity": "sha512-1uyXvzC2Lu1FZa30T4y5jUAC21R309ZMRG0TPt+PPPbNUoDpy8zSmSNVWYaBWxYDqLGQ5oPNWvjvvF2IjJ1jmA==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
|
||||
"integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -3256,9 +3256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "2.2.35",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.35.tgz",
|
||||
"integrity": "sha512-YzR56VAMjqvGIVzMUFwH7jjo7K+Z3Old4nXcigiWsuqXnWQeQi3iPR8p2/xOZzmuZyMTOC+6jjbIJ8hoEOhfcQ==",
|
||||
"version": "2.2.36",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.36.tgz",
|
||||
"integrity": "sha512-QX3ZXgHjn4cQ233PHne1m7ZP+nfP93CkDEgNCIxuiDp+mhLEIEV10F1Qk3NozJ8mmSoyIJR82ea16kMZKOi8hw==",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "1.0.0",
|
||||
"nanoid": "3.3.6",
|
||||
@@ -9514,9 +9514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/npm-run-path": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
|
||||
"integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
|
||||
+4
-4
@@ -12,13 +12,13 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest watch",
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
"test:watch": "TZ=EST vitest watch",
|
||||
"e2e": "playwright test tests/e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.0-rc.13",
|
||||
"@appwrite.io/console": "^0.6.0-rc.14",
|
||||
"@appwrite.io/pink": "0.4.0",
|
||||
"@appwrite.io/pink-icons": "0.4.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -245,6 +245,7 @@ export enum Submit {
|
||||
WebhookUpdateUrl = 'submit_webhook_update_url',
|
||||
WebhookUpdateEvents = 'submit_webhook_update_events',
|
||||
WebhookUpdateName = 'submit_webhook_update_name',
|
||||
WebhookUpdateEnabled = 'submit_webhook_update_enabled',
|
||||
WebhookUpdateSecurity = 'submit_webhook_update_security',
|
||||
BucketCreate = 'submit_bucket_create',
|
||||
BucketDelete = 'submit_bucket_delete',
|
||||
@@ -301,7 +302,7 @@ export enum Submit {
|
||||
MessagingTopicCreate = 'submit_messaging_topic_create',
|
||||
MessagingTopicDelete = 'submit_messaging_topic_delete',
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdateDescription = 'submit_messaging_topic_update_description',
|
||||
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
export let withIndentation = false;
|
||||
export let open = false;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper" bind:open>
|
||||
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class="collapsible-button u-position-relative"
|
||||
@@ -16,13 +17,18 @@
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text"><slot name="title" /></span>
|
||||
<span class="text" class:u-color-text-disabled={disabled}
|
||||
><slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
|
||||
<span class="collapsible-button-optional" class:u-color-text-disabled={disabled}
|
||||
><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down u-font-size-20" aria-hidden="true" />
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div
|
||||
@@ -33,3 +39,24 @@
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
// TODO: remove once pink is updated
|
||||
.collapsible-item {
|
||||
.collapsible-wrapper.is-disabled {
|
||||
&[open] .icon-cheveron-down {
|
||||
rotate: unset;
|
||||
}
|
||||
|
||||
cursor: not-allowed;
|
||||
* {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.collapsible {
|
||||
&-content {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let childStart = false;
|
||||
export let noStyle = false;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let fixed = false;
|
||||
export let display = 'block';
|
||||
|
||||
@@ -104,6 +105,7 @@
|
||||
<div
|
||||
class:drop-wrapper={!noStyle}
|
||||
class:u-cross-child-start={childStart}
|
||||
class:u-width-full-line={wrapperFullWidth}
|
||||
bind:this={element}
|
||||
style:display>
|
||||
<slot />
|
||||
|
||||
@@ -11,10 +11,20 @@
|
||||
export let noStyle = false;
|
||||
export let width: string = null;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
</script>
|
||||
|
||||
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
|
||||
<Drop
|
||||
bind:show
|
||||
{placement}
|
||||
{childStart}
|
||||
{noArrow}
|
||||
{noStyle}
|
||||
{fullWidth}
|
||||
{wrapperFullWidth}
|
||||
{fixed}
|
||||
on:blur>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
@@ -55,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)}>
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
@@ -58,7 +59,7 @@
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<div class={fullWidth ? 'u-width-full-line' : 'u-flex-basis-50-percent'}>
|
||||
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
|
||||
<input
|
||||
{placeholder}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let hideColumns = false;
|
||||
export let allowNoColumns = false;
|
||||
export let showColsTextMobile = false;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
let showSelectColumns = false;
|
||||
|
||||
@@ -31,12 +32,15 @@
|
||||
} else {
|
||||
const prefs = preferences.get($page.route);
|
||||
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
// Override the shown columns only if a preference was set
|
||||
if (prefs?.columns) {
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
columns.subscribe((ctx) => {
|
||||
@@ -71,8 +75,11 @@
|
||||
<div class="grid-header-col-4">
|
||||
{#if !hideColumns && view === View.Table}
|
||||
{#if $columns?.length}
|
||||
<DropList bind:show={showSelectColumns} scrollable>
|
||||
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
|
||||
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showSelectColumns = !showSelectColumns)}
|
||||
{fullWidthMobile}>
|
||||
<span
|
||||
class="icon-view-boards u-opacity-50"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
let element: HTMLOListElement;
|
||||
|
||||
let autoSubmitted = false;
|
||||
|
||||
const {
|
||||
elements: { root, input }
|
||||
} = createPinInput({
|
||||
@@ -20,14 +22,27 @@
|
||||
defaultValue: value.split(''),
|
||||
onValueChange: ({ next }) => {
|
||||
value = next.join('');
|
||||
|
||||
if (value.length === 6 && !autoSubmitted) {
|
||||
autoSubmitted = true;
|
||||
const firstInputElement = element.querySelector('input');
|
||||
firstInputElement?.form.requestSubmit();
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.querySelector('input')?.focus();
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
if (element) {
|
||||
if (input && autofocus) {
|
||||
input.focus();
|
||||
}
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let autocomplete = false;
|
||||
export let maxlength: number = null;
|
||||
export let isPopoverDefined = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
|
||||
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
|
||||
@@ -49,7 +50,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let autocomplete = false;
|
||||
export let tooltip: string = null;
|
||||
export let isPopoverDefined = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -58,7 +59,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
export let minlength = 8;
|
||||
export let maxlength: number = null;
|
||||
export let isPopoverDefined = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
@@ -49,7 +50,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show={showPopover} display="inline-block">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let autocomplete = false;
|
||||
export let maxlength: number = null;
|
||||
export let isPopoverDefined = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
|
||||
|
||||
@@ -48,7 +49,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
|
||||
@@ -41,6 +41,38 @@ export const toLocaleDateTime = (datetime: string | number) => {
|
||||
return date.toLocaleDateString('en', options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleDateISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleDateString('sv');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleTimeISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleTimeString('sv');
|
||||
};
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={createDomain}>
|
||||
<svelte:fragment slot="title">Add function domain</svelte:fragment>
|
||||
<svelte:fragment slot="title">Domain</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Use your self-owned domain as the endpoint of your Appwrite API. <a
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains"
|
||||
|
||||
@@ -4,11 +4,11 @@ type TemplateRuntime = {
|
||||
};
|
||||
|
||||
const TemplateRuntimes = {
|
||||
NODE: { name: 'node', versions: ['20.0', '19.0', '18.0', '16.0', '14.5'] },
|
||||
PHP: { name: 'php', versions: ['8.2', '8.1', '8.0'] },
|
||||
RUBY: { name: 'ruby', versions: ['3.2', '3.1', '3.0'] },
|
||||
NODE: { name: 'node', versions: ['21.0', '20.0', '19.0', '18.0', '16.0', '14.5'] },
|
||||
PHP: { name: 'php', versions: ['8.3', '8.2', '8.1', '8.0'] },
|
||||
RUBY: { name: 'ruby', versions: ['3.3', '3.2', '3.1', '3.0'] },
|
||||
PYTHON: { name: 'python', versions: ['3.12', '3.11', '3.10', '3.9', '3.8'] },
|
||||
DART: { name: 'dart', versions: ['3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
|
||||
DART: { name: 'dart', versions: ['3.3', '3.1', '3.0', '2.19', '2.18', '2.17', '2.16', '2.16'] },
|
||||
BUN: { name: 'bun', versions: ['1.0'] }
|
||||
};
|
||||
|
||||
@@ -515,6 +515,30 @@ export const marketplace: MarketplaceTemplate[] = [
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.PYTHON,
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.PHP,
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.BUN,
|
||||
'bun install',
|
||||
'src/main.ts',
|
||||
'bun/sync-with-meilisearch'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.RUBY,
|
||||
'bundle install',
|
||||
'lib/main.rb',
|
||||
'ruby/sync-with-meilisearch'
|
||||
)
|
||||
],
|
||||
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/sync-with-meilisearch">file</a>.`,
|
||||
@@ -599,6 +623,36 @@ export const marketplace: MarketplaceTemplate[] = [
|
||||
'npm install',
|
||||
'src/main.js',
|
||||
'node/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.PYTHON,
|
||||
'pip install -r requirements.txt',
|
||||
'src/main.py',
|
||||
'python/whatsapp_with_vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.PHP,
|
||||
'composer install',
|
||||
'src/index.php',
|
||||
'php/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.DART,
|
||||
'dart pub get',
|
||||
'lib/main.dart',
|
||||
'dart/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.RUBY,
|
||||
'bundle install',
|
||||
'lib/main.rb',
|
||||
'ruby/whatsapp-with-vonage'
|
||||
),
|
||||
...getRuntimes(
|
||||
TemplateRuntimes.BUN,
|
||||
'bun install',
|
||||
'src/main.ts',
|
||||
'bun/whatsapp-with-vonage'
|
||||
)
|
||||
],
|
||||
instructions: `For documentation and instructions check out <a target="_blank" rel="noopener noreferrer" class="link" href="https://github.com/appwrite/templates/tree/main/node/whatsapp-with-vonage">file</a>.`,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Execution permissions</svelte:fragment>
|
||||
<svelte:fragment slot="title">Permissions</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Choose who can execute this function using the client API. For more information, visit our
|
||||
<a
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
'/recover',
|
||||
'/invite',
|
||||
'/card',
|
||||
'/hackathon'
|
||||
'/hackathon',
|
||||
'/mfa'
|
||||
];
|
||||
if ($user) {
|
||||
if (
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
let selectedOrg = organizations.length ? organizations[0].$id : null;
|
||||
|
||||
let projects = [] as Models.ProjectList['projects'];
|
||||
$: hasProjects = projects.length > 0;
|
||||
let loadingProjects = false;
|
||||
|
||||
let projectType = 'existing';
|
||||
@@ -43,9 +42,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">
|
||||
{hasProjects ? 'Select project' : 'Create project'}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">Project</svelte:fragment>
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
{#if organizations.length > 1}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Select resources</svelte:fragment>
|
||||
<svelte:fragment slot="title">Resources</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Select the resources you need to migrate to Appwrite. Some resources can be migrated, but
|
||||
with limitations. <a
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let showSetup = false;
|
||||
export let showRecoveryCodes = false;
|
||||
|
||||
let copyParent: HTMLElement;
|
||||
let code: string;
|
||||
let type: Models.MfaType = null;
|
||||
async function addAuthenticator(): Promise<URL> {
|
||||
@@ -48,13 +49,43 @@
|
||||
Install an authenticator app on your mobile device, open it and scan the provided QR
|
||||
code or enter it manually.
|
||||
</p>
|
||||
<div class="qr">
|
||||
{#await addAuthenticator()}
|
||||
|
||||
{#await addAuthenticator()}
|
||||
<div class="qr">
|
||||
<LoadingDots />
|
||||
{:then qr}
|
||||
</div>
|
||||
{:then qr}
|
||||
<div class="qr">
|
||||
<img alt="MFA QR Code" class="code" src={qr.toString()} />
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
|
||||
<div>
|
||||
<label class="label" for="manual-code">Manual entry code</label>
|
||||
<button class="tooltip" aria-label="sec retinfo">
|
||||
<span class="icon-info" aria-hidden="true"></span>
|
||||
<span class="tooltip-popup" role="tooltip">
|
||||
<p class="text u-margin-block-start-8">
|
||||
Manually enter the following code into the authenticator app
|
||||
</p>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input id="manual-code" type="text" value={type.secret} readonly={true} />
|
||||
<div class="options-list">
|
||||
<div bind:this={copyParent}>
|
||||
{#key copyParent}
|
||||
<Copy value={type.secret} appendTo="parent">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<hr />
|
||||
<FormList>
|
||||
<label for="code">Enter the 6-digit one-time code generated by the app</label>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={validateCoupon}>
|
||||
<svelte:fragment slot="title">Add credits</svelte:fragment>
|
||||
<svelte:fragment slot="title">Credits</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Add Appwrite credits to your organization. A payment method is required before credits can
|
||||
be applied.
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={handleSubmit}>
|
||||
<svelte:fragment slot="title">Payment details</svelte:fragment>
|
||||
<svelte:fragment slot="title">Details</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Add a payment method to your organization. You will not be charged until you upgrade to a
|
||||
Pro or Scale plan.
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import ChangeOrganizationTierCloud from '../changeOrganizationTierCloud.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { last } from '$lib/helpers/array';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let show = false;
|
||||
@@ -58,25 +57,14 @@
|
||||
</script>
|
||||
|
||||
<Modal bind:show title="Limit reached">
|
||||
<svelte:fragment slot="title">
|
||||
Your usage exceeds the {tierToPlan($organization.billingPlan).name} plan limits
|
||||
</svelte:fragment>
|
||||
|
||||
{#if $organization.billingPlan === BillingPlan.STARTER}
|
||||
<p class="text">
|
||||
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan. Consider upgrading to increase your resource usage.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">
|
||||
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan. Excess usage fees will apply.
|
||||
</p>
|
||||
{/if}
|
||||
<p class="text">
|
||||
Usage for <b>{$organization.name}</b> organization has reached the limits of the {tierToPlan(
|
||||
$organization.billingPlan
|
||||
).name} plan. Excess usage fees will apply.
|
||||
</p>
|
||||
|
||||
<PlanExcess {excess} currentTier={$organization.billingPlan} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<div class="u-flex u-main-space-between u-width-full-line">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Project Details</svelte:fragment>
|
||||
<svelte:fragment slot="title">Details</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
label="Name"
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
EmailTemplateType,
|
||||
EmailTemplateLocale
|
||||
} from '@appwrite.io/console';
|
||||
import Email2FaTemplate from './email2FATemplate.svelte';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
|
||||
@@ -68,6 +69,7 @@
|
||||
$: emailMagicSessionOpen = emailOpen === 'magicSession';
|
||||
$: emailResetPassword = emailOpen === 'recovery';
|
||||
$: emailInviteUser = emailOpen === 'invitation';
|
||||
$: email2FAVerificationOpen = emailOpen === 'mfaChallenge';
|
||||
|
||||
// let smsOpen = 'verification';
|
||||
// $: smsVerificationOpen = smsOpen === 'verification';
|
||||
@@ -192,6 +194,16 @@
|
||||
<p class="text">Send an invitation email to become a member of your project.</p>
|
||||
<EmailInviteTemplate />
|
||||
</CollapsibleItem>
|
||||
<CollapsibleItem
|
||||
bind:open={email2FAVerificationOpen}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
openEmail('mfaChallenge');
|
||||
}}>
|
||||
<svelte:fragment slot="title">2FA verification</svelte:fragment>
|
||||
<p class="text">Send a two-factor authentication email to a user.</p>
|
||||
<Email2FaTemplate />
|
||||
</CollapsibleItem>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import EmailTemplate from './emailTemplate.svelte';
|
||||
import LocaleOptions from './localeOptions.svelte';
|
||||
import { loadEmailTemplate } from './+page.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { baseEmailTemplate, emailTemplate } from './store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Id } from '$lib/components';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
let locale = 'en';
|
||||
let loading = false;
|
||||
|
||||
async function onLocaleChange() {
|
||||
const timeout = setTimeout(() => {
|
||||
loading = true;
|
||||
}, 1000);
|
||||
try {
|
||||
const template = await loadEmailTemplate(projectId, 'mfaChallenge', locale);
|
||||
emailTemplate.set(template);
|
||||
$baseEmailTemplate = { ...$emailTemplate };
|
||||
trackEvent(Submit.EmailChangeLocale, { locale, type: 'mfaChallenge' });
|
||||
} catch (error) {
|
||||
trackError(error, Submit.EmailChangeLocale);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="boxes-wrapper u-margin-block-start-16">
|
||||
<LocaleOptions on:select={onLocaleChange} bind:value={locale} />
|
||||
<EmailTemplate bind:loading>
|
||||
<Id value={'{{user}}'}>{'{{user}}'}</Id>
|
||||
<Id value={'{{project}}'}>{'{{project}}'}</Id>
|
||||
<Id value={'{{otp}}'}>{'{{otp}}'}</Id>
|
||||
</EmailTemplate>
|
||||
</div>
|
||||
@@ -39,7 +39,6 @@
|
||||
<div class="boxes-wrapper u-margin-block-start-16">
|
||||
<LocaleOptions on:select={onLocaleChange} bind:value={locale} />
|
||||
<EmailTemplate bind:loading>
|
||||
<Id value={'{{team}}'}>{'{{team}}'}</Id>
|
||||
<Id value={'{{user}}'}>{'{{user}}'}</Id>
|
||||
<Id value={'{{project}}'}>{'{{project}}'}</Id>
|
||||
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<div class="boxes-wrapper u-margin-block-start-16">
|
||||
<LocaleOptions on:select={onLocaleChange} bind:value={locale} />
|
||||
<EmailTemplate bind:loading>
|
||||
<Id value={'{{team}}'}>{'{{team}}'}</Id>
|
||||
<Id value={'{{user}}'}>{'{{user}}'}</Id>
|
||||
<Id value={'{{project}}'}>{'{{project}}'}</Id>
|
||||
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<LocaleOptions on:select={onLocaleChange} bind:value={locale} />
|
||||
|
||||
<EmailTemplate bind:loading>
|
||||
<Id value={'{{team}}'}>{'{{team}}'}</Id>
|
||||
<Id value={'{{user}}'}>{'{{user}}'}</Id>
|
||||
<Id value={'{{project}}'}>{'{{project}}'}</Id>
|
||||
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
async function deleteProvider() {
|
||||
try {
|
||||
await sdk.forConsole.users.deleteAuthenticator($user.$id, AuthenticatorType.Totp, '');
|
||||
await sdk.forConsole.users.deleteAuthenticator($user.$id, AuthenticatorType.Totp);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.FACTORS);
|
||||
showDelete = false;
|
||||
@@ -43,12 +43,8 @@
|
||||
state="warning"
|
||||
bind:error
|
||||
headerDivider={false}>
|
||||
<p class="u-bold">
|
||||
Deleting the authentication method will disable multi-factor authentication for this
|
||||
account. To re-enable it, user will need to add a new authentication method.
|
||||
</p>
|
||||
<p>Are you sure you want to delete this authentication method from your account?</p>
|
||||
|
||||
<p>Are you sure you want to delete this authentication?</p>
|
||||
<p class="text u-bold">This action is irreversible.</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import EmptySearch from '$lib/components/emptySearch.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Form, Button, FormList, InputChoice } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -11,41 +11,94 @@
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { onMount } from 'svelte';
|
||||
import DeleteMfa from './deleteMfa.svelte';
|
||||
import { userFactors } from './store';
|
||||
import { user } from './store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
let showDelete = false;
|
||||
let userMfa: boolean = null;
|
||||
|
||||
onMount(async () => {
|
||||
userMfa ??= $user.mfa;
|
||||
});
|
||||
|
||||
async function updateMfa() {
|
||||
try {
|
||||
await sdk.forProject.users.updateMfa($user.$id, userMfa);
|
||||
await invalidate(Dependencies.USER);
|
||||
addNotification({
|
||||
message: `Multi-factor authentication has been ${userMfa ? 'enabled' : 'disabled'}`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.UserUpdateEmail);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.UserUpdateEmail);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
|
||||
<p class="text">MFA allow users to enhance the security of their account in you app.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if $userFactors.totp}
|
||||
<Table noMargin noStyles transparent>
|
||||
<TableHeader>
|
||||
<TableCellHead>Authenticator</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCellText title="Authenticator">TOTP (One-time code)</TableCellText>
|
||||
<TableCell>
|
||||
<Button round text ariaLabel="Delete authenticator">
|
||||
<span class="icon-trash" aria-hidden="true" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button disabled text noMargin>
|
||||
<span class="icon-plus" />
|
||||
<span class="text">Add authentication factor</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<EmptySearch hidePagination hidePages>
|
||||
<p class="text u-text-center">
|
||||
No authenticators have been enabled. Once the user adds an authenticator, you’ll
|
||||
see it here.
|
||||
</p>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
<Form onSubmit={updateMfa}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
|
||||
<p class="text">MFA allow users to enhance the security of their account in you app.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="mfa"
|
||||
label="Multi-factor authentication"
|
||||
bind:value={userMfa} />
|
||||
</FormList>
|
||||
{#if $userFactors.totp}
|
||||
<Table noMargin noStyles transparent>
|
||||
<TableHeader>
|
||||
<TableCellHead>Authenticator</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCellText title="Authenticator"
|
||||
>TOTP (One-time code)</TableCellText>
|
||||
<TableCell>
|
||||
<Button
|
||||
on:click={() => (showDelete = true)}
|
||||
round
|
||||
text
|
||||
ariaLabel="Delete authenticator">
|
||||
<span class="icon-trash" aria-hidden="true" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button disabled text noMargin>
|
||||
<span class="icon-plus" />
|
||||
<span class="text">Add authentication factor</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<EmptySearch hidePagination hidePages>
|
||||
<p class="text u-text-center">
|
||||
No authenticators have been enabled. Once the user adds an authenticator,
|
||||
you’ll see it here.
|
||||
</p>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={userMfa === $user.mfa} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<DeleteMfa bind:showDelete />
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Create data</svelte:fragment>
|
||||
<svelte:fragment slot="title">Data</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Provide document data based on attributes you created earlier.
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -121,11 +121,7 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`{ "myKey": "myValue" }`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
<InputTextarea label="Body" placeholder={`Hello, World!`} id="body" bind:value={body} />
|
||||
{:else}
|
||||
<InputText label="Path" id="path" placeholder="/" bind:value={path} required />
|
||||
|
||||
@@ -214,7 +210,7 @@
|
||||
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`{ "myKey": "myValue" }`}
|
||||
placeholder={`Hello, World!`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
</FormList>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Configuration function</svelte:fragment>
|
||||
<svelte:fragment slot="title">Configuration</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">Create your new Appwrite Function.</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
|
||||
@@ -27,19 +27,21 @@
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { Container, ContainerHeader } from '$lib/layout';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import CreateMessageDropdown from './createMessageDropdown.svelte';
|
||||
import FailedModal from './failedModal.svelte';
|
||||
import MessageStatusPill from './messageStatusPill.svelte';
|
||||
import ProviderType from './providerType.svelte';
|
||||
import { columns, showCreate } from './store';
|
||||
import { showCreate } from './store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let data: PageData;
|
||||
let selected: string[] = [];
|
||||
@@ -47,9 +49,16 @@
|
||||
let deleting = false;
|
||||
let showFailed = false;
|
||||
let errors: string[] = [];
|
||||
let showCreateDropdownMobile = false;
|
||||
let showCreateDropdownDesktop = false;
|
||||
let showCreateDropdownEmpty = false;
|
||||
const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'message', title: 'Message', type: 'string', show: false, filter: false, width: 140 },
|
||||
{ id: 'providerType', title: 'Type', type: 'string', show: true, width: 100 },
|
||||
{ id: 'status', title: 'Status', type: 'string', show: true, width: 120 },
|
||||
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', show: true, width: 120 },
|
||||
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', show: false, width: 120 }
|
||||
]);
|
||||
|
||||
const project = $page.params.project;
|
||||
|
||||
@@ -82,46 +91,42 @@
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Messages</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
<ContainerHeader title="Messages">
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
|
||||
</ContainerHeader>
|
||||
<div class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-16">
|
||||
<SearchQuery
|
||||
fullWidth
|
||||
search={data.search}
|
||||
placeholder="Search by message ID, description, type, or status">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<!-- TODO: make this not database-specific -->
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
|
||||
<div class="is-not-mobile u-width-full-line">
|
||||
<div class="u-flex u-gap-16 u-main-end">
|
||||
<!-- TODO: make this not database-specific -->
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="is-only-mobile u-flex u-gap-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
showColsTextMobile
|
||||
fullWidthMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
<Filters query={data.query} {columns} fullWidthMobile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.messages.total}
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
@@ -141,7 +146,7 @@
|
||||
<TableCellCheck
|
||||
bind:selectedIds={selected}
|
||||
id={message.$id}
|
||||
disabled={message.status === MessageStatus.Processing} />
|
||||
disabled={message.status === 'processing'} />
|
||||
|
||||
{#each $columns as column (column.id)}
|
||||
{#if column.show}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import Step1 from './wizard/step1.svelte';
|
||||
import Step2 from './wizard/step2.svelte';
|
||||
import Step3 from './wizard/step3.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { project } from '../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providerType, messageParams } from './wizard/store';
|
||||
import { ID, MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let response: Models.Message;
|
||||
const messageId = $messageParams[$providerType].messageId || ID.unique();
|
||||
|
||||
switch ($providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
response = await sdk.forProject.messaging.createEmail(
|
||||
messageId,
|
||||
$messageParams[$providerType].subject,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].draft,
|
||||
$messageParams[$providerType].html,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
response = await sdk.forProject.messaging.createSms(
|
||||
messageId,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
$messageParams[$providerType].draft,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
{
|
||||
const customData: Record<string, string> = {};
|
||||
const { data } = $messageParams[MessagingProviderType.Push];
|
||||
if (data && data.length > 0) {
|
||||
data.forEach((item) => {
|
||||
if (item[0] === '') return;
|
||||
customData[item[0]] = item[1];
|
||||
});
|
||||
}
|
||||
|
||||
response = await sdk.forProject.messaging.createPush(
|
||||
messageId,
|
||||
$messageParams[$providerType].title,
|
||||
$messageParams[$providerType].body,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
customData,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].draft,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
wizard.hide();
|
||||
let message = '';
|
||||
switch (response.status) {
|
||||
case 'draft':
|
||||
message = 'The message has been saved as draft.';
|
||||
break;
|
||||
case 'processing':
|
||||
message = 'The message is queued for processing.';
|
||||
break;
|
||||
case 'scheduled':
|
||||
message = 'The message has been scheduled.';
|
||||
break;
|
||||
}
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageCreate, {
|
||||
providerType: $providerType,
|
||||
status: response.status
|
||||
});
|
||||
await goto(`${base}/console/project-${$project.$id}/messaging/message-${response.$id}`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
$messageParams[$providerType].draft = true;
|
||||
create();
|
||||
}
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Message',
|
||||
component: Step1,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Targets',
|
||||
component: Step2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
stepsComponents.set(3, {
|
||||
label: 'Schedule',
|
||||
component: Step3,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard title="Create message" steps={stepsComponents} on:finish={create} finalAction="Send" />
|
||||
@@ -3,8 +3,8 @@
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providers } from './providers/store';
|
||||
import Wizard from './wizard.svelte';
|
||||
import { messageParams, operation, providerType, targetsById } from './wizard/store';
|
||||
import Create from './create.svelte';
|
||||
import { messageParams, providerType, targetsById } from './wizard/store';
|
||||
import { topicsById } from './store';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
$operation = 'create';
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
const common = {
|
||||
@@ -62,7 +61,7 @@
|
||||
break;
|
||||
}
|
||||
showCreateDropdown = false;
|
||||
wizard.start(Wizard);
|
||||
wizard.start(Create);
|
||||
}}>
|
||||
{option.name}
|
||||
</DropListItem>
|
||||
|
||||
@@ -1,118 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import Delete from './delete.svelte';
|
||||
import EmailPreview from './emailPreview.svelte';
|
||||
import EmailMessage from './emailMessage.svelte';
|
||||
import Overview from './overview.svelte';
|
||||
import { message } from './store';
|
||||
import SMSPreview from './smsPreview.svelte';
|
||||
import PushPreview from './pushPreview.svelte';
|
||||
import { messageParams, operation, providerType, targetsById } from '../wizard/store';
|
||||
import { topicsById } from '../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Wizard from '../wizard.svelte';
|
||||
import SMSMessage from './smsMessage.svelte';
|
||||
import PushMessage from './pushMessage.svelte';
|
||||
import { providerType } from '../wizard/store';
|
||||
import type { PageData } from './$types';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import Topics from './topics.svelte';
|
||||
import Targets from './targets.svelte';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import UpdateTopics from './updateTopics.svelte';
|
||||
import UpdateTargets from './updateTargets.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
async function onEdit() {
|
||||
if (!isValueOfStringEnum(MessagingProviderType, $message.providerType)) {
|
||||
throw new Error(`Invalid provider type: ${$message.providerType}`);
|
||||
onMount(() => {
|
||||
if (isValueOfStringEnum(MessagingProviderType, $message.providerType)) {
|
||||
$providerType = $message.providerType;
|
||||
}
|
||||
$operation = 'update';
|
||||
$providerType = $message.providerType;
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
|
||||
$topicsById = data.topicsById;
|
||||
$targetsById = data.targetsById;
|
||||
|
||||
$messageParams[$providerType] = {
|
||||
messageId: $message.$id,
|
||||
topics: $message.topics,
|
||||
users: $message.users,
|
||||
targets: $message.targets,
|
||||
status: MessageStatus.Draft,
|
||||
scheduledAt: $message.scheduledAt
|
||||
};
|
||||
|
||||
switch ($providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = ['subject', 'content', 'html'];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = ['content'];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = [
|
||||
'title',
|
||||
'body',
|
||||
'action',
|
||||
'icon',
|
||||
'sound',
|
||||
'color',
|
||||
'tag',
|
||||
'badge'
|
||||
];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
const dataEntries: [string, string][] = [];
|
||||
Object.entries(data['data'] ?? {}).forEach(([key, value]) => {
|
||||
dataEntries.push([key, value.toString()]);
|
||||
});
|
||||
$messageParams[$providerType]['data'] = dataEntries.length
|
||||
? dataEntries
|
||||
: [['', '']];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
wizard.start(Wizard);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Overview />
|
||||
<Overview message={$message} topics={Object.values(data.topicsById)} />
|
||||
{#if $message.providerType === MessagingProviderType.Email}
|
||||
<EmailPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
|
||||
<EmailMessage message={$message} />
|
||||
{:else if $message.providerType === MessagingProviderType.Sms}
|
||||
<SMSPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
|
||||
<SMSMessage message={$message} />
|
||||
{:else if $message.providerType === MessagingProviderType.Push}
|
||||
<PushPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
|
||||
<PushMessage message={$message} />
|
||||
{/if}
|
||||
<Topics topics={Object.values(data.topicsById)} />
|
||||
<Targets targets={Object.values(data.targetsById)} usersById={data.usersById} />
|
||||
{#if $message.status !== MessageStatus.Processing}
|
||||
<UpdateTopics message={$message} selectedTopicsById={data.topicsById} />
|
||||
<UpdateTargets message={$message} selectedTargetsById={data.targetsById} />
|
||||
{#if $message.status !== 'processing'}
|
||||
<Delete message={$message} />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The scheduling has been cancelled.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Cancel scheduling"
|
||||
bind:show
|
||||
onSubmit={update}
|
||||
headerDivider={false}
|
||||
size="small"
|
||||
icon="exclamation"
|
||||
state="warning">
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p data-private>
|
||||
Are you sure you want to cancel the scheduling of <span class="u-bold"
|
||||
>{message.data.title ??
|
||||
message.data.subject ??
|
||||
message.data.content ??
|
||||
'Message'}</span
|
||||
>?
|
||||
</p>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary submit>Cancel scheduling</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormList,
|
||||
InputSwitch,
|
||||
InputText,
|
||||
InputTextarea
|
||||
} from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let subject = '';
|
||||
let content = '';
|
||||
let html = false;
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
subject = message.data.subject;
|
||||
content = message.data.content;
|
||||
html = (message.data['html'] ?? false) as boolean;
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
subject,
|
||||
content,
|
||||
undefined,
|
||||
html
|
||||
);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled =
|
||||
subject === message.data.subject &&
|
||||
content === message.data.content &&
|
||||
html === ((message.data['html'] ?? false) as boolean);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={subject}></InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={content}></InputTextarea>
|
||||
<InputSwitch label="HTML mode" id="html" bind:value={html}>
|
||||
<svelte:fragment slot="description">
|
||||
Enable the HTML mode if your message contains HTML tags.
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
disabled={true}
|
||||
bind:value={message.data.subject}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,64 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { message } from './store';
|
||||
import ProviderType from '../providerType.svelte';
|
||||
import MessageStatusPill from '../messageStatusPill.svelte';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import SendModal from './sendModal.svelte';
|
||||
import ScheduleModal from './scheduleModal.svelte';
|
||||
import CancelModal from './cancelModal.svelte';
|
||||
|
||||
let scheduledAt: string = '';
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let showSend = false;
|
||||
let showSchedule = false;
|
||||
let showCancel = false;
|
||||
let showFailed = false;
|
||||
let errors: string[] = [];
|
||||
|
||||
if ($message.status === 'sent') {
|
||||
scheduledAt = $message.deliveredAt;
|
||||
} else if ($message.status === 'scheduled') {
|
||||
scheduledAt = $message.scheduledAt;
|
||||
}
|
||||
|
||||
let providerType = 'Invalid provider type';
|
||||
switch ($message.providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
providerType = 'Email';
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
providerType = 'SMS';
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
providerType = 'Push';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid hideFooter={$message.status != 'failed'}>
|
||||
<CardGrid hideFooter={['processing', 'sent'].includes(message.status)}>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
|
||||
<ProviderType type={$message.providerType} size="l">
|
||||
<Heading tag="h6" size="7">{providerType}</Heading>
|
||||
<ProviderType type={message.providerType} size="l">
|
||||
<Heading tag="h6" size="7">
|
||||
<ProviderType type={message.providerType} noIcon />
|
||||
</Heading>
|
||||
</ProviderType>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div data-private>
|
||||
<p class="title">Created: {toLocaleDateTime($message.$createdAt)}</p>
|
||||
<p class="title">Scheduled at: {toLocaleDateTime(scheduledAt)}</p>
|
||||
<p class="title">Created: {toLocaleDateTime(message.$createdAt)}</p>
|
||||
{#if message.scheduledAt}
|
||||
<p class="title">Scheduled at: {toLocaleDateTime(message.scheduledAt)}</p>
|
||||
{/if}
|
||||
{#if message.deliveredAt}
|
||||
<p class="title">Sent at: {toLocaleDateTime(message.deliveredAt)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<MessageStatusPill status={$message.status} />
|
||||
<MessageStatusPill status={message.status} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
secondary
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
errors = $message.deliveryErrors;
|
||||
showFailed = true;
|
||||
}}>View logs</Button>
|
||||
{#if message.status === 'draft'}
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button text on:click={() => (showSchedule = true)}>Schedule</Button>
|
||||
<Button secondary on:click={() => (showSend = true)}>Send message</Button>
|
||||
</div>
|
||||
{:else if message.status === 'scheduled'}
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button text on:click={() => (showCancel = true)}>Cancel scheduling</Button>
|
||||
<Button secondary on:click={() => (showSchedule = true)}>Reschedule</Button>
|
||||
</div>
|
||||
{:else if message.status === 'failed'}
|
||||
<Button
|
||||
secondary
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
errors = message.deliveryErrors;
|
||||
showFailed = true;
|
||||
}}>View logs</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<ScheduleModal bind:show={showSchedule} {message} {topics} />
|
||||
|
||||
<SendModal bind:show={showSend} {message} {topics} />
|
||||
|
||||
<CancelModal bind:show={showCancel} {message} />
|
||||
|
||||
<FailedModal bind:show={showFailed} {errors} />
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
FormItemPart,
|
||||
FormList,
|
||||
Helper,
|
||||
InputText,
|
||||
InputTextarea,
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { validateData } from '../wizard/pushFormList.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let title = '';
|
||||
let body = '';
|
||||
let originalCustomData: [string, string][] = [['', '']];
|
||||
let customData: [string, string][] = [['', '']];
|
||||
let dataError = '';
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
title = message.data.title;
|
||||
body = message.data.body;
|
||||
|
||||
const dataEntries: [string, string][] = [];
|
||||
Object.entries(message.data['data'] ?? {}).forEach(([key, value]) => {
|
||||
dataEntries.push([key, value.toString()]);
|
||||
});
|
||||
customData = dataEntries.length ? dataEntries : [['', '']];
|
||||
originalCustomData = structuredClone(customData);
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
const data = customData.reduce((acc, [key, value]) => {
|
||||
if (key) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
title,
|
||||
body,
|
||||
data
|
||||
);
|
||||
originalCustomData = structuredClone(customData);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: dataError = validateData(customData || []);
|
||||
$: disabled =
|
||||
title === message.data.title &&
|
||||
body === message.data.body &&
|
||||
originalCustomData.length === customData.length &&
|
||||
originalCustomData.every(
|
||||
(ocd, i) =>
|
||||
ocd.length === customData[i].length && ocd.every((v, j) => v === customData[i][j])
|
||||
);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
<div class="u-flex u-margin-block-start-24 u-width-full-line">
|
||||
<PushPhone {title} {body} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="title"
|
||||
label="Title"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={title}></InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={body}></InputTextarea>
|
||||
<form class="form">
|
||||
<FormItem>
|
||||
<Label
|
||||
tooltip="A key/value payload of additional metadata that's hidden from users. Use this to include information to support logic such as redirection and routing."
|
||||
>Custom data <span class="u-color-text-gray">(Optional)</span></Label>
|
||||
</FormItem>
|
||||
<div class=" u-grid u-gap-8">
|
||||
<ul class="form-list" style="--p-form-list-gap: 1rem">
|
||||
{#each customData || [] as _, rowIndex}
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
id={`${rowIndex}-key`}
|
||||
isMultiple
|
||||
fullWidth
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={customData[rowIndex][0]}
|
||||
placeholder="Enter key"
|
||||
label="Key"
|
||||
showLabel={false} />
|
||||
|
||||
<InputText
|
||||
id={`${rowIndex}-value`}
|
||||
isMultiple
|
||||
fullWidth
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={customData[rowIndex][1]}
|
||||
placeholder="Enter value"
|
||||
label="Value"
|
||||
showLabel={false}
|
||||
required />
|
||||
<FormItemPart alignEnd>
|
||||
<Button
|
||||
text
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => {
|
||||
if (customData.length === 1) {
|
||||
customData = [['', '']];
|
||||
return;
|
||||
}
|
||||
|
||||
customData = customData.filter(
|
||||
(_, i) => i !== rowIndex
|
||||
);
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</FormItemPart>
|
||||
</FormItem>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if dataError}
|
||||
<Helper type="warning">{dataError}</Helper>
|
||||
{/if}
|
||||
<Button
|
||||
noMargin
|
||||
text
|
||||
disabled={customData && customData[customData.length - 1][0] === ''}
|
||||
on:click={() => {
|
||||
customData = [...customData, ['', '']];
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add data</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<div class="u-flex u-margin-block-start-24 u-width-full-line">
|
||||
<PushPhone title={message.data.title} body={message.data.body} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText id="title" label="Title" disabled={true} bind:value={message.data.title}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.body}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputDate, InputTime, Helper } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let now = new Date();
|
||||
let minDate: string;
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
let date = message.scheduledAt == null ? null : toLocaleDateISO(message.scheduledAt);
|
||||
let time = message.scheduledAt == null ? null : toLocaleTimeISO(message.scheduledAt);
|
||||
let dateTime: Date;
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hourCycle: 'h23',
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been scheduled and will be sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
$: minDate = toLocaleDateISO(now.getTime());
|
||||
$: minTime = isSameDay(new Date(date), new Date(minDate))
|
||||
? toLocaleTimeISO(now.getTime())
|
||||
: '00:00';
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
</script>
|
||||
|
||||
<Modal title="Schedule message" bind:show onSubmit={update} headerDivider={false} size="big">
|
||||
<div>
|
||||
<FormList>
|
||||
<div
|
||||
class="u-grid u-gap-16"
|
||||
style="grid-auto-rows: 1fr; grid-template-columns: 1fr 1fr;">
|
||||
<InputDate id="date" label="Date" required={true} min={minDate} bind:value={date} />
|
||||
<InputTime id="time" label="Time" required={true} min={minTime} bind:value={time} />
|
||||
</div>
|
||||
</FormList>
|
||||
<Helper type="neutral">
|
||||
{#if !dateTime || isNaN(dateTime.getTime())}
|
||||
The message will be sent later
|
||||
{:else}
|
||||
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
|
||||
{/if}
|
||||
</Helper>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Schedule</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title="Send message" bind:show onSubmit={update} headerDivider={false} size="small">
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p data-private>
|
||||
You are about to send a message to an estimated <span class="u-bold"
|
||||
>{totalTargets}</span> targets. Would you like to proceed?
|
||||
</p>
|
||||
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Send</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, Form, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let content = '';
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
content = message.data.content;
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
content
|
||||
);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = content === message.data.content;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
<SMSPhone {content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={content}></InputTextarea>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<SMSPhone content={message.data.content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let targets: Models.Target[];
|
||||
export let usersById: Record<string, Models.User<Models.Preferences>>;
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targets.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Identifier</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={target.$id}>{target.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{usersById[target.userId]?.name || 'Unknown'}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{target.providerType === MessagingProviderType.Push
|
||||
? target.name
|
||||
: target.identifier}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Target</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topics.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Subscribers</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={topic.$id}>{topic.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{topic.name}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{topic.smsTotal + topic.emailTotal + topic.pushTotal}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Topic</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import UserTargetsModal from '../userTargetsModal.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let selectedTargetsById: Record<string, Models.Target>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTargets = false;
|
||||
let targetsById: Record<string, Models.Target>;
|
||||
let targetIds: string[] = [];
|
||||
let targets: Models.Target[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
targetsById = { ...selectedTargetsById };
|
||||
});
|
||||
|
||||
function removeTarget(targetId: string) {
|
||||
const { [targetId]: _, ...rest } = targetsById;
|
||||
targetsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Targets have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
targetIds = [];
|
||||
targets = [];
|
||||
for (const targetId in targetsById) {
|
||||
targetIds.push(targetId);
|
||||
targets.push(targetsById[targetId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(targetIds, Object.keys(selectedTargetsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targetIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Target</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTargets = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow />
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Target">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2" data-private>
|
||||
{#if target.providerType === MessagingProviderType.Push}
|
||||
{target.name}
|
||||
{:else}
|
||||
{target.identifier}
|
||||
{/if}
|
||||
</span>
|
||||
</span></span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove">
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTarget(target.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTargets = true)}>Add a target</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No targets have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/targets"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<UserTargetsModal
|
||||
title="Select targets"
|
||||
{providerType}
|
||||
bind:show={showTargets}
|
||||
{targetsById}
|
||||
on:update={(e) => {
|
||||
showTargets = false;
|
||||
targetsById = e.detail;
|
||||
}}>
|
||||
<svelte:fragment slot="description"
|
||||
>Select existing targets to which you want to send this message.</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { type Models, MessagingProviderType } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import TopicsModal from '../topicsModal.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Form, Button } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { getTotal } from '../wizard/store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message;
|
||||
export let selectedTopicsById: Record<string, Models.Topic>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTopics = false;
|
||||
let topicsById: Record<string, Models.Topic> = {};
|
||||
let topicIds: string[] = [];
|
||||
let topics: Models.Topic[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
topicsById = { ...selectedTopicsById };
|
||||
});
|
||||
|
||||
function removeTopic(topicId: string) {
|
||||
const { [topicId]: _, ...rest } = topicsById;
|
||||
topicsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(message.$id, topicIds);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Topics have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
topicIds = [];
|
||||
topics = [];
|
||||
for (const topicId in topicsById) {
|
||||
topicIds.push(topicId);
|
||||
topics.push(topicsById[topicId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(topicIds, Object.keys(selectedTopicsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topicIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Topic</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTopics = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Topic">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2 u-bold" data-private>
|
||||
{topic.name}
|
||||
</span>
|
||||
<span class="collapsible-button-optional">
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove" width={40}>
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTopic(topic.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No topics have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<TopicsModal
|
||||
{providerType}
|
||||
bind:show={showTopics}
|
||||
{topicsById}
|
||||
on:update={(e) => {
|
||||
showTopics = false;
|
||||
topicsById = e.detail;
|
||||
}} />
|
||||
@@ -44,8 +44,8 @@
|
||||
providerId,
|
||||
$providerParams[$provider].name,
|
||||
$providerParams[$provider].from,
|
||||
$providerParams[$provider].username,
|
||||
$providerParams[$provider].password,
|
||||
$providerParams[$provider].customerId,
|
||||
$providerParams[$provider].apiKey,
|
||||
$providerParams[$provider].enabled
|
||||
);
|
||||
break;
|
||||
@@ -156,7 +156,7 @@
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Proivder',
|
||||
label: 'Provider',
|
||||
component: Provider
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
|
||||
+3
-3
@@ -48,8 +48,8 @@
|
||||
providerId: $providerData.$id,
|
||||
name: $providerData.name,
|
||||
enabled: $providerData.enabled,
|
||||
username: $providerData.credentials['username'],
|
||||
password: $providerData.credentials['password'],
|
||||
customerId: $providerData.credentials['customerId'],
|
||||
apiKey: $providerData.credentials['apiKey'],
|
||||
from: $providerData.options['from']
|
||||
};
|
||||
break;
|
||||
@@ -78,7 +78,7 @@
|
||||
providerId: $providerData.$id,
|
||||
name: $providerData.name,
|
||||
enabled: $providerData.enabled,
|
||||
isEuRegion: false,
|
||||
isEuRegion: $providerData.credentials['isEuRegion'],
|
||||
fromEmail: $providerData.options['fromEmail'],
|
||||
fromName: $providerData.options['fromName'],
|
||||
replyToEmail: $providerData.options['replyToEmail'],
|
||||
|
||||
+14
-5
@@ -13,12 +13,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import CreateMember from '$routes/console/organization-[organization]/createMember.svelte';
|
||||
|
||||
export let inputs: ProviderInput[];
|
||||
export let inputs: (ProviderInput | ProviderInput[])[];
|
||||
export let providerType: MessagingProviderType;
|
||||
export let provider: Providers;
|
||||
export let providerId: string;
|
||||
export let params: object;
|
||||
|
||||
let flattened: ProviderInput[] = [];
|
||||
let formValues: object = {};
|
||||
let originalfiles: Record<string, FileList> = {};
|
||||
let files: Record<string, FileList> = {};
|
||||
@@ -41,7 +42,15 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
for (const input of inputs) {
|
||||
flattened = [];
|
||||
for (const i of inputs) {
|
||||
if (Array.isArray(i)) {
|
||||
flattened.push(...i);
|
||||
} else {
|
||||
flattened.push(i);
|
||||
}
|
||||
}
|
||||
for (const input of flattened) {
|
||||
const value = params[input.name];
|
||||
formValues[input.name] = value;
|
||||
if (input.type === 'file' && typeof value == 'string' && value.length > 0) {
|
||||
@@ -91,8 +100,8 @@
|
||||
providerId,
|
||||
undefined,
|
||||
undefined,
|
||||
formValues['username'],
|
||||
formValues['password'],
|
||||
formValues['customerId'],
|
||||
formValues['apiKey'],
|
||||
formValues['from']
|
||||
);
|
||||
break;
|
||||
@@ -198,7 +207,7 @@
|
||||
}
|
||||
|
||||
$: updated =
|
||||
inputs
|
||||
flattened
|
||||
.filter((input) => input.type !== 'file')
|
||||
.some((input) => formValues[input.name] !== params[input.name]) ||
|
||||
Object.keys(files).some((key) => files[key] !== originalfiles[key]);
|
||||
|
||||
+8
-4
@@ -134,16 +134,20 @@
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div data-private>
|
||||
<div data-private class="u-flex-vertical u-gap-16">
|
||||
<ul>
|
||||
<InputSwitch
|
||||
id="enabled"
|
||||
label={enabled ? 'Enabled' : 'Disabled'}
|
||||
bind:value={enabled} />
|
||||
</ul>
|
||||
<p class="title">Provider: <Provider noIcon provider={$providerData.provider} /></p>
|
||||
<p class="title">Channel: <ProviderType noIcon type={$providerData.type} /></p>
|
||||
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
|
||||
<div>
|
||||
<p class="title">
|
||||
Provider: <Provider noIcon provider={$providerData.provider} />
|
||||
</p>
|
||||
<p class="title">Type: <ProviderType noIcon type={$providerData.type} /></p>
|
||||
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
InputDomain,
|
||||
InputEmail,
|
||||
InputFile,
|
||||
InputSelect,
|
||||
InputPhone,
|
||||
InputSwitch,
|
||||
InputText,
|
||||
InputPassword
|
||||
} from '$lib/elements/forms';
|
||||
import InputCheckbox from '$lib/elements/forms/inputCheckbox.svelte';
|
||||
import PopoverContent from './popoverContent.svelte';
|
||||
import type {
|
||||
ProviderInput,
|
||||
TwilioProviderParams,
|
||||
Msg91ProviderParams,
|
||||
TelesignProviderParams,
|
||||
TextmagicProviderParams,
|
||||
VonageProviderParams,
|
||||
MailgunProviderParams,
|
||||
SendgridProviderParams,
|
||||
SMTPProviderParams,
|
||||
FCMProviderParams,
|
||||
APNSProviderParams
|
||||
} from './store';
|
||||
|
||||
export let files: Record<string, FileList>;
|
||||
export let input: ProviderInput;
|
||||
export let params: Partial<
|
||||
| TwilioProviderParams
|
||||
| Msg91ProviderParams
|
||||
| TelesignProviderParams
|
||||
| TextmagicProviderParams
|
||||
| VonageProviderParams
|
||||
| MailgunProviderParams
|
||||
| SendgridProviderParams
|
||||
| SMTPProviderParams
|
||||
| FCMProviderParams
|
||||
| APNSProviderParams
|
||||
>;
|
||||
export let fullWidth = false;
|
||||
|
||||
let popoverImage = input.popoverImage;
|
||||
</script>
|
||||
|
||||
{#if input.type === 'text'}
|
||||
<InputText
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
tooltip={input.tooltip}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
{fullWidth}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputText>
|
||||
{:else if input.type === 'password'}
|
||||
<InputPassword
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
showPasswordButton
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
{fullWidth}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputPassword>
|
||||
{:else if input.type === 'email'}
|
||||
<InputEmail
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
tooltip={input.tooltip}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
{fullWidth}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputEmail>
|
||||
{:else if input.type === 'domain'}
|
||||
<InputDomain
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
{fullWidth}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputDomain>
|
||||
{:else if input.type === 'phone'}
|
||||
<InputPhone
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
{fullWidth}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputPhone>
|
||||
{:else if input.type === 'file'}
|
||||
<InputFile
|
||||
label={input.label}
|
||||
allowedFileExtensions={input.allowedFileExtensions}
|
||||
required={!input.optional}
|
||||
tooltip={input.tooltip}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:files={files[input.name]}>
|
||||
{fullWidth}
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputFile>
|
||||
{:else if input.type === 'switch'}
|
||||
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="description">
|
||||
<p>{@html input.description}</p>
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
{:else if input.type === 'checkbox'}
|
||||
<InputCheckbox label={input.label} id={input.name} bind:checked={params[input.name]}>
|
||||
<svelte:fragment slot="description">
|
||||
{input.description}
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
{:else if input.type === 'select'}
|
||||
<InputSelect
|
||||
label={input.label}
|
||||
id={input.name}
|
||||
options={input.options}
|
||||
required={!input.optional}
|
||||
bind:value={params[input.name]} />
|
||||
{/if}
|
||||
@@ -1,17 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FormList,
|
||||
InputDomain,
|
||||
InputEmail,
|
||||
InputFile,
|
||||
InputSelect,
|
||||
InputPhone,
|
||||
InputSwitch,
|
||||
InputText,
|
||||
InputPassword
|
||||
} from '$lib/elements/forms';
|
||||
import InputCheckbox from '$lib/elements/forms/inputCheckbox.svelte';
|
||||
import PopoverContent from './popoverContent.svelte';
|
||||
import { FormItem, FormList } from '$lib/elements/forms';
|
||||
import SettingsFormInput from './settingsFormInput.svelte';
|
||||
import type {
|
||||
ProviderInput,
|
||||
TwilioProviderParams,
|
||||
@@ -27,7 +16,7 @@
|
||||
} from './store';
|
||||
|
||||
export let files: Record<string, FileList>;
|
||||
export let inputs: ProviderInput[];
|
||||
export let inputs: (ProviderInput | ProviderInput[])[];
|
||||
export let params: Partial<
|
||||
| TwilioProviderParams
|
||||
| Msg91ProviderParams
|
||||
@@ -44,98 +33,16 @@
|
||||
|
||||
<FormList>
|
||||
{#each inputs as input}
|
||||
{@const popoverImage = input.popoverImage}
|
||||
{#if input.type === 'text'}
|
||||
<InputText
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputText>
|
||||
{:else if input.type === 'password'}
|
||||
<InputPassword
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
showPasswordButton
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputPassword>
|
||||
{:else if input.type === 'email'}
|
||||
<InputEmail
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputEmail>
|
||||
{:else if input.type === 'domain'}
|
||||
<InputDomain
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputDomain>
|
||||
{:else if input.type === 'phone'}
|
||||
<InputPhone
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputPhone>
|
||||
{:else if input.type === 'file'}
|
||||
<InputFile
|
||||
label={input.label}
|
||||
allowedFileExtensions={input.allowedFileExtensions}
|
||||
required={!input.optional}
|
||||
isPopoverDefined={input.popover !== undefined}
|
||||
bind:files={files[input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
<PopoverContent lines={input.popover} {popoverImage} />
|
||||
</svelte:fragment>
|
||||
</InputFile>
|
||||
{:else if input.type === 'switch'}
|
||||
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
|
||||
<svelte:fragment slot="description">
|
||||
{input.description}
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
{:else if input.type === 'checkbox'}
|
||||
<InputCheckbox label={input.label} id={input.name} bind:checked={params[input.name]}>
|
||||
<svelte:fragment slot="description">
|
||||
{input.description}
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
{:else if input.type === 'select'}
|
||||
<InputSelect
|
||||
label={input.label}
|
||||
id={input.name}
|
||||
options={input.options}
|
||||
required={!input.optional}
|
||||
bind:value={params[input.name]} />
|
||||
{#if Array.isArray(input)}
|
||||
<FormItem isMultiple>
|
||||
{#each input as i}
|
||||
<div class="u-flex u-flex-basis-50-percent">
|
||||
<SettingsFormInput input={i} bind:params bind:files fullWidth />
|
||||
</div>
|
||||
{/each}
|
||||
</FormItem>
|
||||
{:else}
|
||||
<SettingsFormInput {input} bind:params bind:files />
|
||||
{/if}
|
||||
{/each}
|
||||
</FormList>
|
||||
|
||||
@@ -26,6 +26,7 @@ export type ProviderInput = {
|
||||
| 'checkbox';
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
tooltip?: string;
|
||||
popover?: string[];
|
||||
popoverImage?: { src: { light: string; dark: string }; alt: string };
|
||||
allowedFileExtensions?: string[];
|
||||
@@ -45,7 +46,7 @@ type ProvidersMap = {
|
||||
title: string;
|
||||
description: string;
|
||||
needAHand?: string[];
|
||||
configure: ProviderInput[];
|
||||
configure: (ProviderInput | ProviderInput[])[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -167,7 +168,8 @@ export const providers: ProvidersMap = {
|
||||
label: 'Sandbox',
|
||||
name: 'sandbox',
|
||||
type: 'switch',
|
||||
description: 'Use the development APNS server.'
|
||||
description:
|
||||
'Enable sandbox mode for testing on apps signed with development provisioning profiles. <a class="link" href="https://appwrite.io/docs/products/messaging/apns#configure-provider" target="_blank" rel="noopener noreferrer">Learn more</a>.'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -217,33 +219,37 @@ export const providers: ProvidersMap = {
|
||||
description:
|
||||
'Enable the EU region setting if your domain is within the European Union.'
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Reply to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
[
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Reply-to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
[Providers.Sendgrid]: {
|
||||
@@ -262,66 +268,44 @@ export const providers: ProvidersMap = {
|
||||
'Head to <b>Settings -> API Keys -> Create API key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Reply to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
[
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Reply-to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
[Providers.SMTP]: {
|
||||
classIcon: 'mail',
|
||||
title: 'SMTP',
|
||||
description: '',
|
||||
description: 'Generic SMTP server.',
|
||||
configure: [
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Reply to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Server host',
|
||||
name: 'host',
|
||||
@@ -366,16 +350,45 @@ export const providers: ProvidersMap = {
|
||||
type: 'checkbox',
|
||||
optional: true
|
||||
},
|
||||
[
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Reply-to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
optional: true,
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
],
|
||||
{
|
||||
label: 'Mailer',
|
||||
label: 'X-Mailer header',
|
||||
name: 'mailer',
|
||||
type: 'text',
|
||||
optional: true,
|
||||
placeholder: 'Enter mailer',
|
||||
popover: [
|
||||
'<b>What is the Mailer?</b>',
|
||||
'The value to use for the X-Mailer header.'
|
||||
]
|
||||
tooltip:
|
||||
'The X-Mailer header is used to identify the software or client used for composing or sending the email. It typically includes information such as the name and version of the email program, aiding in troubleshooting and debugging.'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -469,16 +482,21 @@ export const providers: ProvidersMap = {
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
label: 'Customer ID',
|
||||
name: 'customerId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter username'
|
||||
placeholder: 'Enter customer ID'
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
label: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'password',
|
||||
placeholder: 'Enter password'
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in <a class="link" href="https://portal.telesign.com/" target="_blank" rel="noopener noreferrer">Telesign</a>.',
|
||||
'Head to <b>Telesign portal -> Profile -> API Keys.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
@@ -579,8 +597,8 @@ export type Msg91ProviderParams = ProviderParams & {
|
||||
|
||||
export type TelesignProviderParams = ProviderParams & {
|
||||
from: string;
|
||||
username: string;
|
||||
password: string;
|
||||
customerId: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type TextmagicProviderParams = ProviderParams & {
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
username: '',
|
||||
password: '',
|
||||
customerId: '',
|
||||
apiKey: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
@@ -134,6 +134,7 @@
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Provider</svelte:fragment>
|
||||
<div class="u-sep-block-start u-padding-block-end-32" />
|
||||
<FormList gap={16}>
|
||||
<InputText
|
||||
id="name"
|
||||
@@ -173,3 +174,15 @@
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-box {
|
||||
--grid-gap: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
:global(body:not(.theme-dark)) .grid-box :global(.card) {
|
||||
--p-card-bg-color-default: var(--color-neutral-5);
|
||||
--p-card-bg-color-hover: var(--color-neutral-10);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,15 @@
|
||||
let open = false;
|
||||
|
||||
onMount(() => {
|
||||
for (const input of inputs) {
|
||||
const flattened = [];
|
||||
for (const i of inputs) {
|
||||
if (Array.isArray(i)) {
|
||||
flattened.push(...i);
|
||||
} else {
|
||||
flattened.push(i);
|
||||
}
|
||||
}
|
||||
for (const input of flattened) {
|
||||
if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const f = new File(
|
||||
@@ -97,7 +105,7 @@
|
||||
|
||||
<ClickableList>
|
||||
<ClickableListItem
|
||||
href={`https://appwrite.io/docs/messaging/${$provider}`}
|
||||
href={`https://appwrite.io/docs/products/messaging/${$provider}`}
|
||||
external>
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
@@ -135,7 +143,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.need-a-hand {
|
||||
--p-bg-color-hover: var(--color-neutral-10);
|
||||
--p-bg-color-hover: var(--color-neutral-5);
|
||||
.how-to-enable {
|
||||
border-radius: var(--border-radius-small);
|
||||
|
||||
@@ -150,7 +158,8 @@
|
||||
}
|
||||
|
||||
:global(.clickable-list) {
|
||||
--color-border: var(--color-neutral-10);
|
||||
--color-border: var(--color-neutral-5);
|
||||
--p-clickable-button-bg-color-hover: var(--color-border);
|
||||
|
||||
:global(.theme-dark) & {
|
||||
--color-border: var(--color-neutral-85);
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const showCreate = writable(false);
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Message ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'message', title: 'Message', type: 'string', show: false, filter: false, width: 140 },
|
||||
{ id: 'providerType', title: 'Type', type: 'string', show: true, width: 100 },
|
||||
{ id: 'status', title: 'Status', type: 'string', show: true, width: 120 },
|
||||
{ id: 'scheduledAt', title: 'Scheduled at', type: 'datetime', show: true, width: 120 },
|
||||
{ id: 'deliveredAt', title: 'Delivered at', type: 'datetime', show: false, width: 120 }
|
||||
]);
|
||||
|
||||
export const targetsById = writable<Record<string, Models.Target>>({});
|
||||
export const topicsById = writable<Record<string, Models.Topic>>({});
|
||||
|
||||
@@ -16,14 +16,33 @@
|
||||
import { base } from '$app/paths';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import { columns, showCreate } from './store';
|
||||
import { showCreate } from './store';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import Table from './table.svelte';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const project = $page.params.project;
|
||||
const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'name', title: 'Name', type: 'string', show: true, width: 140 },
|
||||
{ id: 'emailTotal', title: 'Email Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{ id: 'smsTotal', title: 'SMS Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{ id: 'pushTotal', title: 'Push Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{
|
||||
id: 'total',
|
||||
title: 'Subscribers',
|
||||
type: 'integer',
|
||||
show: true,
|
||||
filter: false,
|
||||
width: 140
|
||||
},
|
||||
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 }
|
||||
]);
|
||||
|
||||
const topicCreated = async (event: CustomEvent<Models.Team<Record<string, unknown>>>) => {
|
||||
await goto(`${base}/console/project-${project}/messaging/topics/topic-${event.detail.$id}`);
|
||||
};
|
||||
@@ -73,7 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if data.topics.total}
|
||||
<Table {data} />
|
||||
<Table columns={$columns} {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Topics"
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const showCreate = writable(false);
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Topic ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'name', title: 'Name', type: 'string', show: true, width: 140 },
|
||||
{ id: 'emailTotal', title: 'Email Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{ id: 'smsTotal', title: 'SMS Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{ id: 'pushTotal', title: 'Push Subscribers', type: 'integer', show: false, width: 140 },
|
||||
{ id: 'total', title: 'Subscribers', type: 'integer', show: true, filter: false, width: 140 },
|
||||
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 140 }
|
||||
]);
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
} from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
|
||||
export let columns: Column[];
|
||||
export let data: PageData;
|
||||
|
||||
let selectedIds: string[] = [];
|
||||
@@ -62,7 +63,7 @@
|
||||
<TableCellHeadCheck
|
||||
bind:selected={selectedIds}
|
||||
pageItemsIds={data.topics.topics.map((d) => d.$id)} />
|
||||
{#each $columns as column}
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
@@ -74,10 +75,10 @@
|
||||
href={`${base}/console/project-${$project.$id}/messaging/topics/topic-${topic.$id}`}>
|
||||
<TableCellCheck bind:selectedIds id={topic.$id} />
|
||||
|
||||
{#each $columns as column (column.id)}
|
||||
{#each columns as column (column.id)}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key $columns}
|
||||
{#key column.id}
|
||||
<TableCell title={column.title} width={column.width}>
|
||||
<Id value={topic.$id}>{topic.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,12 +1,178 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Empty,
|
||||
EmptySearch,
|
||||
SearchQuery,
|
||||
PaginationWithLimit,
|
||||
Heading,
|
||||
ViewSelector,
|
||||
EmptyFilter
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
import Details from './details.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import { ID, type Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Table from './table.svelte';
|
||||
import { targetsById } from '../../store';
|
||||
import UserTargetsModal from '../../userTargetsModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
|
||||
export let data: PageData;
|
||||
let showAdd = false;
|
||||
let subscribersByTargetId: Record<string, Models.Subscriber> = {};
|
||||
const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'userName', title: 'Name', type: 'string', show: true, filter: false, width: 100 },
|
||||
{ id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
|
||||
{ id: 'type', title: 'Type', type: 'string', show: true, width: 80 },
|
||||
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 100 }
|
||||
]);
|
||||
|
||||
onMount(() => {
|
||||
$targetsById = {};
|
||||
for (const subscriber of $page.data.subscribers.subscribers) {
|
||||
const { target } = subscriber;
|
||||
$targetsById[target.$id] = target;
|
||||
subscribersByTargetId[target.$id] = subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
async function addTargets(event: CustomEvent<Record<string, Models.Target>>) {
|
||||
showAdd = false;
|
||||
$targetsById = event.detail;
|
||||
|
||||
const targetIds = Object.keys($targetsById).filter(
|
||||
(targetId) => !(targetId in subscribersByTargetId)
|
||||
);
|
||||
const promises = targetIds.map(async (targetId) => {
|
||||
const subscriber = await sdk.forProject.messaging.createSubscriber(
|
||||
$page.params.topic,
|
||||
ID.unique(),
|
||||
targetId
|
||||
);
|
||||
subscribersByTargetId[targetId] = subscriber;
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Added ${targetIds.length} subscriber${
|
||||
targetIds.length > 1 ? 's' : ''
|
||||
} to topic`
|
||||
});
|
||||
trackEvent(Submit.MessagingTopicSubscriberAdd);
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingTopicSubscriberAdd);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Details />
|
||||
<UpdateName />
|
||||
<DangerZone />
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Subscribers</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<Button on:click={() => (showAdd = true)} event="create_subscriber">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add subscriber</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
<SearchQuery
|
||||
search={data.search}
|
||||
placeholder="Search by subscriber ID, target ID, user ID, or type">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<Button on:click={() => (showAdd = true)} event="create_subscriber">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add subscriber</span>
|
||||
</Button>
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.subscribers.total}
|
||||
<Table columns={$columns} {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Subscribers"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.subscribers.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="subscribers" />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no subscribers that match your search.</p>
|
||||
</div>
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/project-${$page.params.project}/messaging/topics/topic-${$page.params.topic}/subscribers`}>
|
||||
Clear Search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
on:click={() => (showAdd = true)}
|
||||
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
|
||||
target="subscriber" />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<UserTargetsModal
|
||||
title="Select subscribers"
|
||||
bind:show={showAdd}
|
||||
bind:targetsById={$targetsById}
|
||||
on:update={addTargets}>
|
||||
<svelte:fragment slot="description">
|
||||
<p class="text">
|
||||
Add subscribers to this topic by selecting the targets for directing messages. <a
|
||||
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more about subscribers</a
|
||||
>.
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
const tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Overview',
|
||||
event: 'overview'
|
||||
},
|
||||
{
|
||||
href: `${path}/subscribers`,
|
||||
title: 'Subscribers',
|
||||
event: 'subscribers'
|
||||
},
|
||||
@@ -23,6 +18,11 @@
|
||||
href: `${path}/activity`,
|
||||
title: 'Activity',
|
||||
event: 'activity'
|
||||
},
|
||||
{
|
||||
href: `${path}/settings`,
|
||||
title: 'Settings',
|
||||
event: 'settings'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
import Details from './details.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdatePermissions from './updatePermissions.svelte';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Details />
|
||||
<UpdateName />
|
||||
<UpdatePermissions />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
+6
-16
@@ -1,20 +1,10 @@
|
||||
<script lang="ts" context="module">
|
||||
import { get } from 'svelte/store';
|
||||
let showDelete = writable(false);
|
||||
export const promptDeleteUser = (id: string) => {
|
||||
showDelete.set(true);
|
||||
goto(`/console/project-${get(project).$id}/auth/user-${id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { CardGrid, BoxAvatar, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { writable } from 'svelte/store';
|
||||
import { topic, topicTotal } from './store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import DeleteTopic from './deleteTopic.svelte';
|
||||
import { topic, topicTotal } from '../store';
|
||||
import DeleteTopic from '../deleteTopic.svelte';
|
||||
|
||||
let showDelete = false;
|
||||
</script>
|
||||
|
||||
<CardGrid danger>
|
||||
@@ -37,9 +27,9 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($showDelete = true)} event="delete_messaging_provider"
|
||||
<Button secondary on:click={() => (showDelete = true)} event="delete_messaging_topic"
|
||||
>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<DeleteTopic bind:showDelete={$showDelete} />
|
||||
<DeleteTopic bind:showDelete />
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { topic, topicTotal } from './store';
|
||||
import { topic, topicTotal } from '../store';
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { topic } from './store';
|
||||
import { topic } from '../store';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { topic } from '../store';
|
||||
|
||||
const topicId = $page.params.topic;
|
||||
|
||||
let arePermsDisabled = true;
|
||||
let permissions: string[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
permissions = $topic.subscribe || [];
|
||||
});
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateTopic(topicId, undefined, permissions);
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingTopicUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingTopicUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
$: arePermsDisabled = symmetricDifference(permissions, $topic.subscribe).length == 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="permissions">Subscription access</Heading>
|
||||
<p>
|
||||
Choose who can subscribe to this topic using the client API. Learn more about <a
|
||||
href="https://appwrite.io/docs/advanced/platform/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions</a
|
||||
>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Empty,
|
||||
EmptySearch,
|
||||
SearchQuery,
|
||||
PaginationWithLimit,
|
||||
Heading,
|
||||
ViewSelector,
|
||||
EmptyFilter
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import { ID, type Models } from '@appwrite.io/console';
|
||||
import type { PageData } from './$types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import Table from './table.svelte';
|
||||
import { targetsById } from '../../../store';
|
||||
import UserTargetsModal from '../../../userTargetsModal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import { columns } from './store';
|
||||
import { View } from '$lib/helpers/load';
|
||||
|
||||
export let data: PageData;
|
||||
let showAdd = false;
|
||||
let subscribersByTargetId: Record<string, Models.Subscriber> = {};
|
||||
|
||||
onMount(() => {
|
||||
$targetsById = {};
|
||||
for (const subscriber of $page.data.subscribers.subscribers) {
|
||||
const { target } = subscriber;
|
||||
$targetsById[target.$id] = target;
|
||||
subscribersByTargetId[target.$id] = subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
async function addTargets(event: CustomEvent<Record<string, Models.Target>>) {
|
||||
showAdd = false;
|
||||
$targetsById = event.detail;
|
||||
|
||||
const targetIds = Object.keys($targetsById).filter(
|
||||
(targetId) => !(targetId in subscribersByTargetId)
|
||||
);
|
||||
const promises = targetIds.map(async (targetId) => {
|
||||
const subscriber = await sdk.forProject.messaging.createSubscriber(
|
||||
$page.params.topic,
|
||||
ID.unique(),
|
||||
targetId
|
||||
);
|
||||
subscribersByTargetId[targetId] = subscriber;
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Added ${targetIds.length} subscriber${
|
||||
targetIds.length > 1 ? 's' : ''
|
||||
} to topic`
|
||||
});
|
||||
trackEvent(Submit.MessagingTopicSubscriberAdd);
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC_SUBSCRIBERS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingTopicSubscriberAdd);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Subscribers</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<Button on:click={() => (showAdd = true)} event="create_subscriber">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add subscriber</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
<SearchQuery
|
||||
search={data.search}
|
||||
placeholder="Search by subscriber ID, target ID, user ID, or type">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<Button on:click={() => (showAdd = true)} event="create_subscriber">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add subscriber</span>
|
||||
</Button>
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.subscribers.total}
|
||||
<Table {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Subscribers"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.subscribers.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="subscribers" />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no subscribers that match your search.</p>
|
||||
</div>
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/project-${$page.params.project}/messaging/topics/topic-${$page.params.topic}/subscribers`}>
|
||||
Clear Search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
on:click={() => (showAdd = true)}
|
||||
href="https://appwrite.io/docs/products/messaging/topics/#subscribe-targets-to-topics"
|
||||
target="subscriber" />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<UserTargetsModal
|
||||
title="Select subscribers"
|
||||
bind:show={showAdd}
|
||||
bind:targetsById={$targetsById}
|
||||
on:update={addTargets}>
|
||||
<svelte:fragment slot="description">
|
||||
<p class="text">
|
||||
Add subscribers to this topic by selecting the targets for directing messages. <a
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">Learn more about subscribers.</a>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Subscriber ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'userName', title: 'Name', type: 'string', show: true, filter: false, width: 100 },
|
||||
{ id: 'targetId', title: 'Target ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
|
||||
{ id: 'type', title: 'Type', type: 'string', show: true, width: 80 },
|
||||
{ id: '$createdAt', title: 'Created', type: 'datetime', show: true, width: 100 }
|
||||
]);
|
||||
+7
-6
@@ -18,15 +18,16 @@
|
||||
} from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import ProviderType from '../../../providerType.svelte';
|
||||
import ProviderType from '../../providerType.svelte';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/stores';
|
||||
import { targetsById } from '../../../store';
|
||||
import { targetsById } from '../../store';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
|
||||
export let columns: Column[];
|
||||
export let data: PageData;
|
||||
|
||||
let subscribers: Record<string, Models.Subscriber> = {};
|
||||
@@ -84,7 +85,7 @@
|
||||
<TableCellHeadCheck
|
||||
bind:selected={selectedIds}
|
||||
pageItemsIds={data.subscribers.subscribers.map((d) => d.$id)} />
|
||||
{#each $columns as column}
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
@@ -97,10 +98,10 @@
|
||||
href={`${base}/console/project-${$project.$id}/auth/user-${subscriber.target.userId}`}>
|
||||
<TableCellCheck bind:selectedIds id={subscriber.$id} />
|
||||
|
||||
{#each $columns as column}
|
||||
{#each columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key $columns}
|
||||
{#key column.id}
|
||||
<TableCell title="Subscriber ID">
|
||||
<Id value={subscriber.$id}>
|
||||
{subscriber.$id}
|
||||
@@ -91,86 +91,105 @@
|
||||
</script>
|
||||
|
||||
<Modal {title} bind:show onSubmit={submit} on:close={reset} headerDivider={false} size="big">
|
||||
<p class="text">
|
||||
Select existing topics you want to send this message to its targets. The message will be
|
||||
sent only to <ProviderType type={providerType} noIcon /> targets.
|
||||
</p>
|
||||
<InputSearch
|
||||
autofocus
|
||||
disabled={totalResults === 0 && !search}
|
||||
placeholder="Search for topics"
|
||||
bind:value={search} />
|
||||
{#if Object.keys(topicResultsById).length > 0}
|
||||
<FormList>
|
||||
<Table noMargin noStyles>
|
||||
<TableBody>
|
||||
{#each Object.entries(topicResultsById) as [topicId, topic]}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<InputCheckbox
|
||||
id={topicId}
|
||||
disabled={!!topicsById[topicId]}
|
||||
checked={!!selected[topicId]}
|
||||
on:change={(event) => onTopicSelection(event, topic)}>
|
||||
<svelte:fragment slot="description">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-1 u-bold" data-private>
|
||||
{topic.name}
|
||||
</span>
|
||||
<span
|
||||
class="collapsible-button-optional"
|
||||
style="--p-toggle-optional-color: var(--color-neutral-50);">
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span></span>
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</FormList>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total results: {totalResults}</p>
|
||||
<PaginationInline limit={5} bind:offset sum={totalResults} hidePages />
|
||||
<div class="u-flex u-flex-vertical u-gap-32">
|
||||
<p class="text">
|
||||
Select existing topics you want to send this message to its targets. The message will be
|
||||
sent only to <ProviderType type={providerType} noIcon /> targets.
|
||||
</p>
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<InputSearch
|
||||
autofocus
|
||||
disabled={totalResults === 0 && !search}
|
||||
placeholder="Search for topics"
|
||||
bind:value={search} />
|
||||
<div>
|
||||
{#if Object.keys(topicResultsById).length > 0}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<FormList>
|
||||
<Table noMargin noStyles>
|
||||
<TableBody>
|
||||
{#each Object.entries(topicResultsById) as [topicId, topic]}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<InputCheckbox
|
||||
id={topicId}
|
||||
disabled={!!topicsById[topicId]}
|
||||
checked={!!selected[topicId]}
|
||||
on:change={(event) =>
|
||||
onTopicSelection(event, topic)}>
|
||||
<svelte:fragment slot="description">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span
|
||||
class="body-text-1 u-bold"
|
||||
data-private>
|
||||
{topic.name}
|
||||
</span>
|
||||
<span
|
||||
class="collapsible-button-optional"
|
||||
style="--p-toggle-optional-color: var(--color-neutral-50);">
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span></span>
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</FormList>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p class="text">Total results: {totalResults}</p>
|
||||
<PaginationInline limit={5} bind:offset sum={totalResults} hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else if search}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
|
||||
<p>There are no topics that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
text>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}
|
||||
>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no topics. Create a topic to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
>documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if search}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
|
||||
<p>There are no topics that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button external href="https://appwrite.io/docs/products/messaging/topics" text
|
||||
>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no topics. Create a topic to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <a
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
<div class="u-flex u-gap-8">
|
||||
<span class="inline-tag"><span class="text">{selectedSize}</span></span>
|
||||
<span class="body-text-2">Topics selected</span>
|
||||
</div>
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
let totalResults = 0;
|
||||
let userResultsById: Record<string, Models.User<Record<string, unknown>>> = {}; // use a hash map so we can quickly look up a user by id
|
||||
let selected: Record<string, Models.Target> = {};
|
||||
let selectedSize = 0;
|
||||
let selectedUsers = 0;
|
||||
let hasSelection = false;
|
||||
|
||||
function reset() {
|
||||
@@ -108,7 +110,16 @@
|
||||
request();
|
||||
}
|
||||
|
||||
$: selectedSize = Object.keys(selected).length;
|
||||
$: {
|
||||
selectedSize = 0;
|
||||
const users = new Set();
|
||||
for (const s in selected) {
|
||||
const target = selected[s];
|
||||
users.add(target.userId);
|
||||
selectedSize++;
|
||||
}
|
||||
selectedUsers = users.size;
|
||||
}
|
||||
|
||||
$: hasSelection = selectedSize > 0;
|
||||
|
||||
@@ -118,120 +129,160 @@
|
||||
</script>
|
||||
|
||||
<Modal {title} bind:show onSubmit={submit} on:close={reset} size="big" headerDivider={false}>
|
||||
<slot name="description" />
|
||||
<div class="u-flex u-flex-vertical u-gap-32">
|
||||
<slot name="description" />
|
||||
|
||||
<InputSearch
|
||||
autofocus
|
||||
disabled={totalResults === 0 && !search}
|
||||
placeholder="Search by name, email, phone or ID"
|
||||
bind:value={search} />
|
||||
{#if Object.keys(userResultsById).length > 0}
|
||||
<div class="u-sep-block-end">
|
||||
<Collapsible>
|
||||
{#each Object.entries(userResultsById) as [userId, user] (userId)}
|
||||
{@const selectedCount = user.targets.filter(
|
||||
(target) => selected[target.$id]
|
||||
).length}
|
||||
<CollapsibleItem withIndentation>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<InputCheckbox
|
||||
id={userId}
|
||||
size="small"
|
||||
disabled={user.targets.length > 0 &&
|
||||
user.targets.every((target) => targetsById[target.$id])}
|
||||
checked={selectedCount > 0 && selectedCount === user.targets.length}
|
||||
on:change={(event) => onUserSelection(event, userId)} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span
|
||||
class="body-text-1 u-bold"
|
||||
style="color:hsl(var(--color-neutral-80))"
|
||||
data-private>
|
||||
{#if user.name}
|
||||
{user.name}
|
||||
{:else if user.email}
|
||||
{user.email}
|
||||
{:else if user.phone}
|
||||
{user.phone}
|
||||
{:else}
|
||||
{userId}
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
({selectedCount}/{user.targets.length} targets)
|
||||
</svelte:fragment>
|
||||
<FormList>
|
||||
{#each user.targets as target}
|
||||
<div class="u-flex">
|
||||
<InputCheckbox
|
||||
id={target.$id}
|
||||
size="small"
|
||||
disabled={!!targetsById[target.$id]}
|
||||
checked={!!selected[target.$id]}
|
||||
on:change={(event) => onTargetSelection(event, target)}>
|
||||
<svelte:fragment slot="description">
|
||||
<div class="u-inline-flex u-gap-8">
|
||||
<span class="inline-tag u-normal u-x-small"
|
||||
><ProviderType
|
||||
type={target.providerType}
|
||||
noIcon /></span>
|
||||
{#if target.providerType !== MessagingProviderType.Push}
|
||||
{target.identifier}
|
||||
{:else}
|
||||
{target.name}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<InputSearch
|
||||
autofocus
|
||||
disabled={totalResults === 0 && !search}
|
||||
placeholder="Search by name, email, phone or ID"
|
||||
bind:value={search} />
|
||||
<div>
|
||||
{#if Object.keys(userResultsById).length > 0}
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<div class="u-sep-block-end">
|
||||
<Collapsible>
|
||||
{#each Object.entries(userResultsById) as [userId, user] (userId)}
|
||||
{@const selectedCount = user.targets.filter(
|
||||
(target) => selected[target.$id]
|
||||
).length}
|
||||
<CollapsibleItem
|
||||
withIndentation
|
||||
disabled={!user.targets.length}>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<InputCheckbox
|
||||
id={userId}
|
||||
size="small"
|
||||
disabled={!user.targets.length ||
|
||||
(user.targets.length > 0 &&
|
||||
user.targets.every(
|
||||
(target) => targetsById[target.$id]
|
||||
))}
|
||||
checked={selectedCount > 0 &&
|
||||
selectedCount === user.targets.length}
|
||||
on:change={(event) =>
|
||||
onUserSelection(event, userId)} />
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
</div>
|
||||
{/each}
|
||||
</FormList>
|
||||
</CollapsibleItem>
|
||||
{/each}
|
||||
</Collapsible>
|
||||
</div>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total results: {totalResults}</p>
|
||||
<PaginationInline limit={5} bind:offset sum={totalResults} hidePages />
|
||||
</div>
|
||||
{:else if search}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
|
||||
<p>There are no Users that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button external href="https://appwrite.io/docs/products/auth/accounts" text
|
||||
>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}>Clear search</Button>
|
||||
</div>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span
|
||||
class="user-name body-text-1 u-bold"
|
||||
data-private>
|
||||
{#if user.name}
|
||||
{user.name}
|
||||
{:else if user.email}
|
||||
{user.email}
|
||||
{:else if user.phone}
|
||||
{user.phone}
|
||||
{:else}
|
||||
{userId}
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
{#if user.targets.length === 0}
|
||||
(0 targets)
|
||||
{:else}
|
||||
({selectedCount}/{user.targets.length} targets)
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<FormList>
|
||||
{#each user.targets as target}
|
||||
<div class="u-flex">
|
||||
<InputCheckbox
|
||||
id={target.$id}
|
||||
size="small"
|
||||
disabled={!!targetsById[target.$id]}
|
||||
checked={!!selected[target.$id]}
|
||||
on:change={(event) =>
|
||||
onTargetSelection(event, target)}>
|
||||
<svelte:fragment slot="description">
|
||||
<div class="u-inline-flex u-gap-8">
|
||||
<span
|
||||
class="inline-tag u-normal u-x-small"
|
||||
><ProviderType
|
||||
type={target.providerType}
|
||||
noIcon /></span>
|
||||
{#if target.providerType !== MessagingProviderType.Push}
|
||||
{target.identifier}
|
||||
{:else}
|
||||
{target.name}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</InputCheckbox>
|
||||
</div>
|
||||
{/each}
|
||||
</FormList>
|
||||
</CollapsibleItem>
|
||||
{/each}
|
||||
</Collapsible>
|
||||
</div>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p class="text">Total results: {totalResults}</p>
|
||||
<PaginationInline limit={5} bind:offset sum={totalResults} hidePages />
|
||||
</div>
|
||||
</div>
|
||||
{:else if search}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry we couldn't find "{search}"</b>
|
||||
<p>There are no Users that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/auth/accounts"
|
||||
text>Documentation</Button>
|
||||
<Button secondary on:click={() => (search = '')}
|
||||
>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no users. Create a user to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/auth/quick-start"
|
||||
>documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<p class="text u-line-height-1-5">
|
||||
You have no users. Create a user to see them here.
|
||||
</p>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <a
|
||||
href="https://appwrite.io/docs/products/auth/quick-start"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
<div class="u-flex u-gap-16 u-cross-center">
|
||||
<div class="u-flex u-gap-8">
|
||||
<span class="inline-tag"><span class="text">{selectedUsers}</span></span>
|
||||
<span class="body-text-2">Users selected</span>
|
||||
</div>
|
||||
<Button submit disabled={!hasSelection}>Add</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.collapsible-wrapper:not(.is-disabled)) {
|
||||
.user-name {
|
||||
color: hsl(var(--color-neutral-80));
|
||||
|
||||
:global(.theme-dark) & {
|
||||
color: hsl(var(--color-neutral-20));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
|
||||
import Step1 from './wizard/step1.svelte';
|
||||
import Step2 from './wizard/step2.svelte';
|
||||
import Step3 from './wizard/step3.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { project } from '../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providerType, messageParams, operation } from './wizard/store';
|
||||
import { ID, MessageStatus, MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let response: Models.Message;
|
||||
const messageId = $messageParams[$providerType].messageId || ID.unique();
|
||||
|
||||
switch ($providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
response = await sdk.forProject.messaging.createEmail(
|
||||
messageId,
|
||||
$messageParams[$providerType].subject,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].html,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
response = await sdk.forProject.messaging.createSms(
|
||||
messageId,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
{
|
||||
const customData: Record<string, string> = {};
|
||||
const { data } = $messageParams[MessagingProviderType.Push];
|
||||
if (data && data.length > 0) {
|
||||
data.forEach((item) => {
|
||||
if (item[0] === '') return;
|
||||
customData[item[0]] = item[1];
|
||||
});
|
||||
}
|
||||
|
||||
response = await sdk.forProject.messaging.createPush(
|
||||
messageId,
|
||||
$messageParams[$providerType].title,
|
||||
$messageParams[$providerType].body,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
customData,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
wizard.hide();
|
||||
let message = '';
|
||||
switch (response.status) {
|
||||
case MessageStatus.Draft:
|
||||
message = 'The message has been saved as draft.';
|
||||
break;
|
||||
case MessageStatus.Processing:
|
||||
message = 'The message is queued for processing.';
|
||||
break;
|
||||
case 'scheduled':
|
||||
// TODO: fix message status
|
||||
message = 'The message has been scheduled.';
|
||||
break;
|
||||
}
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageCreate, {
|
||||
providerType: $providerType,
|
||||
status: response.status
|
||||
});
|
||||
await goto(`${base}/console/project-${$project.$id}/messaging/message-${response.$id}`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
let response: Models.Message;
|
||||
|
||||
const messageId = $messageParams[$providerType].messageId;
|
||||
|
||||
const params = $messageParams[$providerType];
|
||||
|
||||
console.log(params);
|
||||
|
||||
switch ($providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
response = await sdk.forProject.messaging.updateEmail(
|
||||
messageId,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
$messageParams[$providerType].subject,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].html,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
response = await sdk.forProject.messaging.updateSms(
|
||||
messageId,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
$messageParams[$providerType].content,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
{
|
||||
const customData: Record<string, string> = {};
|
||||
const { data } = $messageParams[MessagingProviderType.Push];
|
||||
if (data && data.length > 0) {
|
||||
data.forEach((item) => {
|
||||
if (item[0] === '') return;
|
||||
customData[item[0]] = item[1];
|
||||
});
|
||||
}
|
||||
|
||||
response = await sdk.forProject.messaging.updatePush(
|
||||
messageId,
|
||||
$messageParams[$providerType].topics,
|
||||
$messageParams[$providerType].users,
|
||||
$messageParams[$providerType].targets,
|
||||
$messageParams[$providerType].title,
|
||||
$messageParams[$providerType].body,
|
||||
customData,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
$messageParams[$providerType].status,
|
||||
$messageParams[$providerType].scheduledAt
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
wizard.hide();
|
||||
let message = '';
|
||||
switch (response.status) {
|
||||
case MessageStatus.Draft:
|
||||
message = 'The message has been saved as draft.';
|
||||
break;
|
||||
case MessageStatus.Processing:
|
||||
message = 'The message is queued for processing.';
|
||||
break;
|
||||
case 'scheduled':
|
||||
// TODO: fix message status
|
||||
message = 'The message has been scheduled.';
|
||||
break;
|
||||
}
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message
|
||||
});
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: $providerType,
|
||||
status: params.status
|
||||
});
|
||||
await goto(`${base}/console/project-${$project.$id}/messaging/message-${response.$id}`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
$messageParams[$providerType].status = MessageStatus.Draft;
|
||||
if ($operation === 'create') {
|
||||
create();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Message',
|
||||
component: Step1,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Targets',
|
||||
component: Step2,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
stepsComponents.set(3, {
|
||||
label: 'Schedule',
|
||||
component: Step3,
|
||||
actions: [
|
||||
{
|
||||
label: 'Save as draft',
|
||||
onClick: saveDraft
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard
|
||||
title={$operation === 'create' ? 'Create message' : 'Update message'}
|
||||
steps={stepsComponents}
|
||||
on:finish={$operation === 'create' ? create : update}
|
||||
finalAction="Send" />
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { messageParams, providerType, operation } from './store';
|
||||
import { messageParams, providerType } from './store';
|
||||
import {
|
||||
Button,
|
||||
FormList,
|
||||
@@ -10,10 +10,10 @@
|
||||
InputTextarea
|
||||
} from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { CustomId, Modal } from '$lib/components';
|
||||
import { Modal } from '$lib/components';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { ID, MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { ID, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
let showCustomId = false;
|
||||
@@ -36,7 +36,7 @@
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
MessageStatus.Processing,
|
||||
undefined,
|
||||
$messageParams[MessagingProviderType.Email]?.html || false,
|
||||
undefined
|
||||
);
|
||||
@@ -49,6 +49,7 @@
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
required
|
||||
placeholder="Enter subject"
|
||||
bind:value={$messageParams[$providerType]['subject']}>
|
||||
</InputText>
|
||||
@@ -56,10 +57,10 @@
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
required
|
||||
placeholder="Type here..."
|
||||
bind:value={$messageParams[$providerType]['content']}>
|
||||
</InputTextarea>
|
||||
<!-- TODO: Add support for draft messages -->
|
||||
<!-- <div class="u-flex u-main-end">
|
||||
<Button text on:click={() => (showTest = true)}>Send test message</Button>
|
||||
</div> -->
|
||||
@@ -105,20 +106,12 @@
|
||||
Enable the HTML mode if your message contains HTML tags.
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
{#if $operation === 'create'}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Message"
|
||||
bind:id={$messageParams[$providerType].messageId}
|
||||
autofocus={false} />
|
||||
{/if}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { messageParams, providerType, operation } from './store';
|
||||
import { messageParams, providerType } from './store';
|
||||
import {
|
||||
Button,
|
||||
FormItem,
|
||||
@@ -38,10 +38,10 @@
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { CustomId, Modal } from '$lib/components';
|
||||
import { Modal } from '$lib/components';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { ID, MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { ID, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -76,7 +76,7 @@
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
MessageStatus.Processing,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
@@ -91,6 +91,7 @@
|
||||
<InputText
|
||||
id="title"
|
||||
label="Title"
|
||||
required
|
||||
placeholder="Enter title"
|
||||
bind:value={$messageParams[MessagingProviderType.Push]['title']}>
|
||||
</InputText>
|
||||
@@ -99,6 +100,7 @@
|
||||
id="message"
|
||||
label="Message"
|
||||
placeholder="Type here..."
|
||||
required
|
||||
maxlength={1000}
|
||||
bind:value={$messageParams[MessagingProviderType.Push]['body']}>
|
||||
</InputTextarea>
|
||||
@@ -214,21 +216,13 @@
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{#if $operation === 'create'}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Message"
|
||||
bind:id={$messageParams[$providerType].messageId}
|
||||
autofocus={false} />
|
||||
{/if}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<PushPhone
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { messageParams, providerType, operation } from './store';
|
||||
import { messageParams, providerType } from './store';
|
||||
import { Button, FormList, InputEmail, InputRadio, InputTextarea } from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { CustomId, Modal } from '$lib/components';
|
||||
import { Modal } from '$lib/components';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { ID, MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { ID, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
$messageParams[MessagingProviderType.Sms]?.topics || [],
|
||||
$messageParams[MessagingProviderType.Sms]?.users || [],
|
||||
$messageParams[MessagingProviderType.Sms]?.targets || [],
|
||||
MessageStatus.Processing,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
@@ -38,8 +38,9 @@
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
placeholder="Type here..."
|
||||
required
|
||||
maxlength={900}
|
||||
placeholder="Type here..."
|
||||
bind:value={$messageParams[$providerType]['content']}>
|
||||
</InputTextarea>
|
||||
<!-- TODO: Add support for draft messages -->
|
||||
@@ -83,21 +84,13 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
{#if $operation === 'create'}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Message"
|
||||
bind:id={$messageParams[$providerType].messageId}
|
||||
autofocus={false} />
|
||||
{/if}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<SMSPhone content={$messageParams[$providerType]['content']} classes="is-only-desktop" />
|
||||
|
||||
@@ -41,13 +41,19 @@
|
||||
$targetsById = rest;
|
||||
}
|
||||
|
||||
async function beforeSubmit() {
|
||||
if (topicIdsLength === 0 && targetIdsLength === 0) {
|
||||
throw new Error('At least one topic or target is required.');
|
||||
}
|
||||
}
|
||||
|
||||
$: topicIdsLength = Object.keys($topicsById).length;
|
||||
$: targetIdsLength = Object.keys($targetsById).length;
|
||||
$: $messageParams[$providerType].targets = Object.keys($targetsById);
|
||||
$: $messageParams[$providerType].topics = Object.keys($topicsById);
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Targets</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Select targets to whom this message should be directed.
|
||||
@@ -89,7 +95,7 @@
|
||||
{topic.name}
|
||||
</span>
|
||||
<span class="collapsible-button-optional">
|
||||
({getTotal(topic)} subscribers)
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span></span>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import { Button, FormList, InputDate, InputSelect, InputTime } from '$lib/elements/forms';
|
||||
import Helper from '$lib/elements/forms/helper.svelte';
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import { messageParams, providerType } from './store';
|
||||
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
|
||||
|
||||
let when: 'now' | 'later' = 'now';
|
||||
let now = new Date();
|
||||
let timeZoneOffset: number;
|
||||
let minDate: string;
|
||||
let date: string;
|
||||
let time: string;
|
||||
@@ -40,30 +40,23 @@
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
async function beforeSubmit() {
|
||||
$messageParams[$providerType].status = MessageStatus.Processing;
|
||||
if (when === 'later') {
|
||||
$messageParams[$providerType].status = 'scheduled' as MessageStatus;
|
||||
$messageParams[$providerType].scheduledAt = dateTime.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
$: if (when === 'now') {
|
||||
date = time = '';
|
||||
}
|
||||
$: if (when === 'later') {
|
||||
now = new Date();
|
||||
}
|
||||
$: timeZoneOffset = now ? now.getTimezoneOffset() * 60 * 1000 : 0;
|
||||
$: minDate = new Date(now.getTime() - timeZoneOffset).toISOString().split('T')[0];
|
||||
$: minTime =
|
||||
date === minDate
|
||||
? new Date(now.getTime() - timeZoneOffset).toISOString().split('T')[1].substring(0, 5)
|
||||
: '00:00';
|
||||
$: minDate = toLocaleDateISO(now.getTime());
|
||||
$: minTime = isSameDay(new Date(date), new Date(minDate))
|
||||
? toLocaleTimeISO(now.getTime())
|
||||
: '00:00';
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
$: if (!isNaN(dateTime.getTime())) {
|
||||
$messageParams[$providerType].scheduledAt = dateTime.toISOString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Schedule</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle"
|
||||
>Schedule the time you want to deliver this message. Learn more in our <Button
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MessageStatus, MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export type MessageParams = {
|
||||
@@ -6,7 +6,7 @@ export type MessageParams = {
|
||||
topics: string[];
|
||||
users: string[];
|
||||
targets: string[];
|
||||
status: MessageStatus;
|
||||
draft: boolean;
|
||||
scheduledAt?: string;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ export type PushMessageParams = MessageParams & {
|
||||
badge?: string;
|
||||
};
|
||||
|
||||
export const operation = writable<'create' | 'update'>('create');
|
||||
export const providerType = writable<MessagingProviderType>(null);
|
||||
export const targetsById = writable<Record<string, Models.Target>>({});
|
||||
export const messageParams = writable<{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Create an API key</svelte:fragment>
|
||||
<svelte:fragment slot="title">API key</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">Let's create an API key.</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Add Scopes</svelte:fragment>
|
||||
<svelte:fragment slot="title">Scopes</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Choose which permission scopes to grant your application. It is best practice to allow only
|
||||
the permissions you need to meet your project goals.
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Register your Android app</svelte:fragment>
|
||||
<svelte:fragment slot="title">Android registration</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Get the SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Install</svelte:fragment>
|
||||
<p>First, add this to your root level <code class="inline-code">build.gradle</code> file.</p>
|
||||
<Code label="Groovy" labelIcon="android" language="kotlin" code={example1} withCopy />
|
||||
<p class="common-section">And add this to your project's build.gradle file.</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ val client = Client(context)
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Initialize SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Initialize</svelte:fragment>
|
||||
|
||||
<h2 class="heading-level-7">Initialize your SDK</h2>
|
||||
<p>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Register your bundle ID</svelte:fragment>
|
||||
<svelte:fragment slot="title">Apple registration</svelte:fragment>
|
||||
|
||||
<FormList isCommonSection>
|
||||
<FormItem>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Get the SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Install</svelte:fragment>
|
||||
<div class="u-flex u-gap-16 u-margin-block-start-8">
|
||||
<Pill button on:click={() => (method = Method.Xcode)} selected={method === Method.Xcode}>
|
||||
Xcode
|
||||
|
||||
@@ -17,7 +17,7 @@ let client = Client()
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Initialize SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Initialize</svelte:fragment>
|
||||
|
||||
<h2 class="heading-level-7">Initialize your SDK</h2>
|
||||
<p>
|
||||
|
||||
@@ -149,16 +149,16 @@
|
||||
|
||||
$: registee = {
|
||||
[PlatformType.Flutterandroid]: 'package name',
|
||||
[PlatformType.Flutterios]: 'bundle ID',
|
||||
[PlatformType.Flutterlinux]: 'package name',
|
||||
[PlatformType.Fluttermacos]: 'bundle ID',
|
||||
[PlatformType.Flutterwindows]: 'package name',
|
||||
[PlatformType.Flutterweb]: 'hostname'
|
||||
[PlatformType.Flutterios]: 'Bundle ID',
|
||||
[PlatformType.Flutterlinux]: 'Package name',
|
||||
[PlatformType.Fluttermacos]: 'Bundle ID',
|
||||
[PlatformType.Flutterwindows]: 'Package name',
|
||||
[PlatformType.Flutterweb]: 'Hostname'
|
||||
}[platform];
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Register your {registee}</svelte:fragment>
|
||||
<svelte:fragment slot="title">{registee} registration</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
<div class="u-flex u-gap-16 u-margin-block-start-8 u-flex-wrap">
|
||||
<Pill
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Get the SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Install</svelte:fragment>
|
||||
|
||||
<p>
|
||||
Add Appwrite SDK to your package's <code class="inline-code">pubspec.yaml</code> file. You can
|
||||
|
||||
@@ -18,7 +18,7 @@ client
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Initialize SDK</svelte:fragment>
|
||||
<svelte:fragment slot="title">Initialize</svelte:fragment>
|
||||
|
||||
<h2 class="heading-level-7">Initialize your SDK</h2>
|
||||
<p data-private>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">You're ready to go 🎉</svelte:fragment>
|
||||
<svelte:fragment slot="title">All set</svelte:fragment>
|
||||
<p>
|
||||
{#if isFirstProject}
|
||||
Congratulations on adding the first platform to your project! Start exploring by
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Register your hostname</svelte:fragment>
|
||||
<svelte:fragment slot="title">Hostname registration</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user