mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.5.x' of https://github.com/appwrite/console into feat-file-picker
This commit is contained in:
Generated
+4
-4
@@ -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
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -41,6 +41,38 @@ export const toLocaleDateTime = (datetime: string | number) => {
|
||||
return date.toLocaleDateString('en', options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleDateISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleDateString('sv');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleTimeISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleTimeString('sv');
|
||||
};
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={createDomain}>
|
||||
<svelte:fragment slot="title">Add function domain</svelte:fragment>
|
||||
<svelte:fragment slot="title">Domain</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Use your self-owned domain as the endpoint of your Appwrite API. <a
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Create data</svelte:fragment>
|
||||
<svelte:fragment slot="title">Data</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Provide document data based on attributes you created earlier.
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -121,11 +121,7 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</Alert>
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`{ "myKey": "myValue" }`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
<InputTextarea label="Body" placeholder={`Hello, World!`} id="body" bind:value={body} />
|
||||
{:else}
|
||||
<InputText label="Path" id="path" placeholder="/" bind:value={path} required />
|
||||
|
||||
@@ -214,7 +210,7 @@
|
||||
|
||||
<InputTextarea
|
||||
label="Body"
|
||||
placeholder={`{ "myKey": "myValue" }`}
|
||||
placeholder={`Hello, World!`}
|
||||
id="body"
|
||||
bind:value={body} />
|
||||
</FormList>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Configuration function</svelte:fragment>
|
||||
<svelte:fragment slot="title">Configuration</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">Create your new Appwrite Function.</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
|
||||
@@ -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>
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputDate, InputTime, Helper } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { isSameDay, toLocaleDateISO, toLocaleTimeISO } from '$lib/helpers/date';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let now = new Date();
|
||||
let minDate: string;
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
let date = message.scheduledAt == null ? null : toLocaleDateISO(message.scheduledAt);
|
||||
let time = message.scheduledAt == null ? null : toLocaleTimeISO(message.scheduledAt);
|
||||
let dateTime: Date;
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hourCycle: 'h23',
|
||||
timeZoneName: 'longGeneric'
|
||||
};
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
dateTime.toISOString()
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been scheduled and will be sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
$: minDate = toLocaleDateISO(now.getTime());
|
||||
$: minTime = isSameDay(new Date(date), new Date(minDate))
|
||||
? toLocaleTimeISO(now.getTime())
|
||||
: '00:00';
|
||||
$: dateTime = new Date(`${date}T${time}`);
|
||||
</script>
|
||||
|
||||
<Modal title="Schedule message" bind:show onSubmit={update} headerDivider={false} size="big">
|
||||
<div>
|
||||
<FormList>
|
||||
<div
|
||||
class="u-grid u-gap-16"
|
||||
style="grid-auto-rows: 1fr; grid-template-columns: 1fr 1fr;">
|
||||
<InputDate id="date" label="Date" required={true} min={minDate} bind:value={date} />
|
||||
<InputTime id="time" label="Time" required={true} min={minTime} bind:value={time} />
|
||||
</div>
|
||||
</FormList>
|
||||
<Helper type="neutral">
|
||||
{#if !dateTime || isNaN(dateTime.getTime())}
|
||||
The message will be sent later
|
||||
{:else}
|
||||
The message will be sent at {dateTime.toLocaleString('en', formatOptions)}
|
||||
{/if}
|
||||
</Helper>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Schedule</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let show = false;
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let totalTargets = message.targets?.length ?? 0;
|
||||
|
||||
for (const topic of topics) {
|
||||
if (message.providerType == MessagingProviderType.Push) {
|
||||
totalTargets = totalTargets + topic.pushTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Email) {
|
||||
totalTargets = totalTargets + topic.emailTotal;
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
totalTargets = totalTargets + topic.smsTotal;
|
||||
}
|
||||
}
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: `The message has been sent to an estimated ${totalTargets} targets.`,
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate, {
|
||||
providerType: message.providerType,
|
||||
status
|
||||
});
|
||||
show = false;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title="Send message" bind:show onSubmit={update} headerDivider={false} size="small">
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<p data-private>
|
||||
You are about to send a message to an estimated <span class="u-bold"
|
||||
>{totalTargets}</span> targets. Would you like to proceed?
|
||||
</p>
|
||||
|
||||
<p class="u-bold">This action is irreversible.</p>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Send</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, Form, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
|
||||
let content = '';
|
||||
let disabled = true;
|
||||
|
||||
onMount(() => {
|
||||
content = message.data.content;
|
||||
});
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
content
|
||||
);
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Message has been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = content === message.data.content;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Message</Heading>
|
||||
<SMSPhone {content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={message.status != 'draft'}
|
||||
bind:value={content}></InputTextarea>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -1,29 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string> };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<SMSPhone content={message.data.content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let targets: Models.Target[];
|
||||
export let usersById: Record<string, Models.User<Models.Preferences>>;
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targets.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Identifier</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={target.$id}>{target.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{usersById[target.userId]?.name || 'Unknown'}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{target.providerType === MessagingProviderType.Push
|
||||
? target.name
|
||||
: target.identifier}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Target</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, Id } from '$lib/components';
|
||||
|
||||
export let topics: Models.Topic[];
|
||||
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topics.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead>ID</TableCellHead>
|
||||
<TableCellHead>Name</TableCellHead>
|
||||
<TableCellHead>Subscribers</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="id">
|
||||
<Id value={topic.$id}>{topic.$id}</Id>
|
||||
</TableCell>
|
||||
|
||||
<TableCellText title="name">
|
||||
{topic.name}
|
||||
</TableCellText>
|
||||
|
||||
<TableCellText title="subscribers">
|
||||
{topic.smsTotal + topic.emailTotal + topic.pushTotal}
|
||||
</TableCellText>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty>Edit the message to add a Topic</Empty>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
<script lang="ts">
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import { onMount } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { trackEvent, Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import UserTargetsModal from '../userTargetsModal.svelte';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, unknown> };
|
||||
export let selectedTargetsById: Record<string, Models.Target>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTargets = false;
|
||||
let targetsById: Record<string, Models.Target>;
|
||||
let targetIds: string[] = [];
|
||||
let targets: Models.Target[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
targetsById = { ...selectedTargetsById };
|
||||
});
|
||||
|
||||
function removeTarget(targetId: string) {
|
||||
const { [targetId]: _, ...rest } = targetsById;
|
||||
targetsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(
|
||||
message.$id,
|
||||
undefined,
|
||||
undefined,
|
||||
targetIds
|
||||
);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Targets have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
targetIds = [];
|
||||
targets = [];
|
||||
for (const targetId in targetsById) {
|
||||
targetIds.push(targetId);
|
||||
targets.push(targetsById[targetId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(targetIds, Object.keys(selectedTargetsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Targets</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = targetIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Target</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTargets = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow />
|
||||
{#each targets.slice(offset, offset + limit) as target (target.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Target">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2" data-private>
|
||||
{#if target.providerType === MessagingProviderType.Push}
|
||||
{target.name}
|
||||
{:else}
|
||||
{target.identifier}
|
||||
{/if}
|
||||
</span>
|
||||
</span></span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove">
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTarget(target.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total targets: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTargets = true)}>Add a target</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No targets have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/targets"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<UserTargetsModal
|
||||
title="Select targets"
|
||||
{providerType}
|
||||
bind:show={showTargets}
|
||||
{targetsById}
|
||||
on:update={(e) => {
|
||||
showTargets = false;
|
||||
targetsById = e.detail;
|
||||
}}>
|
||||
<svelte:fragment slot="description"
|
||||
>Select existing targets to which you want to send this message.</svelte:fragment>
|
||||
</UserTargetsModal>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { type Models, MessagingProviderType } from '@appwrite.io/console';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { CardGrid, Heading, Empty, PaginationInline, EmptySearch } from '$lib/components';
|
||||
import TopicsModal from '../topicsModal.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { Form, Button } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { getTotal } from '../wizard/store';
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
|
||||
export let message: Models.Message;
|
||||
export let selectedTopicsById: Record<string, Models.Topic>;
|
||||
|
||||
let providerType: MessagingProviderType;
|
||||
let offset = 0;
|
||||
const limit = 10;
|
||||
let showTopics = false;
|
||||
let topicsById: Record<string, Models.Topic> = {};
|
||||
let topicIds: string[] = [];
|
||||
let topics: Models.Topic[] = [];
|
||||
let disabled = true;
|
||||
|
||||
if (isValueOfStringEnum(MessagingProviderType, message.providerType)) {
|
||||
providerType = message.providerType;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
topicsById = { ...selectedTopicsById };
|
||||
});
|
||||
|
||||
function removeTopic(topicId: string) {
|
||||
const { [topicId]: _, ...rest } = topicsById;
|
||||
topicsById = rest;
|
||||
}
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (message.providerType == MessagingProviderType.Email) {
|
||||
await sdk.forProject.messaging.updateEmail(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Sms) {
|
||||
await sdk.forProject.messaging.updateSms(message.$id, topicIds);
|
||||
} else if (message.providerType == MessagingProviderType.Push) {
|
||||
await sdk.forProject.messaging.updatePush(message.$id, topicIds);
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_MESSAGE);
|
||||
addNotification({
|
||||
message: 'Topics have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
topicIds = [];
|
||||
topics = [];
|
||||
for (const topicId in topicsById) {
|
||||
topicIds.push(topicId);
|
||||
topics.push(topicsById[topicId]);
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = symmetricDifference(topicIds, Object.keys(selectedTopicsById)).length === 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={update}>
|
||||
<CardGrid hideFooter={message.status != 'draft'}>
|
||||
<Heading tag="h6" size="7" id="variables">Topics</Heading>
|
||||
<svelte:fragment slot="aside">
|
||||
{@const sum = topicIds.length}
|
||||
{#if sum}
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<div>
|
||||
<span class="eyebrow-heading-3">Topic</span>
|
||||
</div>
|
||||
{#if message.status == 'draft'}
|
||||
<Button
|
||||
text
|
||||
noMargin
|
||||
on:click={() => {
|
||||
showTopics = true;
|
||||
}}>
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-gap-24">
|
||||
<Table noMargin noStyles>
|
||||
<TableHeader>
|
||||
<TableCellHead style="padding: 0" />
|
||||
<TableCellHead width={40} style="padding: 0" />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each topics.slice(offset, offset + limit) as topic (topic.$id)}
|
||||
<TableRow>
|
||||
<TableCell title="Topic">
|
||||
<div class="u-flex u-cross-center">
|
||||
<span class="title">
|
||||
<span class="u-line-height-1-5">
|
||||
<span class="body-text-2 u-bold" data-private>
|
||||
{topic.name}
|
||||
</span>
|
||||
<span class="collapsible-button-optional">
|
||||
({getTotal(topic)} targets)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell title="Remove" width={40}>
|
||||
{#if message.status === 'draft'}
|
||||
<div
|
||||
class="u-flex u-main-end"
|
||||
style="--p-button-size: 1.25rem">
|
||||
<Button
|
||||
text
|
||||
class="is-only-icon"
|
||||
ariaLabel="delete"
|
||||
disabled={message.status != 'draft'}
|
||||
on:click={() => removeTopic(topic.$id)}>
|
||||
<span
|
||||
class="icon-x u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="u-flex u-main-space-between">
|
||||
<p class="text">Total topics: {sum}</p>
|
||||
<PaginationInline {sum} {limit} bind:offset />
|
||||
</div>
|
||||
</div>
|
||||
{:else if message.status == 'draft'}
|
||||
<Empty on:click={() => (showTopics = true)}>Add a topic</Empty>
|
||||
{:else}
|
||||
<EmptySearch hidePagination>
|
||||
<div class="u-text-center">
|
||||
No topics have been selected.
|
||||
<p>
|
||||
Need a hand? Check out our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/messaging/topics"
|
||||
text>
|
||||
documentation</Button
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button {disabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
<TopicsModal
|
||||
{providerType}
|
||||
bind:show={showTopics}
|
||||
{topicsById}
|
||||
on:update={(e) => {
|
||||
showTopics = false;
|
||||
topicsById = e.detail;
|
||||
}} />
|
||||
@@ -44,8 +44,8 @@
|
||||
providerId,
|
||||
$providerParams[$provider].name,
|
||||
$providerParams[$provider].from,
|
||||
$providerParams[$provider].username,
|
||||
$providerParams[$provider].password,
|
||||
$providerParams[$provider].customerId,
|
||||
$providerParams[$provider].apiKey,
|
||||
$providerParams[$provider].enabled
|
||||
);
|
||||
break;
|
||||
@@ -156,7 +156,7 @@
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Proivder',
|
||||
label: 'Provider',
|
||||
component: Provider
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
|
||||
+2
-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;
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { Roles } from '$lib/components/permissions';
|
||||
import { symmetricDifference } from '$lib/helpers/array';
|
||||
import { topic } from './store';
|
||||
|
||||
const topicId = $page.params.topic;
|
||||
|
||||
let arePermsDisabled = true;
|
||||
let permissions: string[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
permissions = $topic.subscribe || [];
|
||||
});
|
||||
|
||||
async function updatePermissions() {
|
||||
try {
|
||||
await sdk.forProject.messaging.updateTopic(topicId, undefined, permissions);
|
||||
await invalidate(Dependencies.MESSAGING_TOPIC);
|
||||
addNotification({
|
||||
message: 'Permissions have been updated',
|
||||
type: 'success'
|
||||
});
|
||||
trackEvent(Submit.MessagingTopicUpdatePermissions);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.MessagingTopicUpdatePermissions);
|
||||
}
|
||||
}
|
||||
|
||||
$: arePermsDisabled = symmetricDifference(permissions, $topic.subscribe).length == 0;
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updatePermissions}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7" id="permissions">Subscription access</Heading>
|
||||
<p>
|
||||
Choose who can subscribe to this topic using the client API. Learn more about <a
|
||||
href="https://appwrite.io/docs/advanced/platform/permissions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link">
|
||||
Permissions</a
|
||||
>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Roles bind:roles={permissions} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={arePermsDisabled} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -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'}
|
||||
|
||||
@@ -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