Merge branch '1.5.x' of https://github.com/appwrite/console into feat-file-picker

This commit is contained in:
Torsten Dittmann
2024-02-29 13:01:15 +01:00
91 changed files with 1963 additions and 1027 deletions
+4 -4
View File
@@ -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.5.0",
"@appwrite.io/pink-icons": "0.5.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"
+4 -4
View File
@@ -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.5.0",
"@appwrite.io/pink-icons": "0.5.0",
"@popperjs/core": "^2.11.8",
+1 -1
View File
@@ -301,7 +301,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'
}
+31 -4
View File
@@ -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>
+2 -2
View File
@@ -274,7 +274,7 @@
<div class="u-flex-vertical u-gap-8">
<div
role="button"
style:background-position="center"
style:background-size="cover"
style:background-image={`url(${getPreview(
currentBucket.$id,
file.$id,
@@ -287,7 +287,7 @@
style:display="flex"
style:align-items="flex-end"
style:flex-direction="row-reverse"
style:border-shadow="none"
style:box-shadow="none"
class="card u-height-100-percent u-gap-16"
style="--card-padding:0.5rem; --card-border-radius:var(--border-radius-large);">
<input
+18 -3
View File
@@ -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>
+32
View File
@@ -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() &&
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Client, Models, Query } from '@appwrite.io/console';
import type { Client, Models } from '@appwrite.io/console';
import type { Organization } from '../stores/organization';
import type { PaymentMethod } from '@stripe/stripe-js';
import type { Tier } from '$lib/stores/billing';
@@ -849,7 +849,7 @@ export class Billing {
);
}
async listAddresses(queries: Query[] = []): Promise<AddressesList> {
async listAddresses(queries: string[] = []): Promise<AddressesList> {
const path = `/account/billing-addresses`;
const params = {
queries
@@ -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
+2 -1
View File
@@ -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
+6 -2
View File
@@ -39,14 +39,18 @@
</script>
<Modal
title="Delete authentication provider"
title="Delete authentication method"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p>Are you sure you want to delete this authentication method from your account?</p>
<p class="u-bold">
Deleting the authentication method will disable multi-factor authentication for your
account. To re-enable it, you'll need to add a new authentication method.
</p>
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
+36 -5
View File
@@ -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>
@@ -78,7 +78,7 @@
},
...permanentTabSettings
]
: permanentTabs;
: [...permanentTabs, ...permanentTabSettings];
</script>
{#if $organization?.$id}
@@ -99,4 +99,6 @@
{/if}
</Container>
<Delete bind:showDelete invoices={data.invoices} />
{#if isCloud && data.invoices}
<Delete bind:showDelete invoices={data.invoices} />
{/if}
@@ -2,12 +2,14 @@ import type { PageLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { isCloud } from '$lib/system';
export const load: PageLoad = async ({ depends, parent }) => {
const { organization } = await parent();
depends(Dependencies.ORGANIZATION);
return {
projects: await sdk.forConsole.projects.list([Query.equal('teamId', organization.$id)]),
invoices: await sdk.forConsole.billing.listInvoices(organization.$id)
invoices: isCloud ? await sdk.forConsole.billing.listInvoices(organization.$id) : undefined
};
};
@@ -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"
@@ -4,8 +4,6 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType } from '@appwrite.io/console';
@@ -13,12 +11,11 @@
export let showDelete = false;
let code: string;
let error: string;
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;
@@ -34,23 +31,23 @@
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
<Modal
title="Delete authentication provider"
title="Delete authentication method"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
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>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
@@ -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
@@ -28,7 +28,7 @@
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
import { MessagingProviderType } from '@appwrite.io/console';
import type { PageData } from './$types';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
@@ -141,7 +141,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>
@@ -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>
@@ -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, {
@@ -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;
@@ -91,8 +91,8 @@
providerId,
undefined,
undefined,
formValues['username'],
formValues['password'],
formValues['customerId'],
formValues['apiKey'],
formValues['from']
);
break;
@@ -120,7 +120,7 @@
{:else if input.type === 'switch'}
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
<svelte:fragment slot="description">
{input.description}
<p>{@html input.description}</p>
</svelte:fragment>
</InputSwitch>
{:else if input.type === 'checkbox'}
@@ -167,7 +167,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>.'
}
]
}
@@ -469,16 +470,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 +585,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;
@@ -97,7 +97,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 +135,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 +150,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);
@@ -36,7 +36,7 @@
</div>
<div class="project-name u-text-center u-x-small u-width-full-line">{$project.name}</div>
<div class="chat u-text-center u-width-full-line">
<div class="time u-text-center u-x-small u-width-full-line u-line-height-0-7">
<div class="time u-text-center u-width-full-line u-line-height-0-7">
Today {getTime()}
</div>
<div class="chat-bubble-container">
@@ -51,7 +51,7 @@
.phone {
position: relative;
width: 320px;
height: 539px;
height: 410px;
background-image: url('./sms-preview-light.svg');
background-repeat: no-repeat;
@@ -3,10 +3,12 @@
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>
@@ -146,7 +146,7 @@
<Empty
single
on:click={() => (showAdd = true)}
href="https://appwrite.io/docs/products/messaging/topics/#subscribe-targets-to-topics"
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="subscriber" />
{/if}
</Container>
@@ -159,10 +159,11 @@
<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"
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about subscribers.</a>
class="link">Learn more about subscribers</a
>.
</p>
</svelte:fragment>
</UserTargetsModal>
@@ -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>
@@ -91,86 +91,112 @@
</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}
<div class="u-flex-vertical u-gap-8">
<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>
<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}
<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,167 @@
</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>
<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}
<div class="u-flex-vertical u-gap-8">
<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>
{/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>
<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>
<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}
<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
);
@@ -105,20 +105,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, type Models } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import PushPhone from '../pushPhone.svelte';
import { onMount } from 'svelte';
@@ -52,6 +52,7 @@
let selected = 'self';
let otherEmail = '';
let dataError = '';
let file: Models.File = null;
let customData: [string, string][] = [];
onMount(() => {
@@ -77,7 +78,7 @@
undefined,
undefined,
undefined,
MessageStatus.Processing,
undefined,
undefined
);
}
@@ -151,7 +152,9 @@
<form class="form">
<FormList>
<FormItem>
<InputFilePicker label="Media" />
<InputFilePicker
bind:value={$messageParams[MessagingProviderType.Push].file}
label="Media" />
</FormItem>
<FormItem>
<Label
@@ -221,21 +224,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
);
}
@@ -83,25 +83,17 @@
</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" />
</div>
<div class="u-flex u-main-center u-margin-block-start-24">
<SMSPhone content={$messageParams[$providerType]['content']} classes="is-not-desktop" />
<div class="u-flex u-main-center u-margin-block-start-24 is-not-desktop">
<SMSPhone content={$messageParams[$providerType]['content']} />
</div>
@@ -89,7 +89,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;
};
@@ -23,6 +23,7 @@ export type SMSMessageParams = MessageParams & {
export type PushMessageParams = MessageParams & {
title: string;
body: string;
file: Models.File;
data: [string, string][];
action?: string;
icon?: string;
@@ -32,7 +33,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"
@@ -23,7 +23,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Get the SDK</svelte:fragment>
<svelte:fragment slot="title">Install</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose your preferred method of installation
<div class="u-flex u-gap-16 u-margin-block-start-8">
@@ -19,7 +19,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Select source</svelte:fragment>
<svelte:fragment slot="title">Source</svelte:fragment>
<svelte:fragment slot="subtitle">
Select a source platform with the project you want to migrate. <a
class="link"
@@ -7,7 +7,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Select source</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
@@ -6,7 +6,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Add your webhook</svelte:fragment>
<svelte:fragment slot="title">Configuration</svelte:fragment>
<svelte:fragment slot="subtitle">
Use webhooks to build integrations that subscribe to specific events on Appwrite. Appwrite
will send an HTTP POST payload to the webhook's configured URL when one of the specified
@@ -18,7 +18,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Create file</svelte:fragment>
<svelte:fragment slot="title">File</svelte:fragment>
<svelte:fragment slot="subtitle">Upload a file to add it to your bucket.</svelte:fragment>
{#if isCloud}
@@ -7,7 +7,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Update Permissions</svelte:fragment>
<svelte:fragment slot="title">Permissions</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose who can access your buckets and files. For more information, check out the
<a
@@ -51,7 +51,7 @@
</script>
<WizardStep beforeSubmit={handleBefore}>
<svelte:fragment slot="title">Confirm your details</svelte:fragment>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
</svelte:fragment>
@@ -36,7 +36,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Invite organization members</svelte:fragment>
<svelte:fragment slot="title">Invites</svelte:fragment>
<svelte:fragment slot="subtitle">
Invite team members to collaborate with you in the Appwrite console. Members will have
access to all services and payment data within your organization.
@@ -103,7 +103,7 @@
</script>
<WizardStep beforeSubmit={handleBefore}>
<svelte:fragment slot="title">Choose a plan</svelte:fragment>
<svelte:fragment slot="title">Plan selection</svelte:fragment>
<p class="body-text-1 u-bold common-section">Plan</p>
<p class="text u-margin-block-start-4">
@@ -49,7 +49,7 @@
{#if downgradeToStarter}
<WizardStep>
<svelte:fragment slot="title">Confirm plan change</svelte:fragment>
<svelte:fragment slot="title">Change confirmation</svelte:fragment>
<svelte:fragment slot="subtitle">
Your feedback is important to us and helps us improve the services Appwrite offer. If
there is a specific reason you chose to change your plan at this time, please let us
@@ -65,7 +65,7 @@
</WizardStep>
{:else}
<WizardStep>
<svelte:fragment slot="title">Confirm your details</svelte:fragment>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
</svelte:fragment>
@@ -52,7 +52,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Invite organization members</svelte:fragment>
<svelte:fragment slot="title">Invites</svelte:fragment>
<svelte:fragment slot="subtitle">
Invite team members to collaborate with you in the Appwrite console. Members will have
access to all services and payment data within your organization.
@@ -7,8 +7,8 @@
</script>
<WizardStep>
<svelte:fragment slot="title"
>Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan</svelte:fragment>
<svelte:fragment slot="title">
Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan</svelte:fragment>
<svelte:fragment slot="subtitle">
View the current usage for <b>{$organization.name}</b> and where you will exceed the limits of
your new plan.
@@ -6,7 +6,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Thanks for the feedback!</svelte:fragment>
<svelte:fragment slot="title">Thank you</svelte:fragment>
<div class="u-flex u-main-center">
{#if $app.themeInUse === 'dark'}
+39 -3
View File
@@ -4,13 +4,16 @@ import {
toLocaleDateTime,
isSameDay,
isValidDate,
diffDays
diffDays,
toLocaleDateISO,
toLocaleTimeISO
} from '$lib/helpers/date';
describe('local date', () => {
[
['2022-11-15 08:26:28', 'Nov 15, 2022'],
['2022-11-15 00:26:28', 'Nov 15, 2022']
['2022-11-15 00:26:28', 'Nov 15, 2022'],
['2022-11-15 00:26:28Z', 'Nov 14, 2022']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDate(value)).toBe(expected);
@@ -25,7 +28,8 @@ describe('local date', () => {
describe('local date time', () => {
[
['2022-11-15 08:26:28', 'Nov 15, 2022, 08:26'],
['2022-11-15 00:26:28', 'Nov 15, 2022, 00:26']
['2022-11-15 00:26:28', 'Nov 15, 2022, 00:26'],
['2022-11-15 00:26:28Z', 'Nov 14, 2022, 19:26']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDateTime(value)).toBe(expected);
@@ -37,6 +41,38 @@ describe('local date time', () => {
});
});
describe('local date ISO', () => {
[
['2022-11-15 20:26:28Z', '2022-11-15'],
['2022-11-15 08:26:28Z', '2022-11-15'],
['2022-11-16 00:26:28Z', '2022-11-15']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleDateISO(value)).toBe(expected);
});
});
it('invalid date', () => {
expect(toLocaleDateISO('')).toBe('n/a');
});
});
describe('local time ISO', () => {
[
['2022-11-15 20:26:28Z', '15:26:28'],
['2022-11-15 08:26:28Z', '03:26:28'],
['2022-11-16 00:26:28Z', '19:26:28']
].forEach(([value, expected]) => {
it(value, () => {
expect(toLocaleTimeISO(value)).toBe(expected);
});
});
it('invalid date', () => {
expect(toLocaleTimeISO('')).toBe('n/a');
});
});
describe('is same day', () => {
const entries: Array<[string, string, boolean]> = [
['2022-11-15 08:26:28', '2022-11-15 08:26:28', true],