mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #882 from appwrite/feat-messaging-edit-message
Update flow for editing a message
This commit is contained in:
+3
-3
@@ -12,9 +12,9 @@
|
||||
"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": {
|
||||
|
||||
@@ -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() &&
|
||||
|
||||
+4
-114
@@ -7,13 +7,12 @@
|
||||
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 { goto } 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 { providerType, messageParams } from './wizard/store';
|
||||
import { ID, MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
@@ -111,114 +110,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
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].draft,
|
||||
$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].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.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].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
|
||||
});
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
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.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
$messageParams[$providerType].draft = true;
|
||||
if ($operation === 'create') {
|
||||
create();
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
create();
|
||||
}
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
@@ -254,8 +148,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard
|
||||
title={$operation === 'create' ? 'Create message' : 'Update message'}
|
||||
steps={stepsComponents}
|
||||
on:finish={$operation === 'create' ? create : update}
|
||||
finalAction="Send" />
|
||||
<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,111 +1,39 @@
|
||||
<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 { MessagingProviderType } from '@appwrite.io/console';
|
||||
import Topics from './topics.svelte';
|
||||
import Targets from './targets.svelte';
|
||||
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,
|
||||
draft: $message.status === '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 === 'draft' ? onEdit : null} />
|
||||
<EmailMessage message={$message} />
|
||||
{:else if $message.providerType === MessagingProviderType.Sms}
|
||||
<SMSPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
|
||||
<SMSMessage message={$message} />
|
||||
{:else if $message.providerType === MessagingProviderType.Push}
|
||||
<PushPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
|
||||
<PushMessage message={$message} />
|
||||
{/if}
|
||||
<Topics topics={Object.values(data.topicsById)} />
|
||||
<Targets targets={Object.values(data.targetsById)} usersById={data.usersById} />
|
||||
<UpdateTopics message={$message} selectedTopicsById={data.topicsById} />
|
||||
<UpdateTargets message={$message} selectedTargetsById={data.targetsById} />
|
||||
{#if $message.status !== 'processing'}
|
||||
<Delete message={$message} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The scheduling has been cancelled.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Cancel scheduling"
|
||||
bind:show
|
||||
onSubmit={update}
|
||||
headerDivider={false}
|
||||
size="small"
|
||||
icon="exclamation"
|
||||
state="warning">
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p data-private>
|
||||
Are you sure you want to cancel the scheduling of <span class="u-bold"
|
||||
>{message.data.title ??
|
||||
message.data.subject ??
|
||||
message.data.content ??
|
||||
'Message'}</span
|
||||
>?
|
||||
</p>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary submit>Cancel scheduling</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormList,
|
||||
InputSwitch,
|
||||
InputText,
|
||||
InputTextarea
|
||||
} from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let subject = '';
|
||||
let content = '';
|
||||
let html = false;
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
subject = message.data.subject;
|
||||
content = message.data.content;
|
||||
html = (message.data['html'] ?? false) as boolean;
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
subject,
|
||||
content,
|
||||
undefined,
|
||||
html
|
||||
);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled =
|
||||
subject === message.data.subject &&
|
||||
content === message.data.content &&
|
||||
html === ((message.data['html'] ?? false) as boolean);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={subject}></InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={content}></InputTextarea>
|
||||
<InputSwitch label="HTML mode" id="html" bind:value={html}>
|
||||
<svelte:fragment slot="description">
|
||||
Enable the HTML mode if your message contains HTML tags.
|
||||
</svelte:fragment>
|
||||
</InputSwitch>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
disabled={true}
|
||||
bind:value={message.data.subject}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,64 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { message } from './store';
|
||||
import ProviderType from '../providerType.svelte';
|
||||
import MessageStatusPill from '../messageStatusPill.svelte';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import FailedModal from '../failedModal.svelte';
|
||||
import SendModal from './sendModal.svelte';
|
||||
import ScheduleModal from './scheduleModal.svelte';
|
||||
import CancelModal from './cancelModal.svelte';
|
||||
|
||||
let scheduledAt: string = '';
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let showSend = false;
|
||||
let showSchedule = false;
|
||||
let showCancel = false;
|
||||
let showFailed = false;
|
||||
let errors: string[] = [];
|
||||
|
||||
if ($message.status === 'sent') {
|
||||
scheduledAt = $message.deliveredAt;
|
||||
} else if ($message.status === 'scheduled') {
|
||||
scheduledAt = $message.scheduledAt;
|
||||
}
|
||||
|
||||
let providerType = 'Invalid provider type';
|
||||
switch ($message.providerType) {
|
||||
case MessagingProviderType.Email:
|
||||
providerType = 'Email';
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
providerType = 'SMS';
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
providerType = 'Push';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid hideFooter={$message.status != 'failed'}>
|
||||
<CardGrid hideFooter={['processing', 'sent'].includes(message.status)}>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
|
||||
<ProviderType type={$message.providerType} size="l">
|
||||
<Heading tag="h6" size="7">{providerType}</Heading>
|
||||
<ProviderType type={message.providerType} size="l">
|
||||
<Heading tag="h6" size="7">
|
||||
<ProviderType type={message.providerType} noIcon />
|
||||
</Heading>
|
||||
</ProviderType>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div data-private>
|
||||
<p class="title">Created: {toLocaleDateTime($message.$createdAt)}</p>
|
||||
<p class="title">Scheduled at: {toLocaleDateTime(scheduledAt)}</p>
|
||||
<p class="title">Created: {toLocaleDateTime(message.$createdAt)}</p>
|
||||
{#if message.scheduledAt}
|
||||
<p class="title">Scheduled at: {toLocaleDateTime(message.scheduledAt)}</p>
|
||||
{/if}
|
||||
{#if message.deliveredAt}
|
||||
<p class="title">Sent at: {toLocaleDateTime(message.deliveredAt)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<MessageStatusPill status={$message.status} />
|
||||
<MessageStatusPill status={message.status} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
secondary
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
errors = $message.deliveryErrors;
|
||||
showFailed = true;
|
||||
}}>View logs</Button>
|
||||
{#if message.status === 'draft'}
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button text on:click={() => (showSchedule = true)}>Schedule</Button>
|
||||
<Button secondary on:click={() => (showSend = true)}>Send message</Button>
|
||||
</div>
|
||||
{:else if message.status === 'scheduled'}
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button text on:click={() => (showCancel = true)}>Cancel scheduling</Button>
|
||||
<Button secondary on:click={() => (showSchedule = true)}>Reschedule</Button>
|
||||
</div>
|
||||
{:else if message.status === 'failed'}
|
||||
<Button
|
||||
secondary
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
errors = message.deliveryErrors;
|
||||
showFailed = true;
|
||||
}}>View logs</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<ScheduleModal bind:show={showSchedule} {message} {topics} />
|
||||
|
||||
<SendModal bind:show={showSend} {message} {topics} />
|
||||
|
||||
<CancelModal bind:show={showCancel} {message} />
|
||||
|
||||
<FailedModal bind:show={showFailed} {errors} />
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
FormItemPart,
|
||||
FormList,
|
||||
Helper,
|
||||
InputText,
|
||||
InputTextarea,
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { validateData } from '../wizard/pushFormList.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let title = '';
|
||||
let body = '';
|
||||
let originalCustomData: [string, string][] = [['', '']];
|
||||
let customData: [string, string][] = [['', '']];
|
||||
let dataError = '';
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
title = message.data.title;
|
||||
body = message.data.body;
|
||||
|
||||
const dataEntries: [string, string][] = [];
|
||||
Object.entries(message.data['data'] ?? {}).forEach(([key, value]) => {
|
||||
dataEntries.push([key, value.toString()]);
|
||||
});
|
||||
customData = dataEntries.length ? dataEntries : [['', '']];
|
||||
originalCustomData = structuredClone(customData);
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
const data = customData.reduce((acc, [key, value]) => {
|
||||
if (key) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
title,
|
||||
body,
|
||||
data
|
||||
);
|
||||
originalCustomData = structuredClone(customData);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: dataError = validateData(customData || []);
|
||||
$: disabled =
|
||||
title === message.data.title &&
|
||||
body === message.data.body &&
|
||||
originalCustomData.length === customData.length &&
|
||||
originalCustomData.every(
|
||||
(ocd, i) =>
|
||||
ocd.length === customData[i].length && ocd.every((v, j) => v === customData[i][j])
|
||||
);
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
<div class="u-flex u-margin-block-start-24 u-width-full-line">
|
||||
<PushPhone {title} {body} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="title"
|
||||
label="Title"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={title}></InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={body}></InputTextarea>
|
||||
<form class="form">
|
||||
<FormItem>
|
||||
<Label
|
||||
tooltip="A key/value payload of additional metadata that's hidden from users. Use this to include information to support logic such as redirection and routing."
|
||||
>Custom data <span class="u-color-text-gray">(Optional)</span></Label>
|
||||
</FormItem>
|
||||
<div class=" u-grid u-gap-8">
|
||||
<ul class="form-list" style="--p-form-list-gap: 1rem">
|
||||
{#each customData || [] as _, rowIndex}
|
||||
<FormItem isMultiple>
|
||||
<InputText
|
||||
id={`${rowIndex}-key`}
|
||||
isMultiple
|
||||
fullWidth
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={customData[rowIndex][0]}
|
||||
placeholder="Enter key"
|
||||
label="Key"
|
||||
showLabel={false} />
|
||||
|
||||
<InputText
|
||||
id={`${rowIndex}-value`}
|
||||
isMultiple
|
||||
fullWidth
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={customData[rowIndex][1]}
|
||||
placeholder="Enter value"
|
||||
label="Value"
|
||||
showLabel={false}
|
||||
required />
|
||||
<FormItemPart alignEnd>
|
||||
<Button
|
||||
text
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => {
|
||||
if (customData.length === 1) {
|
||||
customData = [['', '']];
|
||||
return;
|
||||
}
|
||||
|
||||
customData = customData.filter(
|
||||
(_, i) => i !== rowIndex
|
||||
);
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</Button>
|
||||
</FormItemPart>
|
||||
</FormItem>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if dataError}
|
||||
<Helper type="warning">{dataError}</Helper>
|
||||
{/if}
|
||||
<Button
|
||||
noMargin
|
||||
text
|
||||
disabled={customData && customData[customData.length - 1][0] === ''}
|
||||
on:click={() => {
|
||||
customData = [...customData, ['', '']];
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add data</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<div class="u-flex u-margin-block-start-24 u-width-full-line">
|
||||
<PushPhone title={message.data.title} body={message.data.body} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText id="title" label="Title" disabled={true} bind:value={message.data.title}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.body}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputDate, InputTime, Helper } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let now = new Date();
|
||||
let minDate: string;
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
let date = message.scheduledAt == null ? null : toLocaleDateISO(message.scheduledAt);
|
||||
let time = message.scheduledAt == null ? null : toLocaleTimeISO(message.scheduledAt);
|
||||
let dateTime: Date;
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hourCycle: 'h23',
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been scheduled and will be sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
$: minDate = toLocaleDateISO(now.getTime());
|
||||
$: minTime = isSameDay(new Date(date), new Date(minDate))
|
||||
? toLocaleTimeISO(now.getTime())
|
||||
: '00:00';
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
</script>
|
||||
|
||||
<Modal title="Schedule message" bind:show onSubmit={update} headerDivider={false} size="big">
|
||||
<div>
|
||||
<FormList>
|
||||
<div
|
||||
class="u-grid u-gap-16"
|
||||
style="grid-auto-rows: 1fr; grid-template-columns: 1fr 1fr;">
|
||||
<InputDate id="date" label="Date" required={true} min={minDate} bind:value={date} />
|
||||
<InputTime id="time" label="Time" required={true} min={minTime} bind:value={time} />
|
||||
</div>
|
||||
</FormList>
|
||||
<Helper type="neutral">
|
||||
{#if !dateTime || isNaN(dateTime.getTime())}
|
||||
The message will be sent later
|
||||
{:else}
|
||||
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
|
||||
{/if}
|
||||
</Helper>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Schedule</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title="Send message" bind:show onSubmit={update} headerDivider={false} size="small">
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p data-private>
|
||||
You are about to send a message to an estimated <span class="u-bold"
|
||||
>{totalTargets}</span> targets. Would you like to proceed?
|
||||
</p>
|
||||
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Send</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, Form, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let content = '';
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
content = message.data.content;
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
content
|
||||
);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = content === message.data.content;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
<SMSPhone {content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={content}></InputTextarea>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<SMSPhone content={message.data.content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let targets: Models.Target[];
|
||||
export let usersById: Record<string, Models.User<Models.Preferences>>;
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targets.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Identifier</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={target.$id}>{target.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{usersById[target.userId]?.name || 'Unknown'}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{target.providerType === MessagingProviderType.Push
|
||||
? target.name
|
||||
: target.identifier}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Target</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topics.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Subscribers</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={topic.$id}>{topic.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{topic.name}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{topic.smsTotal + topic.emailTotal + topic.pushTotal}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Topic</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import UserTargetsModal from '../userTargetsModal.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let selectedTargetsById: Record<string, Models.Target>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTargets = false;
|
||||
let targetsById: Record<string, Models.Target>;
|
||||
let targetIds: string[] = [];
|
||||
let targets: Models.Target[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
targetsById = { ...selectedTargetsById };
|
||||
});
|
||||
|
||||
function removeTarget(targetId: string) {
|
||||
const { [targetId]: _, ...rest } = targetsById;
|
||||
targetsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Targets have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
targetIds = [];
|
||||
targets = [];
|
||||
for (const targetId in targetsById) {
|
||||
targetIds.push(targetId);
|
||||
targets.push(targetsById[targetId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(targetIds, Object.keys(selectedTargetsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targetIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Target</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTargets = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow />
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Target">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2" data-private>
|
||||
{#if target.providerType === MessagingProviderType.Push}
|
||||
{target.name}
|
||||
{:else}
|
||||
{target.identifier}
|
||||
{/if}
|
||||
</span>
|
||||
</span></span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove">
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTarget(target.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTargets = true)}>Add a target</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No targets have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/targets"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<UserTargetsModal
|
||||
title="Select targets"
|
||||
{providerType}
|
||||
bind:show={showTargets}
|
||||
{targetsById}
|
||||
on:update={(e) => {
|
||||
showTargets = false;
|
||||
targetsById = e.detail;
|
||||
}}>
|
||||
<svelte:fragment slot="description"
|
||||
>Select existing targets to which you want to send this message.</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { type Models, MessagingProviderType } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import TopicsModal from '../topicsModal.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Form, Button } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { getTotal } from '../wizard/store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message;
|
||||
export let selectedTopicsById: Record<string, Models.Topic>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTopics = false;
|
||||
let topicsById: Record<string, Models.Topic> = {};
|
||||
let topicIds: string[] = [];
|
||||
let topics: Models.Topic[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
topicsById = { ...selectedTopicsById };
|
||||
});
|
||||
|
||||
function removeTopic(topicId: string) {
|
||||
const { [topicId]: _, ...rest } = topicsById;
|
||||
topicsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(message.$id, topicIds);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Topics have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
topicIds = [];
|
||||
topics = [];
|
||||
for (const topicId in topicsById) {
|
||||
topicIds.push(topicId);
|
||||
topics.push(topicsById[topicId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(topicIds, Object.keys(selectedTopicsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topicIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Topic</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTopics = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Topic">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2 u-bold" data-private>
|
||||
{topic.name}
|
||||
</span>
|
||||
<span class="collapsible-button-optional">
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove" width={40}>
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTopic(topic.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No topics have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<TopicsModal
|
||||
{providerType}
|
||||
bind:show={showTopics}
|
||||
{topicsById}
|
||||
on:update={(e) => {
|
||||
showTopics = false;
|
||||
topicsById = e.detail;
|
||||
}} />
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { messageParams, providerType, operation } from './store';
|
||||
import { messageParams, providerType } from './store';
|
||||
import {
|
||||
Button,
|
||||
FormList,
|
||||
@@ -10,7 +10,7 @@
|
||||
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, MessagingProviderType } from '@appwrite.io/console';
|
||||
@@ -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,7 +38,7 @@
|
||||
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, MessagingProviderType } from '@appwrite.io/console';
|
||||
@@ -214,21 +214,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,8 +1,8 @@
|
||||
<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, MessagingProviderType } from '@appwrite.io/console';
|
||||
@@ -83,21 +83,13 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</div>
|
||||
{#if $operation === 'create'}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId
|
||||
bind:show={showCustomId}
|
||||
name="Message"
|
||||
bind:id={$messageParams[$providerType].messageId}
|
||||
autofocus={false} />
|
||||
{/if}
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Message ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<SMSPhone content={$messageParams[$providerType]['content']} classes="is-only-desktop" />
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
import { WizardStep } from '$lib/layout';
|
||||
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,28 +40,23 @@
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
async function beforeSubmit() {
|
||||
if (when === 'later') {
|
||||
$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
|
||||
|
||||
@@ -32,7 +32,6 @@ export type PushMessageParams = MessageParams & {
|
||||
badge?: string;
|
||||
};
|
||||
|
||||
export const operation = writable<'create' | 'update'>('create');
|
||||
export const providerType = writable<MessagingProviderType>(null);
|
||||
export const targetsById = writable<Record<string, Models.Target>>({});
|
||||
export const messageParams = writable<{
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user