Merge branch '1.5.x' of github.com:appwrite/console into feat-new-array-filters

This commit is contained in:
Arman
2024-02-28 16:38:36 +01:00
110 changed files with 1724 additions and 1321 deletions
+4 -4
View File
@@ -6,7 +6,7 @@
"": {
"name": "@appwrite/console",
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.12",
"@appwrite.io/console": "^0.6.0-rc.14",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@popperjs/core": "^2.11.8",
@@ -158,9 +158,9 @@
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
},
"node_modules/@appwrite.io/console": {
"version": "0.6.0-rc.12",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.6.0-rc.12.tgz",
"integrity": "sha512-pDov75TCkBmJdUPrWuzRW7uM28HYqiMFtVFrUoavDd/lMPBS7RnPXL4sBHsGY8AUldHE3oBcGVPkNoQfEAgLlw==",
"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"
+1 -1
View File
@@ -18,7 +18,7 @@
"e2e": "playwright test tests/e2e"
},
"dependencies": {
"@appwrite.io/console": "^0.6.0-rc.12",
"@appwrite.io/console": "^0.6.0-rc.14",
"@appwrite.io/pink": "0.4.0",
"@appwrite.io/pink-icons": "0.4.0",
"@popperjs/core": "^2.11.8",
+13
View File
@@ -0,0 +1,13 @@
import { AppwriteException } from '@appwrite.io/console';
import type { HandleClientError } from '@sveltejs/kit';
export const handleError: HandleClientError = async ({ error, message, status }) => {
if (error instanceof AppwriteException && error.code === 0) {
status = undefined;
message = error.message;
}
return {
message,
status
};
};
+1 -1
View File
@@ -301,7 +301,7 @@ export enum Submit {
MessagingTopicCreate = 'submit_messaging_topic_create',
MessagingTopicDelete = 'submit_messaging_topic_delete',
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
MessagingTopicUpdateDescription = 'submit_messaging_topic_update_description',
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
}
+34 -13
View File
@@ -1,18 +1,39 @@
<script lang="ts">
export let href: string;
export let href: string = null;
export let external: boolean = false;
</script>
<li class="clickable-list-item">
<a {href} class="clickable-list-button" on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</a>
{#if href}
<a
{href}
class="clickable-list-button"
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</a>
{:else}
<button class="clickable-list-button u-width-full-line" on:click>
{#if $$slots.default}
<h5 class="clickable-list-title u-trim-1">
<slot />
</h5>
{/if}
{#if $$slots.desc}
<div class="clickable-list-desc">
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
</div>
{/if}
</button>
{/if}
</li>
+31 -4
View File
@@ -3,10 +3,11 @@
export let withIndentation = false;
export let open = false;
export let disabled = false;
</script>
<li class="collapsible-item">
<details class="collapsible-wrapper" {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" 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>
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import { app } from '$lib/stores/app';
export let darkSrc: string;
export let lightSrc: string;
export let alt: string;
</script>
<a
href={$app.themeInUse === 'dark' ? darkSrc : lightSrc}
class="file-preview is-with-image"
style="inline-size: 100%; block-size: 100%;"
target="_blank"
rel="noopener noreferrer"
aria-label="open file in new window">
<div class="file-preview-image">
<img src={$app.themeInUse === 'dark' ? darkSrc : lightSrc} {alt} />
</div>
<div class="file-preview-content">
<div class="avatar">
<span class="icon-external-link" aria-hidden="true" />
</div>
</div>
</a>
+1
View File
@@ -72,3 +72,4 @@ export { default as FakeModal } from './fakeModal.svelte';
export { default as RadioBoxes } from './radioBoxes.svelte';
export { default as ModalWrapper } from './modalWrapper.svelte';
export { default as ModalSideCol } from './modalSideCol.svelte';
export { default as ImagePreview } from './imagePreview.svelte';
+9 -12
View File
@@ -16,16 +16,13 @@
<svelte:window on:resize={throttle(onResize, 250)} />
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
{#if showTooltip}
<span
use:tooltip={{
content: container.innerText,
maxWidth: '30rem'
}}>
<slot />
</span>
{:else}
<span><slot /></span>
{/if}
<span
class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`}
bind:this={container}
use:tooltip={{
disabled: !showTooltip,
content: container?.innerText ?? undefined,
maxWidth: '30rem'
}}>
<span><slot /></span>
</span>
+18 -3
View File
@@ -13,6 +13,8 @@
let element: HTMLOListElement;
let autoSubmitted = false;
const {
elements: { root, input }
} = createPinInput({
@@ -20,14 +22,27 @@
defaultValue: value.split(''),
onValueChange: ({ next }) => {
value = next.join('');
if (value.length === 6 && !autoSubmitted) {
autoSubmitted = true;
const firstInputElement = element.querySelector('input');
firstInputElement?.form.requestSubmit();
}
return next;
}
});
onMount(() => {
if (element && autofocus) {
element.querySelector('input')?.focus();
}
const interval = setInterval(() => {
const input = element.querySelector('input');
if (element) {
if (input && autofocus) {
input.focus();
}
clearInterval(interval);
}
}, 10);
});
</script>
+1 -1
View File
@@ -34,7 +34,7 @@
</script>
<WizardStep beforeSubmit={createDomain}>
<svelte:fragment slot="title">Add function domain</svelte:fragment>
<svelte:fragment slot="title">Domain</svelte:fragment>
<svelte:fragment slot="subtitle">
Use your self-owned domain as the endpoint of your Appwrite API. <a
href="https://appwrite.io/docs/advanced/platform/custom-domains"
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Client, Models, Query } from '@appwrite.io/console';
import type { Client, Models } from '@appwrite.io/console';
import type { Organization } from '../stores/organization';
import type { PaymentMethod } from '@stripe/stripe-js';
import type { Tier } from '$lib/stores/billing';
@@ -849,7 +849,7 @@ export class Billing {
);
}
async listAddresses(queries: Query[] = []): Promise<AddressesList> {
async listAddresses(queries: string[] = []): Promise<AddressesList> {
const path = `/account/billing-addresses`;
const params = {
queries
+19 -17
View File
@@ -35,21 +35,23 @@ export const createMigrationFormStore = () => {
};
};
const resources = [
'user',
'team',
'membership',
'file',
'bucket',
'function',
'envVar',
'deployment',
'database',
'collection',
'index',
'attribute',
'document'
] as const;
export const ResourcesFriendly = {
user: { singular: 'User', plural: 'Users' },
team: { singular: 'Team', plural: 'Teams' },
membership: { singular: 'Membership', plural: 'Memberships' },
file: { singular: 'File', plural: 'Files' },
bucket: { singular: 'Bucket', plural: 'Buckets' },
function: { singular: 'Function', plural: 'Functions' },
'environment variable': { singular: 'Environment Variable', plural: 'Environment Variables' },
deployment: { singular: 'Deployment', plural: 'Deployments' },
database: { singular: 'Database', plural: 'Databases' },
collection: { singular: 'Collection', plural: 'Collections' },
index: { singular: 'Index', plural: 'Indexes' },
attribute: { singular: 'Attribute', plural: 'Attributes' },
document: { singular: 'Document', plural: 'Documents' }
};
const resources = Object.keys(ResourcesFriendly);
type Resource = (typeof resources)[number];
@@ -101,7 +103,7 @@ export const migrationFormToResources = (
addResource('function');
}
if (formData.functions.env) {
addResource('envVar');
addResource('environment variable');
}
if (formData.functions.inactive) {
addResource('deployment');
@@ -155,7 +157,7 @@ export const resourcesToMigrationForm = (
if (resources.includes('function') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.root = true;
}
if (resources.includes('envVar') && isVersionAtLeast(version, '1.4.0')) {
if (resources.includes('environment variable') && isVersionAtLeast(version, '1.4.0')) {
formData.functions.env = true;
}
if (resources.includes('deployment') && isVersionAtLeast(version, '1.4.0')) {
+10
View File
@@ -40,6 +40,11 @@ function createServices() {
method: ApiService.Locale,
value: null
},
{
label: 'Messaging',
method: ApiService.Messaging,
value: null
},
{
label: 'Storage',
method: ApiService.Storage,
@@ -98,6 +103,11 @@ function createServices() {
method: ApiService.Locale,
value: project.serviceStatusForLocale
},
{
label: 'Messaging',
method: ApiService.Messaging,
value: project.serviceStatusForMessaging
},
{
label: 'Storage',
method: ApiService.Storage,
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@
</script>
<Unauthenticated>
<Heading size="1" tag="h3">{$page.status}</Heading>
<Heading size="1" tag="h3">{$page.error.status || 'Invalid Argument'}</Heading>
<Heading size="3" tag="h4">{$page.error.message}</Heading>
<Button href="/">Back to the console</Button>
</Unauthenticated>
+2 -1
View File
@@ -54,7 +54,8 @@
'/recover',
'/invite',
'/card',
'/hackathon'
'/hackathon',
'/mfa'
];
if ($user) {
if (
+4 -3
View File
@@ -13,9 +13,10 @@ export const ssr = false;
export const load: LayoutLoad = async ({ depends, url }) => {
depends(Dependencies.ACCOUNT);
redirectTo.set(url.searchParams.get('forceRedirect') || null);
url.searchParams.delete('forceRedirect');
if (url.searchParams.has('forceRedirect')) {
redirectTo.set(url.searchParams.get('forceRedirect') || null);
url.searchParams.delete('forceRedirect');
}
try {
const account = await sdk.forConsole.account.get<{ organization?: string }>();
@@ -3,7 +3,7 @@
import { Button } from '$lib/elements/forms';
import { deepMap } from '$lib/helpers/object';
import type { WritableValue } from '$lib/helpers/types';
import { sdk, type getSdkForProject } from '$lib/stores/sdk';
import { type getSdkForProject } from '$lib/stores/sdk';
import { onMount } from 'svelte';
@@ -107,19 +107,10 @@
);
break;
case 'firebase':
if ($provider.projectId) {
// OAuth
report = await sdk.forProject.migrations.getFirebaseReportOAuth(
providerResources.firebase,
$provider.projectId
);
} else if ($provider.serviceAccount) {
// Manual auth
report = await projectSdk.migrations.getFirebaseReport(
providerResources.firebase,
$provider.serviceAccount
);
}
report = await projectSdk.migrations.getFirebaseReport(
providerResources.firebase,
$provider.serviceAccount
);
break;
case 'nhost':
@@ -353,7 +344,7 @@
<div />
<span>Import all functions and their active deployment</span>
<ul>
{#if resources?.includes('envVar')}
{#if resources?.includes('environment variable')}
<li class="checkbox-field">
<input
type="checkbox"
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@
</script>
<Container>
<Heading size="1" tag="h3">{$page.status}</Heading>
<Heading size="1" tag="h3">{$page.error.status || 'Invalid Argument'}</Heading>
<Heading size="3" tag="h4">{$page.error.message}</Heading>
<Button href="/console">Back to the console</Button>
</Container>
+6 -2
View File
@@ -39,14 +39,18 @@
</script>
<Modal
title="Delete authentication provider"
title="Delete authentication method"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p>Are you sure you want to delete this authentication method from your account?</p>
<p class="u-bold">
Deleting the authentication method will disable multi-factor authentication for your
account. To re-enable it, you'll need to add a new authentication method.
</p>
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
+36 -5
View File
@@ -13,6 +13,7 @@
export let showSetup = false;
export let showRecoveryCodes = false;
let copyParent: HTMLElement;
let code: string;
let type: Models.MfaType = null;
async function addAuthenticator(): Promise<URL> {
@@ -48,13 +49,43 @@
Install an authenticator app on your mobile device, open it and scan the provided QR
code or enter it manually.
</p>
<div class="qr">
{#await addAuthenticator()}
{#await addAuthenticator()}
<div class="qr">
<LoadingDots />
{:then qr}
</div>
{:then qr}
<div class="qr">
<img alt="MFA QR Code" class="code" src={qr.toString()} />
{/await}
</div>
</div>
<span class="with-separators eyebrow-heading-3">or</span>
<div>
<label class="label" for="manual-code">Manual entry code</label>
<button class="tooltip" aria-label="sec retinfo">
<span class="icon-info" aria-hidden="true"></span>
<span class="tooltip-popup" role="tooltip">
<p class="text u-margin-block-start-8">
Manually enter the following code into the authenticator app
</p>
</span>
</button>
<div class="input-text-wrapper" style="--amount-of-buttons:1">
<input id="manual-code" type="text" value={type.secret} readonly={true} />
<div class="options-list">
<div bind:this={copyParent}>
{#key copyParent}
<Copy value={type.secret} appendTo="parent">
<span class="icon-duplicate" aria-hidden="true" />
</Copy>
{/key}
</div>
</div>
</div>
</div>
{/await}
<hr />
<FormList>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
@@ -10,6 +10,7 @@ import { headerAlert } from '$lib/stores/headerAlert';
import ProjectsAtRisk from '$lib/components/billing/alerts/projectsAtRisk.svelte';
import { get } from 'svelte/store';
import { preferences } from '$lib/stores/preferences';
import type { Organization } from '$lib/stores/organization';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.ORGANIZATION);
@@ -37,7 +38,9 @@ export const load: LayoutLoad = async ({ params, depends }) => {
return {
header: Header,
breadcrumbs: Breadcrumbs,
organization: await sdk.forConsole.teams.get(params.organization),
organization: await (sdk.forConsole.teams.get(
params.organization
) as Promise<Organization>),
members: await sdk.forConsole.teams.listMemberships(params.organization)
};
} catch (e) {
@@ -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.
@@ -57,7 +57,7 @@
}
</script>
<Modal title="Invite Member" {error} size="big" bind:show={showCreate} onSubmit={create}>
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
{#if isCloud}
<Alert type="info">
{#if $organization?.billingPlan === BillingPlan.SCALE}
@@ -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"
@@ -5,6 +5,6 @@
</script>
<Container>
<Heading size="1" tag="h3">{$page.status}</Heading>
<Heading size="1" tag="h3">{$page.error.status || 'Invalid Argument'}</Heading>
<p class="body-text-2 u-bold u-margin-block-start-4">{$page.error.message}</p>
</Container>
@@ -5,6 +5,7 @@ import type { LayoutLoad } from './$types';
import { preferences } from '$lib/stores/preferences';
import { failedInvoice } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import type { Organization } from '$lib/stores/organization';
export const load: LayoutLoad = async ({ params, depends }) => {
depends(Dependencies.PROJECT);
@@ -22,7 +23,7 @@ export const load: LayoutLoad = async ({ params, depends }) => {
return {
project,
organization: await sdk.forConsole.teams.get(project.teamId)
organization: await (sdk.forConsole.teams.get(project.teamId) as Promise<Organization>)
};
} catch (e) {
error(e.code, e.message);
@@ -4,8 +4,6 @@
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import FormList from '$lib/elements/forms/formList.svelte';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorType } from '@appwrite.io/console';
@@ -13,12 +11,11 @@
export let showDelete = false;
let code: string;
let error: string;
async function deleteProvider() {
try {
await sdk.forConsole.users.deleteAuthenticator($user.$id, AuthenticatorType.Totp, '');
await sdk.forConsole.users.deleteAuthenticator($user.$id, AuthenticatorType.Totp);
await invalidate(Dependencies.ACCOUNT);
await invalidate(Dependencies.FACTORS);
showDelete = false;
@@ -34,23 +31,23 @@
}
$: if (showDelete) {
code = '';
error = '';
}
</script>
<Modal
title="Delete authentication provider"
title="Delete authentication method"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
bind:error
headerDivider={false}>
<p class="u-bold">
Deleting the authentication method will disable multi-factor authentication for this
account. To re-enable it, user will need to add a new authentication method.
</p>
<p>Are you sure you want to delete this authentication method from your account?</p>
<FormList>
<InputDigits autofocus required bind:value={code} />
</FormList>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
@@ -5,7 +5,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Create data</svelte:fragment>
<svelte:fragment slot="title">Data</svelte:fragment>
<svelte:fragment slot="subtitle">
Provide document data based on attributes you created earlier.
</svelte:fragment>
@@ -121,11 +121,7 @@
</Button>
</svelte:fragment>
</Alert>
<InputTextarea
label="Body"
placeholder={`{ "myKey": "myValue" }`}
id="body"
bind:value={body} />
<InputTextarea label="Body" placeholder={`Hello, World!`} id="body" bind:value={body} />
{:else}
<InputText label="Path" id="path" placeholder="/" bind:value={path} required />
@@ -214,7 +210,7 @@
<InputTextarea
label="Body"
placeholder={`{ "myKey": "myValue" }`}
placeholder={`Hello, World!`}
id="body"
bind:value={body} />
</FormList>
@@ -20,7 +20,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Configuration function</svelte:fragment>
<svelte:fragment slot="title">Configuration</svelte:fragment>
<svelte:fragment slot="subtitle">Create your new Appwrite Function.</svelte:fragment>
<FormList>
<InputText
@@ -28,7 +28,7 @@
} from '$lib/elements/table';
import { toLocaleDateTime } from '$lib/helpers/date';
import { Container } from '$lib/layout';
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
import { MessagingProviderType } from '@appwrite.io/console';
import type { PageData } from './$types';
import CreateMessageDropdown from './createMessageDropdown.svelte';
import FailedModal from './failedModal.svelte';
@@ -141,7 +141,7 @@
<TableCellCheck
bind:selectedIds={selected}
id={message.$id}
disabled={message.status === MessageStatus.Processing} />
disabled={message.status === 'processing'} />
{#each $columns as column (column.id)}
{#if column.show}
@@ -12,7 +12,7 @@
import Wizard from '../wizard.svelte';
import type { PageData } from './$types';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { MessageStatus, MessagingProviderType } from '@appwrite.io/console';
import { MessagingProviderType } from '@appwrite.io/console';
import Topics from './topics.svelte';
import Targets from './targets.svelte';
@@ -35,7 +35,7 @@
topics: $message.topics,
users: $message.users,
targets: $message.targets,
status: MessageStatus.Draft,
draft: $message.status === 'draft',
scheduledAt: $message.scheduledAt
};
@@ -98,21 +98,15 @@
<Container>
<Overview />
{#if $message.providerType === MessagingProviderType.Email}
<EmailPreview
message={$message}
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
<EmailPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
{:else if $message.providerType === MessagingProviderType.Sms}
<SMSPreview
message={$message}
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
<SMSPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
{:else if $message.providerType === MessagingProviderType.Push}
<PushPreview
message={$message}
onEdit={$message.status === MessageStatus.Draft ? onEdit : null} />
<PushPreview message={$message} onEdit={$message.status === 'draft' ? onEdit : null} />
{/if}
<Topics topics={Object.values(data.topicsById)} />
<Targets targets={Object.values(data.targetsById)} usersById={data.usersById} />
{#if $message.status !== MessageStatus.Processing}
{#if $message.status !== 'processing'}
<Delete message={$message} />
{/if}
</Container>
@@ -32,7 +32,7 @@
}
</script>
<CardGrid>
<CardGrid hideFooter={$message.status != 'failed'}>
<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>
@@ -51,15 +51,13 @@
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $message.status === 'failed'}
<Button
secondary
on:click={(e) => {
e.preventDefault();
errors = $message.deliveryErrors;
showFailed = true;
}}>View logs</Button>
{/if}
<Button
secondary
on:click={(e) => {
e.preventDefault();
errors = $message.deliveryErrors;
showFailed = true;
}}>View logs</Button>
</svelte:fragment>
</CardGrid>
@@ -11,6 +11,50 @@
FCM = 'fcm',
APNS = 'apns'
}
export function getProviderDisplayNameAndIcon(provider: Providers | string) {
let icon = '';
let displayName = provider.charAt(0).toUpperCase() + provider.slice(1);
switch (provider) {
case Providers.FCM:
icon = 'firebase';
displayName = 'FCM';
break;
case Providers.APNS:
icon = 'apple';
displayName = 'APNS';
break;
case Providers.Sendgrid:
icon = 'sendgrid';
break;
case Providers.Mailgun:
icon = 'mailgun';
break;
case Providers.SMTP:
icon = 'smtp';
displayName = 'SMTP';
break;
case Providers.Twilio:
icon = 'twilio';
break;
case Providers.Telesign:
icon = 'telesign';
break;
case Providers.Msg91:
icon = 'msg91';
displayName = 'MSG91';
break;
case Providers.Textmagic:
icon = 'textmagic';
displayName = 'Textmagic';
break;
case Providers.Vonage:
icon = 'vonage';
break;
}
return { icon, displayName };
}
</script>
<script lang="ts">
@@ -22,8 +66,7 @@
export let noIcon = false;
export let size: 's' | 'm' | 'l' = 'm';
let icon = '';
let displayName = name || provider.charAt(0).toUpperCase() + provider.slice(1);
const { icon, displayName } = getProviderDisplayNameAndIcon(provider);
let textSize = '1.25rem';
switch (size) {
@@ -34,44 +77,6 @@
textSize = '1.5rem';
break;
}
switch (provider) {
case Providers.FCM:
icon = 'firebase';
displayName = name || 'FCM';
break;
case Providers.APNS:
icon = 'apple';
displayName = name || 'APNS';
break;
case Providers.Sendgrid:
icon = 'sendgrid';
break;
case Providers.Mailgun:
icon = 'mailgun';
break;
case Providers.SMTP:
icon = 'smtp';
displayName = name || 'SMTP';
break;
case Providers.Twilio:
icon = 'twilio';
break;
case Providers.Telesign:
icon = 'telesign';
break;
case Providers.Msg91:
icon = 'msg91';
displayName = name || 'MSG91';
break;
case Providers.Textmagic:
icon = 'textmagic';
displayName = name || 'TextMagic';
break;
case Providers.Vonage:
icon = 'vonage';
break;
}
</script>
{#if icon === ''}
@@ -89,12 +94,12 @@
<img
style:--p-text-size={textSize}
src={`${base}/icons/${$app.themeInUse}/color/${icon}.svg`}
alt={displayName} />
alt={name || displayName} />
{/if}
</div>
{/if}
<slot>
{displayName}
{name || displayName}
</slot>
</div>
{/if}
@@ -1,9 +1,8 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import Provider from './wizard/provider.svelte';
import Configure from './wizard/configure.svelte';
import Settings from './wizard/settings.svelte';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
@@ -45,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;
@@ -130,6 +129,7 @@
$providerParams[$provider].authKeyId,
$providerParams[$provider].teamId,
$providerParams[$provider].bundleId,
$providerParams[$provider].sandbox,
$providerParams[$provider].enabled
);
break;
@@ -154,18 +154,14 @@
}
}
onDestroy(() => {
console.log('destroy');
});
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Proivder',
label: 'Provider',
component: Provider
});
stepsComponents.set(2, {
label: 'Configure',
component: Configure
label: 'Settings',
component: Settings
});
</script>
@@ -0,0 +1,37 @@
<script lang="ts">
import { ImagePreview } from '$lib/components';
export let lines = [];
export let popoverImage: { src: { dark: string; light: string }; alt: string } | null = null;
</script>
<div class="u-flex-vertical u-gap-16">
{#each lines as line}
<p>{@html line}</p>
{/each}
{#if popoverImage}
<ImagePreview
darkSrc={popoverImage.src.dark}
lightSrc={popoverImage.src.light}
alt={popoverImage.alt} />
{/if}
</div>
<style>
div > p {
color: hsl(var(--color-neutral-50));
&:first-child {
color: hsl(var(--color-neutral-70));
}
}
:global(.theme-dark) div > p {
color: hsl(var(--color-neutral-20));
&:first-child {
color: hsl(var(--color-neutral-10));
}
}
</style>
@@ -2,11 +2,160 @@
import { Container } from '$lib/layout';
import DangerZone from './dangerZone.svelte';
import UpdateName from './updateName.svelte';
import UpdateSettings from './updateSettings.svelte';
import UpdateStatus from './updateStatus.svelte';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { MessagingProviderType } from '@appwrite.io/console';
import { Providers } from '../../provider.svelte';
import { providers } from '../store';
import { provider as providerData } from './store';
let providerType: MessagingProviderType;
if (isValueOfStringEnum(MessagingProviderType, $providerData.type)) {
providerType = $providerData.type;
}
let provider: Providers;
if (isValueOfStringEnum(Providers, $providerData.provider)) {
provider = $providerData.provider;
}
let params: object = {};
$: switch (provider) {
case Providers.Twilio:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
accountSid: $providerData.credentials['accountSid'],
authToken: $providerData.credentials['authToken'],
from: $providerData.options['from']
};
break;
case Providers.Msg91:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
from: $providerData.options['from'],
senderId: $providerData.credentials['senderId'],
authKey: $providerData.credentials['authKey']
};
break;
case Providers.Telesign:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
customerId: $providerData.credentials['customerId'],
apiKey: $providerData.credentials['apiKey'],
from: $providerData.options['from']
};
break;
case Providers.Textmagic:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
username: $providerData.credentials['username'],
apiKey: $providerData.credentials['apiKey'],
from: $providerData.options['from']
};
break;
case Providers.Vonage:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
apiKey: $providerData.credentials['apiKey'],
apiSecret: $providerData.credentials['apiSecret'],
from: $providerData.options['from']
};
break;
case Providers.Mailgun:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
isEuRegion: false,
fromEmail: $providerData.options['fromEmail'],
fromName: $providerData.options['fromName'],
replyToEmail: $providerData.options['replyToEmail'],
replyToName: $providerData.options['replyToName'],
apiKey: $providerData.credentials['apiKey'],
domain: $providerData.credentials['domain']
};
break;
case Providers.Sendgrid:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
apiKey: $providerData.credentials['apiKey'],
fromEmail: $providerData.options['fromEmail'],
fromName: $providerData.options['fromName'],
replyToEmail: $providerData.options['replyToEmail'],
replyToName: $providerData.options['replyToName']
};
break;
case Providers.SMTP:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
host: $providerData.credentials['host'],
port: $providerData.credentials['port'],
username: $providerData.credentials['username'],
password: $providerData.credentials['password'],
fromName: $providerData.options['fromName'],
fromEmail: $providerData.options['fromEmail'],
replyToName: $providerData.options['replyToName'],
replyToEmail: $providerData.options['replyToEmail'],
encryption: !$providerData.options['encryption']
? 'none'
: $providerData.options['encryption'],
autoTLS: $providerData.options['autoTLS'],
mailer: $providerData.options['mailer']
};
break;
case Providers.FCM:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled
};
if ('serviceAccountJSON' in $providerData.credentials) {
const serviceAccountJSON = $providerData.credentials['serviceAccountJSON'];
if (typeof serviceAccountJSON === 'string') {
params['serviceAccountJSON'] = serviceAccountJSON;
} else if (serviceAccountJSON instanceof Object) {
params['serviceAccountJSON'] = JSON.stringify(serviceAccountJSON);
}
}
break;
case Providers.APNS:
params = {
providerId: $providerData.$id,
name: $providerData.name,
enabled: $providerData.enabled,
authKey: $providerData.credentials['authKey'],
authKeyId: $providerData.credentials['authKeyId'],
teamId: $providerData.credentials['teamId'],
bundleId: $providerData.credentials['bundleId']
};
break;
}
</script>
<Container>
<UpdateStatus />
<UpdateName />
<UpdateSettings
inputs={providers[providerType].providers[provider].configure}
providerId={$providerData.$id}
{providerType}
{provider}
{params} />
<DangerZone />
</Container>
@@ -2,8 +2,4 @@ import { derived } from 'svelte/store';
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
export const provider = derived(
page,
// TODO: Set actual type
($page) => $page.data.provider as Models.Provider
);
export const provider = derived(page, ($page) => $page.data.provider as Models.Provider);
@@ -69,13 +69,13 @@
);
break;
case Providers.FCM:
response = await sdk.forProject.messaging.updateFCMProvider(
response = await sdk.forProject.messaging.updateFcmProvider(
providerId,
providerName
);
break;
case Providers.APNS:
response = await sdk.forProject.messaging.updateAPNSProvider(
response = await sdk.forProject.messaging.updateApnsProvider(
providerId,
providerName
);
@@ -0,0 +1,233 @@
<script lang="ts">
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { addNotification } from '$lib/stores/notifications';
import { invalidate } from '$app/navigation';
import type { ProviderInput } from '../store';
import { Providers, getProviderDisplayNameAndIcon } from '../../provider.svelte';
import { Dependencies } from '$lib/constants';
import { MessagingProviderType, type Models } from '@appwrite.io/console';
import SettingsFormList from '../settingsFormList.svelte';
import { CardGrid, Heading } from '$lib/components';
import { Button, Form } from '$lib/elements/forms';
import { onMount } from 'svelte';
import CreateMember from '$routes/console/organization-[organization]/createMember.svelte';
export let inputs: ProviderInput[];
export let providerType: MessagingProviderType;
export let provider: Providers;
export let providerId: string;
export let params: object;
let formValues: object = {};
let originalfiles: Record<string, FileList> = {};
let files: Record<string, FileList> = {};
let updated = false;
let newMemberModal = false;
let message = '';
const { displayName } = getProviderDisplayNameAndIcon(provider);
switch (providerType) {
case MessagingProviderType.Push:
message = 'notifications';
break;
case MessagingProviderType.Sms:
message = 'SMS';
break;
case MessagingProviderType.Email:
message = 'emails';
break;
}
onMount(() => {
for (const input of inputs) {
const value = params[input.name];
formValues[input.name] = value;
if (input.type === 'file' && typeof value == 'string' && value.length > 0) {
const dataTransfer = new DataTransfer();
const f = new File([value], `${input.name}.${input.allowedFileExtensions}`);
dataTransfer.items.add(f);
files[input.name] = dataTransfer.files;
originalfiles[input.name] = dataTransfer.files;
}
}
});
async function update() {
try {
const promises = [];
for (const [key, value] of Object.entries(files)) {
const promise = value[0].text().then((text) => {
formValues[key] = text;
});
promises.push(promise);
}
await Promise.all(promises);
let response: Models.Provider;
switch (provider) {
case Providers.Twilio:
response = await sdk.forProject.messaging.updateTwilioProvider(
providerId,
undefined,
undefined,
formValues['accountSid'],
formValues['authToken'],
formValues['from']
);
break;
case Providers.Msg91:
response = await sdk.forProject.messaging.updateMsg91Provider(
providerId,
undefined,
undefined,
formValues['senderId'],
formValues['authKey'],
formValues['from']
);
break;
case Providers.Telesign:
response = await sdk.forProject.messaging.updateTelesignProvider(
providerId,
undefined,
undefined,
formValues['customerId'],
formValues['apiKey'],
formValues['from']
);
break;
case Providers.Textmagic:
response = await sdk.forProject.messaging.updateTextmagicProvider(
providerId,
undefined,
undefined,
formValues['username'],
formValues['apiKey'],
formValues['from']
);
break;
case Providers.Vonage:
response = await sdk.forProject.messaging.updateVonageProvider(
providerId,
undefined,
undefined,
formValues['apiKey'],
formValues['apiSecret'],
formValues['from']
);
break;
case Providers.Mailgun:
response = await sdk.forProject.messaging.updateMailgunProvider(
providerId,
undefined,
formValues['apiKey'],
formValues['domain'],
formValues['isEuRegion'],
undefined,
formValues['fromName'] || undefined,
formValues['fromEmail'],
formValues['replyToName'] || undefined,
formValues['replyToEmail'] || undefined
);
break;
case Providers.Sendgrid:
response = await sdk.forProject.messaging.updateSendgridProvider(
providerId,
undefined,
undefined,
formValues['apiKey'],
formValues['fromName'] || undefined,
formValues['fromEmail'],
formValues['replyToName'] || undefined,
formValues['replyToEmail'] || undefined
);
break;
case Providers.SMTP:
response = await sdk.forProject.messaging.updateSmtpProvider(
providerId,
undefined,
formValues['host'],
formValues['port'] || undefined,
formValues['username'] || undefined,
formValues['password'] || undefined,
formValues['encryption'],
formValues['autoTLS'],
undefined,
formValues['fromName'] || undefined,
formValues['fromEmail'],
formValues['replyToName'] || undefined,
formValues['replyToEmail'] || undefined
);
break;
case Providers.FCM:
response = await sdk.forProject.messaging.updateFcmProvider(
providerId,
undefined,
undefined,
JSON.parse(formValues['serviceAccountJSON'])
);
break;
case Providers.APNS:
response = await sdk.forProject.messaging.updateApnsProvider(
providerId,
undefined,
undefined,
formValues['authKey'],
formValues['authKeyId'],
formValues['teamId'],
formValues['bundleId']
);
break;
}
originalfiles = files;
await invalidate(Dependencies.MESSAGING_PROVIDER);
addNotification({
type: 'success',
message: `${response.name} has been updated`
});
trackEvent(Submit.MessagingProviderUpdate, {
provider: provider
});
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingProviderUpdate);
}
}
$: updated =
inputs
.filter((input) => input.type !== 'file')
.some((input) => formValues[input.name] !== params[input.name]) ||
Object.keys(files).some((key) => files[key] !== originalfiles[key]);
</script>
<Form onSubmit={update}>
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
<Heading tag="h6" size="7">Settings</Heading>
</div>
<p>
Configure the settings to <Button
link
href={`https://appwrite.io/docs/products/messaging/providers/${provider}`}
external>enable {displayName}</Button> to send {message}, or <Button
link
on:click={() => (newMemberModal = true)}>invite a team member</Button> to complete the
provider settings.
</p>
<svelte:fragment slot="aside">
<SettingsFormList bind:files {inputs} bind:params={formValues} />
</svelte:fragment>
<svelte:fragment slot="actions">
<div class="u-flex u-flex-wrap u-gap-12">
<Button disabled={!updated} submit>Update</Button>
</div>
</svelte:fragment>
</CardGrid>
</Form>
<CreateMember bind:showCreate={newMemberModal} />
@@ -5,167 +5,24 @@
import { Dependencies } from '$lib/constants';
import { Button, InputSwitch } from '$lib/elements/forms';
import { toLocaleDateTime } from '$lib/helpers/date';
import { isValueOfStringEnum } from '$lib/helpers/types';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { wizard } from '$lib/stores/wizard';
import { onMount } from 'svelte';
import { MessagingProviderType } from '@appwrite.io/console';
import Provider, { Providers } from '../../provider.svelte';
import ProviderType from '../../providerType.svelte';
import Update from '../update.svelte';
import { providerParams, providerType, provider as wizardProvider } from '../wizard/store';
import { provider } from './store';
import { provider as providerData } from './store';
let enabled: boolean = null;
onMount(() => {
enabled ??= $provider.enabled;
enabled ??= $providerData.enabled;
});
function configure() {
if (!isValueOfStringEnum(MessagingProviderType, $provider.type)) {
throw new Error(`Invalid provider type: ${$provider.type}`);
}
if (!isValueOfStringEnum(Providers, $provider.provider)) {
throw new Error(`Invalid provider: ${$provider.provider}`);
}
$providerType = $provider.type;
$wizardProvider = $provider.provider;
switch ($wizardProvider) {
case Providers.Twilio:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
accountSid: $provider.credentials['accountSid'],
authToken: $provider.credentials['authToken'],
from: $provider.options['from']
};
break;
case Providers.Msg91:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
from: $provider.options['from'],
senderId: $provider.credentials['senderId'],
authKey: $provider.credentials['authKey']
};
break;
case Providers.Telesign:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
username: $provider.credentials['username'],
password: $provider.credentials['password'],
from: $provider.options['from']
};
break;
case Providers.Textmagic:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
username: $provider.credentials['username'],
apiKey: $provider.credentials['apiKey'],
from: $provider.options['from']
};
break;
case Providers.Vonage:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
apiKey: $provider.credentials['apiKey'],
apiSecret: $provider.credentials['apiSecret'],
from: $provider.options['from']
};
break;
case Providers.Mailgun:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
isEuRegion: false,
fromEmail: $provider.options['fromEmail'],
fromName: $provider.options['fromName'],
replyToEmail: $provider.options['replyToEmail'],
replyToName: $provider.options['replyToName'],
apiKey: $provider.credentials['apiKey'],
domain: $provider.credentials['domain']
};
break;
case Providers.Sendgrid:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
apiKey: $provider.credentials['apiKey'],
fromEmail: $provider.options['fromEmail'],
fromName: $provider.options['fromName'],
replyToEmail: $provider.options['replyToEmail'],
replyToName: $provider.options['replyToName']
};
break;
case Providers.SMTP:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
host: $provider.credentials['host'],
port: $provider.credentials['port'],
username: $provider.credentials['username'],
password: $provider.credentials['password'],
fromName: $provider.options['fromName'],
fromEmail: $provider.options['fromEmail'],
replyToName: $provider.options['replyToName'],
replyToEmail: $provider.options['replyToEmail'],
encryption: $provider.options['encryption'],
autoTLS: $provider.options['autoTLS'],
mailer: $provider.options['mailer']
};
break;
case Providers.FCM:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled
};
if ('serviceAccountJSON' in $provider.credentials) {
const serviceAccountJSON = $provider.credentials['serviceAccountJSON'];
if (typeof serviceAccountJSON === 'string') {
$providerParams[$wizardProvider].serviceAccountJSON = serviceAccountJSON;
} else if (serviceAccountJSON instanceof Object) {
$providerParams[$wizardProvider].serviceAccountJSON =
JSON.stringify(serviceAccountJSON);
}
}
break;
case Providers.APNS:
$providerParams[$wizardProvider] = {
providerId: $provider.$id,
name: $provider.name,
enabled: $provider.enabled,
authKey: $provider.credentials['authKey'],
authKeyId: $provider.credentials['authKeyId'],
teamId: $provider.credentials['teamId'],
bundleId: $provider.credentials['bundleId']
};
break;
}
wizard.start(Update);
}
async function updateStatus() {
try {
let response = { $id: '', name: '' };
const providerId = $provider.$id;
switch ($provider.provider) {
const providerId = $providerData.$id;
switch ($providerData.provider) {
case Providers.Twilio:
response = await sdk.forProject.messaging.updateTwilioProvider(
providerId,
@@ -237,14 +94,14 @@
);
break;
case Providers.FCM:
response = await sdk.forProject.messaging.updateFCMProvider(
response = await sdk.forProject.messaging.updateFcmProvider(
providerId,
undefined,
enabled
);
break;
case Providers.APNS:
response = await sdk.forProject.messaging.updateAPNSProvider(
response = await sdk.forProject.messaging.updateApnsProvider(
providerId,
undefined,
enabled
@@ -257,7 +114,7 @@
message: `${response.name} has been ${enabled ? 'enabled' : 'disabled'}`
});
trackEvent(Submit.MessagingProviderUpdate, {
provider: $provider
provider: $providerData
});
} catch (error) {
addNotification({
@@ -271,8 +128,8 @@
<CardGrid>
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
<Provider provider={$provider.provider} size="l">
<Heading tag="h6" size="7">{$provider.name}</Heading>
<Provider provider={$providerData.provider} size="l">
<Heading tag="h6" size="7">{$providerData.name}</Heading>
</Provider>
</div>
<svelte:fragment slot="aside">
@@ -284,17 +141,16 @@
label={enabled ? 'Enabled' : 'Disabled'}
bind:value={enabled} />
</ul>
<p class="title">Provider: <Provider noIcon provider={$provider.provider} /></p>
<p class="title">Channel: <ProviderType noIcon type={$provider.type} /></p>
<p>Created: {toLocaleDateTime($provider.$createdAt)}</p>
<p class="title">Provider: <Provider noIcon provider={$providerData.provider} /></p>
<p class="title">Channel: <ProviderType noIcon type={$providerData.type} /></p>
<p>Created: {toLocaleDateTime($providerData.$createdAt)}</p>
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
<div class="u-flex u-flex-wrap u-gap-12">
<Button secondary on:click={() => configure()}>Configure</Button>
<Button disabled={$provider.enabled === enabled} on:click={() => updateStatus()}
<Button disabled={$providerData.enabled === enabled} on:click={() => updateStatus()}
>Update</Button>
</div>
</svelte:fragment>
@@ -0,0 +1,141 @@
<script lang="ts">
import {
FormList,
InputDomain,
InputEmail,
InputFile,
InputSelect,
InputPhone,
InputSwitch,
InputText,
InputPassword
} from '$lib/elements/forms';
import InputCheckbox from '$lib/elements/forms/inputCheckbox.svelte';
import PopoverContent from './popoverContent.svelte';
import type {
ProviderInput,
TwilioProviderParams,
Msg91ProviderParams,
TelesignProviderParams,
TextmagicProviderParams,
VonageProviderParams,
MailgunProviderParams,
SendgridProviderParams,
SMTPProviderParams,
FCMProviderParams,
APNSProviderParams
} from './store';
export let files: Record<string, FileList>;
export let inputs: ProviderInput[];
export let params: Partial<
| TwilioProviderParams
| Msg91ProviderParams
| TelesignProviderParams
| TextmagicProviderParams
| VonageProviderParams
| MailgunProviderParams
| SendgridProviderParams
| SMTPProviderParams
| FCMProviderParams
| APNSProviderParams
>;
</script>
<FormList>
{#each inputs as input}
{@const popoverImage = input.popoverImage}
{#if input.type === 'text'}
<InputText
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputText>
{:else if input.type === 'password'}
<InputPassword
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
showPasswordButton
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPassword>
{:else if input.type === 'email'}
<InputEmail
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputEmail>
{:else if input.type === 'domain'}
<InputDomain
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputDomain>
{:else if input.type === 'phone'}
<InputPhone
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={params[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputPhone>
{:else if input.type === 'file'}
<InputFile
label={input.label}
allowedFileExtensions={input.allowedFileExtensions}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:files={files[input.name]}>
<svelte:fragment slot="popover">
<PopoverContent lines={input.popover} {popoverImage} />
</svelte:fragment>
</InputFile>
{:else if input.type === 'switch'}
<InputSwitch label={input.label} id={input.name} bind:value={params[input.name]}>
<svelte:fragment slot="description">
<p>{@html input.description}</p>
</svelte:fragment>
</InputSwitch>
{:else if input.type === 'checkbox'}
<InputCheckbox label={input.label} id={input.name} bind:checked={params[input.name]}>
<svelte:fragment slot="description">
{input.description}
</svelte:fragment>
</InputCheckbox>
{:else if input.type === 'select'}
<InputSelect
label={input.label}
id={input.name}
options={input.options}
required={!input.optional}
bind:value={params[input.name]} />
{/if}
{/each}
</FormList>
@@ -1,7 +1,7 @@
import type { Column } from '$lib/helpers/types';
import { writable } from 'svelte/store';
import { Providers } from '../provider.svelte';
import { MessagingProviderType, Encryption } from '@appwrite.io/console';
import { MessagingProviderType, SmtpEncryption } from '@appwrite.io/console';
export const columns = writable<Column[]>([
{ id: '$id', title: 'Provider ID', type: 'string', show: true },
@@ -11,6 +11,28 @@ export const columns = writable<Column[]>([
{ id: 'enabled', title: 'Status', type: 'boolean', show: true }
]);
export type ProviderInput = {
label: string;
name: string;
type:
| 'text'
| 'password'
| 'phone'
| 'email'
| 'domain'
| 'file'
| 'switch'
| 'select'
| 'checkbox';
placeholder?: string;
description?: string;
popover?: string[];
popoverImage?: { src: { light: string; dark: string }; alt: string };
allowedFileExtensions?: string[];
optional?: boolean;
options?: { label: string; value: string | number | boolean }[];
};
type ProvidersMap = {
[key in MessagingProviderType]: {
name: string;
@@ -23,25 +45,7 @@ type ProvidersMap = {
title: string;
description: string;
needAHand?: string[];
configure: {
label: string;
name: string;
type:
| 'text'
| 'password'
| 'phone'
| 'email'
| 'domain'
| 'file'
| 'switch'
| 'select';
placeholder?: string;
description?: string;
popover?: string[];
allowedFileExtensions?: string[];
optional?: boolean;
options?: { label: string; value: string | number | boolean }[];
}[];
configure: ProviderInput[];
};
};
};
@@ -73,7 +77,14 @@ export const providers: ProvidersMap = {
'<b>How to get the <a class="link" href="https://firebase.google.com" target="_blank" rel="noopener noreferrer">FCM</a> service account JSON?</b>',
'Head to <b>Project settings -> Service accounts -> Generate new private key.</b>',
'Generating the new key will result in the download of a JSON file.'
]
],
popoverImage: {
src: {
light: '/images/messaging/fcm-service-account-json-light.png',
dark: '/images/messaging/fcm-service-account-json-dark.png'
},
alt: 'Screenshot of how to generate the Firebase Service Account JSON file'
}
}
]
},
@@ -89,8 +100,15 @@ export const providers: ProvidersMap = {
placeholder: 'Enter team ID',
popover: [
'<b>How to get the team ID?</b>',
'Head to <b><a class="link" href="https://idmsa.apple.com/IDMSWebAuth/signin?appIdKey=891bd3417a7776362562d2197f89480a8547b108fd934911bcbea0110d07f757&path=%2Faccount%2F&rv=1" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Membership details -> Team ID.</b>'
]
'Head to <b><a style="link" href="https://developer.apple.com/account" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Membership details -> Team ID.</b>'
],
popoverImage: {
src: {
light: '/images/messaging/apns-team-id.png',
dark: '/images/messaging/apns-team-id.png'
},
alt: 'Screenshot of where to find the Team ID in the Apple Developer Member Center'
}
},
{
label: 'Bundle ID',
@@ -99,27 +117,15 @@ export const providers: ProvidersMap = {
placeholder: 'Enter bundle ID',
popover: [
'<b>How to get the bundle ID?</b>',
'Head to <b><a class="link" href="https://idmsa.apple.com/IDMSWebAuth/signin?appIdKey=891bd3417a7776362562d2197f89480a8547b108fd934911bcbea0110d07f757&path=%2Faccount%2F&rv=1" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Certificates, Identifiers & Profiles -> Identifiers.</b>',
`<a
href="/images/apns-bundle-id.png"
class="file-preview is-with-image"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer"
aria-label="open file in new window">
<div class="file-preview-image">
<img
width="205"
height="125"
src="/images/apns-bundle-id.png"
alt="Screenshot of Bundle ID in Apple" />
</div>
<div class="file-preview-content">
<div class="avatar">
<span class="icon-external-link" aria-hidden="true" />
</div>
</div>
</a>`
]
'Head to <b><a class="link" href="https://developer.apple.com/account" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Certificates, Identifiers & Profiles -> Identifiers.</b>'
],
popoverImage: {
src: {
light: '/images/messaging/apns-bundle-id.png',
dark: '/images/messaging/apns-bundle-id.png'
},
alt: 'Screenshot of where to find the Bundle ID in the Apple Developer Member Center'
}
},
{
label: 'Authentication key ID',
@@ -128,9 +134,16 @@ export const providers: ProvidersMap = {
placeholder: 'Enter key ID',
popover: [
'<b>How to get the auth key ID?</b>',
'Head to <b><a class="link" href="https://idmsa.apple.com/IDMSWebAuth/signin?appIdKey=891bd3417a7776362562d2197f89480a8547b108fd934911bcbea0110d07f757&path=%2Faccount%2F&rv=1" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Certificates, Identifiers & Profiles -> Keys.</b>',
'Head to <b><a class="link" href="https://developer.apple.com/account" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a> -> Certificates, Identifiers & Profiles -> Keys.</b>',
'Click on your key to view details.'
]
],
popoverImage: {
src: {
light: '/images/messaging/apns-authentication-key-id.png',
dark: '/images/messaging/apns-authentication-key-id.png'
},
alt: 'Screenshot of how to find the Authentication Key ID in the Apple Developer Member Center'
}
},
{
label: 'Auth key (.p8 file)',
@@ -139,9 +152,23 @@ export const providers: ProvidersMap = {
allowedFileExtensions: ['p8'],
popover: [
'<b>How to get the authentication key?</b>',
'Head to <b><a class="link" href="https://idmsa.apple.com/IDMSWebAuth/signin?appIdKey=891bd3417a7776362562d2197f89480a8547b108fd934911bcbea0110d07f757&path=%2Faccount%2F&rv=1" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a></b> (under Program resources) <b>-> Certificates, Identifiers & Profiles -> Keys.</b>',
'Head to <b><a class="link" href="https://developer.apple.com/account" target="_blank" rel="noopener noreferrer">Apple Developer Member Center</a></b> (under Program resources) <b>-> Certificates, Identifiers & Profiles -> Keys.</b>',
'Create a key and give it a name. Enable the Apple Push Notifications service (APNS), and register your key.'
]
],
popoverImage: {
src: {
light: '/images/messaging/apns-auth-key.png',
dark: '/images/messaging/apns-auth-key.png'
},
alt: 'Screenshot of where to download the Authentication Key in the Apple Developer Member Center'
}
},
{
label: 'Sandbox',
name: 'sandbox',
type: 'switch',
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>.'
}
]
}
@@ -300,7 +327,7 @@ export const providers: ProvidersMap = {
label: 'Server host',
name: 'host',
type: 'text',
placeholder: 'Enter host'
placeholder: 'smtp.server.com'
},
{
label: 'Server port',
@@ -328,16 +355,16 @@ export const providers: ProvidersMap = {
name: 'encryption',
type: 'select',
options: [
{ label: 'None', value: Encryption.None },
{ label: 'SSL', value: Encryption.Ssl },
{ label: 'TLS', value: Encryption.Tls }
{ label: 'None', value: SmtpEncryption.None },
{ label: 'SSL', value: SmtpEncryption.Ssl },
{ label: 'TLS', value: SmtpEncryption.Tls }
]
},
{
label: 'Auto TLS',
name: 'autoTLS',
description: 'Automatically uses TLS encryption',
type: 'switch',
description: 'Automatically uses TLS encryption if available.',
type: 'checkbox',
optional: true
},
{
@@ -345,7 +372,11 @@ export const providers: ProvidersMap = {
name: 'mailer',
type: 'text',
optional: true,
placeholder: 'Enter mailer'
placeholder: 'Enter mailer',
popover: [
'<b>What is the Mailer?</b>',
'The value to use for the X-Mailer header.'
]
}
]
}
@@ -439,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',
@@ -525,3 +561,96 @@ export const providers: ProvidersMap = {
}
}
};
type ProviderParams = {
providerId: string;
name: string;
enabled: boolean;
};
/**
* SMS providers
*/
export type TwilioProviderParams = ProviderParams & {
accountSid: string;
authToken: string;
from: string;
};
export type Msg91ProviderParams = ProviderParams & {
from: string;
senderId: string;
authKey: string;
};
export type TelesignProviderParams = ProviderParams & {
from: string;
customerId: string;
apiKey: string;
};
export type TextmagicProviderParams = ProviderParams & {
from: string;
username: string;
apiKey: string;
};
export type VonageProviderParams = ProviderParams & {
from: string;
apiKey: string;
apiSecret: string;
};
/**
* Email providers
*/
export type MailgunProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
isEuRegion: boolean;
apiKey: string;
domain: string;
};
export type SendgridProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
apiKey: string;
};
export type SMTPProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
host: string;
port: number;
username: string;
password: string;
encryption: SmtpEncryption;
autoTLS: boolean;
mailer: string;
};
/**
* Push providers
*/
export type FCMProviderParams = ProviderParams & {
serviceAccountJSON: string;
};
export type APNSProviderParams = ProviderParams & {
authKey: string;
authKeyId: string;
teamId: string;
bundleId: string;
sandbox: boolean;
};
export type MQTTProviderParams = ProviderParams & {
serverKey: string;
};
@@ -1,164 +0,0 @@
<script lang="ts">
import { Wizard } from '$lib/layout';
import type { WizardStepsType } from '$lib/layout/wizard.svelte';
import Configure from './wizard/configure.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 { provider, providerParams } from './wizard/store';
import { Providers } from '../provider.svelte';
import { Dependencies } from '$lib/constants';
import type { Models } from '@appwrite.io/console';
async function update() {
try {
let response: Models.Provider;
const providerId = $providerParams[$provider].providerId;
switch ($provider) {
case Providers.Twilio:
response = await sdk.forProject.messaging.updateTwilioProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].accountSid,
$providerParams[$provider].authToken,
$providerParams[$provider].from
);
break;
case Providers.Msg91:
response = await sdk.forProject.messaging.updateMsg91Provider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].senderId,
$providerParams[$provider].authKey,
$providerParams[$provider].from
);
break;
case Providers.Telesign:
response = await sdk.forProject.messaging.updateTelesignProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].username,
$providerParams[$provider].password,
$providerParams[$provider].from
);
break;
case Providers.Textmagic:
response = await sdk.forProject.messaging.updateTextmagicProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].username,
$providerParams[$provider].apiKey,
$providerParams[$provider].from
);
break;
case Providers.Vonage:
response = await sdk.forProject.messaging.updateVonageProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].apiKey,
$providerParams[$provider].apiSecret,
$providerParams[$provider].from
);
break;
case Providers.Mailgun:
response = await sdk.forProject.messaging.updateMailgunProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].apiKey,
$providerParams[$provider].domain,
$providerParams[$provider].isEuRegion,
$providerParams[$provider].enabled,
$providerParams[$provider].fromName || undefined,
$providerParams[$provider].fromEmail,
$providerParams[$provider].replyToName || undefined,
$providerParams[$provider].replyToEmail || undefined
);
break;
case Providers.Sendgrid:
response = await sdk.forProject.messaging.updateSendgridProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].apiKey,
$providerParams[$provider].fromName || undefined,
$providerParams[$provider].fromEmail,
$providerParams[$provider].replyToName || undefined,
$providerParams[$provider].replyToEmail || undefined
);
break;
case Providers.SMTP:
response = await sdk.forProject.messaging.updateSmtpProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].host,
$providerParams[$provider].port || undefined,
$providerParams[$provider].username || undefined,
$providerParams[$provider].password || undefined,
$providerParams[$provider].encryption,
$providerParams[$provider].autoTLS,
$providerParams[$provider].mailer || undefined,
$providerParams[$provider].fromName || undefined,
$providerParams[$provider].fromEmail,
$providerParams[$provider].replyToName || undefined,
$providerParams[$provider].replyToEmail || undefined,
$providerParams[$provider].enabled
);
break;
case Providers.FCM:
response = await sdk.forProject.messaging.updateFCMProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
JSON.parse($providerParams[$provider].serviceAccountJSON)
);
break;
case Providers.APNS:
response = await sdk.forProject.messaging.updateAPNSProvider(
providerId,
$providerParams[$provider].name,
$providerParams[$provider].enabled,
$providerParams[$provider].authKey,
$providerParams[$provider].authKeyId,
$providerParams[$provider].teamId,
$providerParams[$provider].bundleId
);
break;
}
await invalidate(Dependencies.MESSAGING_PROVIDER);
wizard.hide();
addNotification({
type: 'success',
message: `${response.name} has been updated`
});
trackEvent(Submit.MessagingProviderUpdate, {
provider: $provider
});
await goto(
`${base}/console/project-${$project.$id}/messaging/providers/provider-${response.$id}`
);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.MessagingProviderUpdate);
}
}
const stepsComponents: WizardStepsType = new Map();
stepsComponents.set(1, {
label: 'Configure',
component: Configure
});
</script>
<Wizard title="Update provider" steps={stepsComponents} on:finish={update} finalAction="Update" />
@@ -1,244 +0,0 @@
<script lang="ts">
import {
FormList,
InputDomain,
InputEmail,
InputFile,
InputSelect,
InputSwitch,
InputText,
InputPassword
} from '$lib/elements/forms';
import InputPhone from '$lib/elements/forms/inputPhone.svelte';
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { providers } from '../store';
import { providerType, provider, providerParams } from './store';
import { Table, TableBody, TableCell, TableRowButton, TableRowLink } from '$lib/elements/table';
import { newMemberModal } from '$lib/stores/organization';
import CreateMember from '$routes/console/organization-[organization]/createMember.svelte';
import Collapsible from '$lib/components/collapsible.svelte';
import CollapsibleItem from '$lib/components/collapsibleItem.svelte';
import Provider from '../../provider.svelte';
import { MessagingProviderType } from '@appwrite.io/console';
import ProviderTypeComponent from '$routes/console/project-[project]/messaging/providerType.svelte';
let files: Record<string, FileList> = {};
const inputs = providers[$providerType].providers[$provider].configure;
onMount(() => {
for (const input of inputs) {
if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) {
const dataTransfer = new DataTransfer();
const f = new File(
[$providerParams[$provider][input.name]],
`${input.name}.${input.allowedFileExtensions}`
);
dataTransfer.items.add(f);
files[input.name] = dataTransfer.files;
}
}
});
async function beforeSubmit() {
const promises = [];
for (const [key, value] of Object.entries(files)) {
const promise = value[0].text().then((text) => {
$providerParams[$provider][key] = text;
});
promises.push(promise);
}
await Promise.all(promises);
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Configure</svelte:fragment>
<svelte:fragment slot="subtitle">
Set up the credentials below to enable {providers[$providerType].providers[$provider].title}
for sending
{providers[$providerType].text}.
</svelte:fragment>
<FormList>
{#each inputs as input}
{#if input.type === 'text'}
<InputText
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="popover">
{@html input.popover?.join('<br/><br/>')}
</svelte:fragment>
</InputText>
{:else if input.type === 'password'}
<InputPassword
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
showPasswordButton
isPopoverDefined={input.popover !== undefined}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="popover">
{@html input.popover?.join('<br/><br/>')}
</svelte:fragment>
</InputPassword>
{:else if input.type === 'email'}
<InputEmail
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="popover">
<p class="body-text-2 u-margin-block-end-16">
{@html input.popover?.join('<br/><br/>')}
</p>
</svelte:fragment>
</InputEmail>
{:else if input.type === 'domain'}
<InputDomain
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="popover">
<p class="body-text-2 u-margin-block-end-16">
{@html input.popover?.join('<br/><br/>')}
</p>
</svelte:fragment>
</InputDomain>
{:else if input.type === 'phone'}
<InputPhone
id={input.name}
label={input.label}
placeholder={input.placeholder}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="popover">
<p class="body-text-2 u-margin-block-end-16">
{@html input.popover?.join('<br/><br/>')}
</p>
</svelte:fragment>
</InputPhone>
{:else if input.type === 'file'}
<InputFile
label={input.label}
allowedFileExtensions={input.allowedFileExtensions}
required={!input.optional}
isPopoverDefined={input.popover !== undefined}
bind:files={files[input.name]}>
<svelte:fragment slot="popover">
<p class="body-text-2 u-margin-block-end-16">
{@html input.popover?.join('<br/><br/>')}
</p>
</svelte:fragment>
</InputFile>
{:else if input.type === 'switch'}
<InputSwitch
label={input.label}
id={input.name}
bind:value={$providerParams[$provider][input.name]}>
<svelte:fragment slot="description">
{input.description}
</svelte:fragment>
</InputSwitch>
{:else if input.type === 'select'}
<InputSelect
label={input.label}
id={input.name}
options={input.options}
required={!input.optional}
bind:value={$providerParams[$provider][input.name]} />
{/if}
{/each}
</FormList>
<div class="need-a-hand u-flex-vertical u-gap-8">
<p class="body-text-2 u-bold u-margin-block-start-48">Need a hand?</p>
{#if providers[$providerType].providers[$provider].needAHand}
{@const needAHand = providers[$providerType].providers[$provider].needAHand}
<div class="box u-padding-inline-8 u-padding-0 u-border-width-0">
<Collapsible>
<CollapsibleItem withIndentation>
<svelte:fragment slot="beforetitle">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-info"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
<span class="body-text-2 u-small u-bold"
>How to enable <Provider provider={$provider} noIcon />
{#if $providerType == MessagingProviderType.Push}notifications{:else}<ProviderTypeComponent
type={$providerType}
noIcon />{/if} service?</span>
</div>
</svelte:fragment>
<div class="u-flex-vertical u-gap-16">
{#each needAHand as p}
<p>
{@html p}
</p>
{/each}
</div>
</CollapsibleItem>
</Collapsible>
</div>
{/if}
<Table noMargin noStyles>
<TableBody>
<TableRowLink href={`https://appwrite.io/docs/messaging/${$provider}`}>
<TableCell>
<div class="u-flex u-cross-center u-main-space-between u-padding-8">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-book-open"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
Read the full guide in the documentation
</div>
<span class="icon-arrow-right" aria-hidden="true" />
</div>
</TableCell>
</TableRowLink>
<TableRowButton on:click={() => ($newMemberModal = true)}>
<TableCell>
<div class="u-flex u-cross-center u-main-space-between u-padding-8">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-user-group"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
Invite a team member to complete this step
</div>
<span class="icon-arrow-right" aria-hidden="true" />
</div>
</TableCell>
</TableRowButton>
</TableBody>
</Table>
</div>
</WizardStep>
<CreateMember bind:showCreate={$newMemberModal} />
<style lang="scss">
.need-a-hand :global(.table-row):last-child:last-child {
border-block-end: none;
}
</style>
@@ -6,15 +6,13 @@
import { FormList, InputText } from '$lib/elements/forms';
import { Pill } from '$lib/elements';
import { Providers } from '../../provider.svelte';
import { Encryption } from '@appwrite.io/console';
import { SmtpEncryption } from '@appwrite.io/console';
let name = '';
let showCustomId = false;
let id: string = null;
async function beforeSubmit() {
console.log($provider);
switch ($provider) {
case Providers.Twilio:
$providerParams[$provider] = {
@@ -41,8 +39,8 @@
providerId: id,
name: name,
enabled: true,
username: '',
password: '',
customerId: '',
apiKey: '',
from: ''
};
break;
@@ -102,7 +100,7 @@
username: '',
password: '',
autoTLS: true,
encryption: Encryption.Tls,
encryption: SmtpEncryption.Tls,
mailer: '',
fromEmail: '',
fromName: '',
@@ -126,7 +124,8 @@
authKey: '',
authKeyId: '',
teamId: '',
bundleId: ''
bundleId: '',
sandbox: false
};
break;
}
@@ -0,0 +1,174 @@
<script lang="ts">
import { WizardStep } from '$lib/layout';
import { onMount } from 'svelte';
import { providers } from '../store';
import { providerType, provider, providerParams } from './store';
import SettingsFormList from '../settingsFormList.svelte';
import { newMemberModal } from '$lib/stores/organization';
import CreateMember from '$routes/console/organization-[organization]/createMember.svelte';
import Provider from '../../provider.svelte';
import { MessagingProviderType } from '@appwrite.io/console';
import ProviderTypeComponent from '$routes/console/project-[project]/messaging/providerType.svelte';
import {
Collapsible,
CollapsibleItem,
ClickableList,
ClickableListItem
} from '$lib/components';
let files: Record<string, FileList> = {};
const inputs = providers[$providerType].providers[$provider].configure;
let open = false;
onMount(() => {
for (const input of inputs) {
if (input.type === 'file' && $providerParams[$provider][input.name].length > 0) {
const dataTransfer = new DataTransfer();
const f = new File(
[$providerParams[$provider][input.name]],
`${input.name}.${input.allowedFileExtensions}`
);
dataTransfer.items.add(f);
files[input.name] = dataTransfer.files;
}
}
});
async function beforeSubmit() {
const promises = [];
for (const [key, value] of Object.entries(files)) {
const promise = value[0].text().then((text) => {
$providerParams[$provider][key] = text;
});
promises.push(promise);
}
await Promise.all(promises);
}
</script>
<WizardStep {beforeSubmit}>
<svelte:fragment slot="title">Settings</svelte:fragment>
<svelte:fragment slot="subtitle">
Set up the credentials below to enable {providers[$providerType].providers[$provider].title}
for sending
{providers[$providerType].text}.
</svelte:fragment>
<SettingsFormList bind:files {inputs} bind:params={$providerParams[$provider]} />
<div class="need-a-hand u-flex-vertical u-gap-8">
<p class="body-text-2 u-bold u-margin-block-start-48">Need a hand?</p>
<div>
{#if providers[$providerType].providers[$provider].needAHand}
{@const needAHand = providers[$providerType].providers[$provider].needAHand}
<div
class="how-to-enable u-padding-inline-8 u-padding-0"
style={!open ? '' : 'background-color: hsl(var(--p-bg-color-hover));'}>
<Collapsible>
<CollapsibleItem withIndentation bind:open>
<svelte:fragment slot="beforetitle">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-info"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
<span class="title body-text-2 u-small" class:u-bold={open}
>How to enable <Provider provider={$provider} noIcon />
{#if $providerType == MessagingProviderType.Push}notifications{:else}<ProviderTypeComponent
type={$providerType}
noIcon />{/if} service?</span>
</div>
</svelte:fragment>
<div class="u-flex-vertical u-gap-16">
{#each needAHand as p}
<p>
{@html p}
</p>
{/each}
</div>
</CollapsibleItem>
</Collapsible>
</div>
<div class="u-margin-block-start-4 u-sep-block-end" />
{/if}
<ClickableList>
<ClickableListItem
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">
<div class="avatar is-size-small">
<span
class="icon-book-open"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
<p>Read the full guide in the documentation</p>
</div>
<span class="icon-arrow-sm-right u-font-size-20" aria-hidden="true" />
</div>
</ClickableListItem>
<ClickableListItem on:click={() => ($newMemberModal = true)}>
<div class="u-flex u-cross-center u-main-space-between">
<div class="u-flex u-cross-center u-gap-16">
<div class="avatar is-size-small">
<span
class="icon-user-group"
style:--p-text-size="1.25rem"
aria-hidden="true" />
</div>
<p>Invite a team member to complete this step</p>
</div>
<span class="icon-arrow-sm-right u-font-size-20" aria-hidden="true" />
</div>
</ClickableListItem>
</ClickableList>
</div>
</div>
</WizardStep>
<CreateMember bind:showCreate={$newMemberModal} />
<style lang="scss">
.need-a-hand {
--p-bg-color-hover: var(--color-neutral-5);
.how-to-enable {
border-radius: var(--border-radius-small);
&:hover,
&:focus {
background-color: hsl(var(--p-bg-color-hover));
.title {
font-weight: 600;
}
}
}
:global(.clickable-list) {
--color-border: var(--color-neutral-5);
--p-clickable-button-bg-color-hover: var(--color-border);
:global(.theme-dark) & {
--color-border: var(--color-neutral-85);
}
}
:global(.clickable-list-button) {
padding-inline: 0.5rem;
}
:global(.theme-dark) & {
--p-bg-color-hover: var(--color-neutral-85);
}
}
:global(.clickable-list-button):hover p,
:global(.clickable-list-button):focus p {
font-weight: 600;
}
</style>
@@ -1,101 +1,18 @@
import type { MessagingProviderType } from '@appwrite.io/console';
import { writable } from 'svelte/store';
import type { Providers } from '../../provider.svelte';
import type { MessagingProviderType, Encryption } from '@appwrite.io/console';
type ProviderParams = {
providerId: string;
name: string;
enabled: boolean;
};
/**
* SMS providers
*/
export type TwilioProviderParams = ProviderParams & {
accountSid: string;
authToken: string;
from: string;
};
export type Msg91ProviderParams = ProviderParams & {
from: string;
senderId: string;
authKey: string;
};
export type TelesignProviderParams = ProviderParams & {
from: string;
username: string;
password: string;
};
export type TextmagicProviderParams = ProviderParams & {
from: string;
username: string;
apiKey: string;
};
export type VonageProviderParams = ProviderParams & {
from: string;
apiKey: string;
apiSecret: string;
};
/**
* Email providers
*/
export type MailgunProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
isEuRegion: boolean;
apiKey: string;
domain: string;
};
export type SendgridProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
apiKey: string;
};
export type SMTPProviderParams = ProviderParams & {
fromEmail: string;
fromName: string;
replyToEmail: string;
replyToName: string;
host: string;
port: number;
username: string;
password: string;
encryption: Encryption;
autoTLS: boolean;
mailer: string;
};
/**
* Push providers
*/
export type FCMProviderParams = ProviderParams & {
serviceAccountJSON: string;
};
export type APNSProviderParams = ProviderParams & {
authKey: string;
authKeyId: string;
teamId: string;
bundleId: string;
};
export type MQTTProviderParams = ProviderParams & {
serverKey: string;
};
import type {
APNSProviderParams,
FCMProviderParams,
MailgunProviderParams,
Msg91ProviderParams,
SMTPProviderParams,
SendgridProviderParams,
TelesignProviderParams,
TextmagicProviderParams,
TwilioProviderParams,
VonageProviderParams
} from '../store';
export const providerType = writable<MessagingProviderType>(null);
export const provider = writable<Providers>(null);
@@ -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>
@@ -46,9 +46,14 @@
const targetIds = Object.keys($targetsById).filter(
(targetId) => !(targetId in subscribersByTargetId)
);
const promises = targetIds.map((targetId) =>
sdk.forProject.messaging.createSubscriber($page.params.topic, ID.unique(), targetId)
);
const promises = targetIds.map(async (targetId) => {
const subscriber = await sdk.forProject.messaging.createSubscriber(
$page.params.topic,
ID.unique(),
targetId
);
subscribersByTargetId[targetId] = subscriber;
});
try {
await Promise.all(promises);
@@ -141,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>
@@ -154,10 +159,11 @@
<svelte:fragment slot="description">
<p class="text">
Add subscribers to this topic by selecting the targets for directing messages. <a
href="https://appwrite.io/docs/products/messaging/topics"
href="https://appwrite.io/docs/products/messaging/topics#subscribe-targets-to-topics"
target="_blank"
rel="noopener noreferrer"
class="link">Learn more about subscribers.</a>
class="link">Learn more about subscribers</a
>.
</p>
</svelte:fragment>
</UserTargetsModal>
@@ -0,0 +1,65 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { Roles } from '$lib/components/permissions';
import { symmetricDifference } from '$lib/helpers/array';
import { topic } from './store';
const topicId = $page.params.topic;
let arePermsDisabled = true;
let permissions: string[] = [];
onMount(async () => {
permissions = $topic.subscribe || [];
});
async function updatePermissions() {
try {
await sdk.forProject.messaging.updateTopic(topicId, undefined, permissions);
await invalidate(Dependencies.MESSAGING_TOPIC);
addNotification({
message: 'Permissions have been updated',
type: 'success'
});
trackEvent(Submit.MessagingTopicUpdatePermissions);
} catch (error) {
addNotification({
message: error.message,
type: 'error'
});
trackError(error, Submit.MessagingTopicUpdatePermissions);
}
}
$: arePermsDisabled = symmetricDifference(permissions, $topic.subscribe).length == 0;
</script>
<Form onSubmit={updatePermissions}>
<CardGrid>
<Heading tag="h6" size="7" id="permissions">Subscription access</Heading>
<p>
Choose who can subscribe to this topic using the client API. Learn more about <a
href="https://appwrite.io/docs/advanced/platform/permissions"
target="_blank"
rel="noopener noreferrer"
class="link">
Permissions</a
>.
</p>
<svelte:fragment slot="aside">
<Roles bind:roles={permissions} />
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={arePermsDisabled} submit>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -91,86 +91,112 @@
</script>
<Modal {title} bind:show onSubmit={submit} on:close={reset} headerDivider={false} size="big">
<p class="text">
Select existing topics you want to send this message to its targets. The message will be
sent only to <ProviderType type={providerType} noIcon /> targets.
</p>
<InputSearch
autofocus
disabled={totalResults === 0 && !search}
placeholder="Search for topics"
bind:value={search} />
{#if Object.keys(topicResultsById).length > 0}
<FormList>
<Table noMargin noStyles>
<TableBody>
{#each Object.entries(topicResultsById) as [topicId, topic]}
<TableRow>
<TableCell>
<InputCheckbox
id={topicId}
disabled={!!topicsById[topicId]}
checked={!!selected[topicId]}
on:change={(event) => onTopicSelection(event, topic)}>
<svelte:fragment slot="description">
<span class="title">
<span class="u-line-height-1-5">
<span class="body-text-1 u-bold" data-private>
{topic.name}
</span>
<span
class="collapsible-button-optional"
style="--p-toggle-optional-color: var(--color-neutral-50);">
({getTotal(topic)} subscribers)
</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>
@@ -12,7 +12,7 @@
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 { ID, MessagingProviderType, type Models } from '@appwrite.io/console';
import { Dependencies } from '$lib/constants';
async function create() {
@@ -32,7 +32,7 @@
undefined,
undefined,
undefined,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].html,
$messageParams[$providerType].scheduledAt
);
@@ -44,7 +44,7 @@
$messageParams[$providerType].topics,
$messageParams[$providerType].users,
$messageParams[$providerType].targets,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
break;
@@ -74,7 +74,7 @@
undefined,
undefined,
undefined,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
}
@@ -83,14 +83,13 @@
wizard.hide();
let message = '';
switch (response.status) {
case MessageStatus.Draft:
case 'draft':
message = 'The message has been saved as draft.';
break;
case MessageStatus.Processing:
case 'processing':
message = 'The message is queued for processing.';
break;
case 'scheduled':
// TODO: fix message status
message = 'The message has been scheduled.';
break;
}
@@ -131,7 +130,7 @@
$messageParams[$providerType].targets,
$messageParams[$providerType].subject,
$messageParams[$providerType].content,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].html,
undefined,
undefined,
@@ -145,7 +144,7 @@
$messageParams[$providerType].users,
$messageParams[$providerType].targets,
$messageParams[$providerType].content,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
break;
@@ -175,7 +174,7 @@
undefined,
undefined,
undefined,
$messageParams[$providerType].status,
$messageParams[$providerType].draft,
$messageParams[$providerType].scheduledAt
);
}
@@ -184,14 +183,13 @@
wizard.hide();
let message = '';
switch (response.status) {
case MessageStatus.Draft:
case 'draft':
message = 'The message has been saved as draft.';
break;
case MessageStatus.Processing:
case 'processing':
message = 'The message is queued for processing.';
break;
case 'scheduled':
// TODO: fix message status
message = 'The message has been scheduled.';
break;
}
@@ -202,7 +200,7 @@
await invalidate(Dependencies.MESSAGING_MESSAGE);
trackEvent(Submit.MessagingMessageUpdate, {
providerType: $providerType,
status: params.status
status: response.status
});
await goto(`${base}/console/project-${$project.$id}/messaging/message-${response.$id}`);
} catch (error) {
@@ -215,7 +213,7 @@
}
async function saveDraft() {
$messageParams[$providerType].status = MessageStatus.Draft;
$messageParams[$providerType].draft = true;
if ($operation === 'create') {
create();
} else {
@@ -13,7 +13,7 @@
import { CustomId, 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
);
@@ -41,7 +41,7 @@
import { CustomId, Modal } from '$lib/components';
import { user } from '$lib/stores/user';
import { clickOnEnter } from '$lib/helpers/a11y';
import { ID, MessageStatus, MessagingProviderType } from '@appwrite.io/console';
import { ID, MessagingProviderType } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import PushPhone from '../pushPhone.svelte';
import { onMount } from 'svelte';
@@ -76,7 +76,7 @@
undefined,
undefined,
undefined,
MessageStatus.Processing,
undefined,
undefined
);
}
@@ -5,7 +5,7 @@
import { CustomId, 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
);
}
@@ -102,6 +102,6 @@
</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,7 +2,7 @@
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';
let when: 'now' | 'later' = 'now';
@@ -41,9 +41,7 @@
};
async function beforeSubmit() {
$messageParams[$providerType].status = MessageStatus.Processing;
if (when === 'later') {
$messageParams[$providerType].status = 'scheduled' as MessageStatus;
$messageParams[$providerType].scheduledAt = dateTime.toISOString();
}
}
@@ -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;
};
@@ -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">
@@ -1,19 +1,13 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Dependencies } from '$lib/constants';
import {
FormList,
InputNumber,
InputPassword,
InputSelect,
InputText,
InputTextarea
} from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import type { Provider } from '$lib/stores/migration';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { provider } from '.';
const providers: Record<Provider, string> = {
@@ -22,38 +16,10 @@
supabase: 'Supabase',
nhost: 'NHost'
};
// function connectGoogle() {
// const redirect = new URL($page.url);
// const target = new URL(
// `${sdk.forProject.client.config.endpoint}/migrations/firebase/connect`
// );
// target.searchParams.set('redirect', redirect.toString());
// target.searchParams.set('project', $page.params.project);
// target.searchParams.set('mode', 'admin');
// return target;
// }
function deauthorizeGoogle() {
sdk.forProject.migrations.deleteFirebaseAuth().then(() => {
firebaseProjects = [];
});
addNotification({
type: 'success',
message: 'Signed out of Google successfully'
});
invalidate(Dependencies.MIGRATIONS);
}
$: firebaseProjects = $page.data.firebaseProjects;
// let showAuth = false;
</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"
@@ -102,24 +68,7 @@
bind:value={$provider.apiKey} />
</FormList>
{:else if $provider.provider === 'firebase'}
{#if !firebaseProjects?.length}
<!-- <div class="box u-flex u-flex-vertical u-gap-16 u-cross-center u-margin-block-start-24">
<p class="u-text-center u-bold">Sign in with Google to get started</p>
<Button secondary href={connectGoogle().toString()}>
<SvgIcon name="google" />Sign in
</Button>
</div> -->
<!-- <button
class="tag u-margin-block-start-16"
type="button"
on:click={() => (showAuth = !showAuth)}
class:is-selected={showAuth}>
<span class="icon-lock-closed" aria-hidden="true" />
<span class="text">Manual authentication</span>
</button> -->
<!-- {#if showAuth} -->
<FormList class="u-margin-block-start-24">
<div class="u-margin-block-start-16">
<InputTextarea
id="credentials"
@@ -128,23 +77,7 @@
bind:value={$provider.serviceAccount}
placeholder="Enter account credentials" />
</div>
<!-- {/if} -->
{:else}
<FormList class="u-margin-block-start-24">
<InputSelect
id="firebase-project"
label="Firebase project"
required
bind:value={$provider.projectId}
options={firebaseProjects.map((project) => ({
label: project.displayName,
value: project.projectId
}))} />
</FormList>
<p class="u-text-center u-margin-block-start-24">
<button class="u-bold" on:click|preventDefault={deauthorizeGoogle}>Sign out</button>
</p>
{/if}
</FormList>
{:else if $provider.provider === 'supabase'}
<FormList class="u-margin-block-start-24">
<p class="body-text-1 u-bold">Postgres credentials</p>
@@ -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
@@ -49,19 +49,10 @@
break;
}
case 'firebase': {
if ($provider.projectId) {
// OAuth
await sdk.forProject.migrations.createFirebaseOAuthMigration(
resources,
$provider.projectId
);
} else if ($provider.serviceAccount) {
// Manual auth
await sdk.forProject.migrations.createFirebaseMigration(
resources,
$provider.serviceAccount
);
}
await sdk.forProject.migrations.createFirebaseMigration(
resources,
$provider.serviceAccount
);
invalidate(Dependencies.MIGRATIONS);
break;
}
@@ -1,19 +1,6 @@
import { Dependencies } from '$lib/constants.js';
import { sdk } from '$lib/stores/sdk';
async function getFirebaseProjects() {
try {
const res = await sdk.forProject.migrations.listFirebaseProjects();
return res.projects;
} catch (e) {
if (e.type === 'user_identity_not_found') {
return [];
} else {
throw e;
}
}
}
export async function load({ depends }) {
depends(Dependencies.MIGRATIONS);
@@ -21,8 +8,7 @@ export async function load({ depends }) {
const { migrations } = await sdk.forProject.migrations.list();
return {
migrations,
firebaseProjects: getFirebaseProjects()
migrations
};
} catch {
return {
@@ -6,6 +6,7 @@
import { parseIfString } from '$lib/helpers/object';
import { formatNum } from '$lib/helpers/string';
import type { Models } from '@appwrite.io/console';
import { ResourcesFriendly } from '$lib/stores/migration';
export let migrations: Models.Migration[] = [];
export let migrationId: string = null;
@@ -106,7 +107,10 @@
</div>
<div>
<span class="u-capitalize">{entity + 's'}</span>
<span class="u-capitalize"
>{total(Object.values(entityCounter)) > 1
? ResourcesFriendly[entity].plural
: ResourcesFriendly[entity].singular}</span>
<span class="inline-tag">{totalItems(entityCounter)}</span>
</div>
</div>
@@ -1,24 +1,22 @@
import type { Aggregation, Invoice } from '$lib/sdk/billing';
import { accumulateUsage } from '$lib/sdk/usage';
import { getSdkForProject, sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';
import type { Organization } from '$lib/stores/organization';
import type { Aggregation, Invoice } from '$lib/sdk/billing';
import { accumulateUsage } from '$lib/sdk/usage';
export const load: PageLoad = async ({ params, parent }) => {
const { invoice, project } = params;
const parentData = await parent();
const org = parentData.organization as Organization;
const { organization } = await parent();
let startDate: string = org.billingCurrentInvoiceDate;
let endDate: string = org.billingNextInvoiceDate;
let startDate: string = organization.billingCurrentInvoiceDate;
let endDate: string = organization.billingNextInvoiceDate;
let currentInvoice: Invoice = undefined;
let currentAggregation: Aggregation = undefined;
if (invoice) {
currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice);
currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice);
currentAggregation = await sdk.forConsole.billing.getAggregation(
org.$id,
organization.$id,
currentInvoice.aggregationId
);
@@ -27,7 +25,7 @@ export const load: PageLoad = async ({ params, parent }) => {
}
const [invoices, usage] = await Promise.all([
sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]),
sdk.forConsole.billing.listInvoices(organization.$id, [Query.orderDesc('from')]),
/**
* Workaround because project id might not be populated yet.
*/
@@ -6,7 +6,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Add your webhook</svelte:fragment>
<svelte:fragment slot="title">Configuration</svelte:fragment>
<svelte:fragment slot="subtitle">
Use webhooks to build integrations that subscribe to specific events on Appwrite. Appwrite
will send an HTTP POST payload to the webhook's configured URL when one of the specified
@@ -18,7 +18,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Create file</svelte:fragment>
<svelte:fragment slot="title">File</svelte:fragment>
<svelte:fragment slot="subtitle">Upload a file to add it to your bucket.</svelte:fragment>
{#if isCloud}
@@ -7,7 +7,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Update Permissions</svelte:fragment>
<svelte:fragment slot="title">Permissions</svelte:fragment>
<svelte:fragment slot="subtitle">
Choose who can access your buckets and files. For more information, check out the
<a
@@ -51,7 +51,7 @@
</script>
<WizardStep beforeSubmit={handleBefore}>
<svelte:fragment slot="title">Confirm your details</svelte:fragment>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
</svelte:fragment>
@@ -36,7 +36,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Invite organization members</svelte:fragment>
<svelte:fragment slot="title">Invites</svelte:fragment>
<svelte:fragment slot="subtitle">
Invite team members to collaborate with you in the Appwrite console. Members will have
access to all services and payment data within your organization.
@@ -103,7 +103,7 @@
</script>
<WizardStep beforeSubmit={handleBefore}>
<svelte:fragment slot="title">Choose a plan</svelte:fragment>
<svelte:fragment slot="title">Plan selection</svelte:fragment>
<p class="body-text-1 u-bold common-section">Plan</p>
<p class="text u-margin-block-start-4">
@@ -49,7 +49,7 @@
{#if downgradeToStarter}
<WizardStep>
<svelte:fragment slot="title">Confirm plan change</svelte:fragment>
<svelte:fragment slot="title">Change confirmation</svelte:fragment>
<svelte:fragment slot="subtitle">
Your feedback is important to us and helps us improve the services Appwrite offer. If
there is a specific reason you chose to change your plan at this time, please let us
@@ -65,7 +65,7 @@
</WizardStep>
{:else}
<WizardStep>
<svelte:fragment slot="title">Confirm your details</svelte:fragment>
<svelte:fragment slot="title">Details</svelte:fragment>
<svelte:fragment slot="subtitle">
Confirm the details of your new organization and start your free trial.
</svelte:fragment>
@@ -52,7 +52,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Invite organization members</svelte:fragment>
<svelte:fragment slot="title">Invites</svelte:fragment>
<svelte:fragment slot="subtitle">
Invite team members to collaborate with you in the Appwrite console. Members will have
access to all services and payment data within your organization.
@@ -7,8 +7,8 @@
</script>
<WizardStep>
<svelte:fragment slot="title"
>Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan</svelte:fragment>
<svelte:fragment slot="title">
Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan</svelte:fragment>
<svelte:fragment slot="subtitle">
View the current usage for <b>{$organization.name}</b> and where you will exceed the limits of
your new plan.
@@ -6,7 +6,7 @@
</script>
<WizardStep>
<svelte:fragment slot="title">Thanks for the feedback!</svelte:fragment>
<svelte:fragment slot="title">Thank you</svelte:fragment>
<div class="u-flex u-main-center">
{#if $app.themeInUse === 'dark'}

Some files were not shown because too many files have changed in this diff Show More