mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch '1.5.x' of https://github.com/appwrite/console into feat-mfa
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
@@ -24,8 +19,8 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
# - name: Audit dependencies
|
||||
# run: npm audit --audit-level low
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level low
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Svelte Diagnostics
|
||||
|
||||
Generated
+532
-408
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -18,7 +18,7 @@
|
||||
"e2e": "playwright test tests/e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "0.6.0-rc.7",
|
||||
"@appwrite.io/console": "^0.6.0-rc.8",
|
||||
"@appwrite.io/pink": "0.2.0",
|
||||
"@appwrite.io/pink-icons": "0.2.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
|
||||
@@ -139,6 +139,7 @@ export function isTrackingAllowed() {
|
||||
}
|
||||
|
||||
export enum Submit {
|
||||
DownloadDPA = 'submit_download_dpa',
|
||||
Error = 'submit_error',
|
||||
AccountCreate = 'submit_account_create',
|
||||
AccountLogin = 'submit_account_login',
|
||||
@@ -161,6 +162,8 @@ export enum Submit {
|
||||
UserUpdateStatus = 'submit_user_update_status',
|
||||
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
|
||||
UserUpdateVerificationPhone = 'submit_user_update_verification_phone',
|
||||
UserTargetCreate = 'submit_user_target_create',
|
||||
UserTargetDelete = 'submit_user_target_delete',
|
||||
OrganizationCreate = 'submit_organization_create',
|
||||
OrganizationDelete = 'submit_organization_delete',
|
||||
OrganizationUpdateName = 'submit_organization_update_name',
|
||||
@@ -287,5 +290,17 @@ export enum Submit {
|
||||
SmsResetTemplate = 'submit_sms_reset_template',
|
||||
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
|
||||
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
|
||||
MessagingProviderCreate = 'submit_messaging_provider_create',
|
||||
MessagingProviderDelete = 'submit_messaging_provider_delete',
|
||||
MessagingProviderUpdate = 'submit_messaging_provider_update',
|
||||
MessagingMessageCreate = 'submit_messaging_message_create',
|
||||
MessagingMessageUpdate = 'submit_messaging_message_update',
|
||||
MessagingMessageDelete = 'submit_messaging_message_delete',
|
||||
MessagingTopicCreate = 'submit_messaging_topic_create',
|
||||
MessagingTopicDelete = 'submit_messaging_topic_delete',
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdateDescription = 'submit_messaging_topic_update_description',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const groups = [
|
||||
'platforms',
|
||||
'databases',
|
||||
'functions',
|
||||
'messaging',
|
||||
'storage',
|
||||
'domains',
|
||||
'webhooks',
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts" context="module">
|
||||
type Consent = {
|
||||
key: string;
|
||||
accepted: Record<string, boolean>;
|
||||
};
|
||||
export const settings = writable<boolean>(false);
|
||||
export const show = writable<boolean>(false);
|
||||
export const consent = writable<Consent>(
|
||||
JSON.parse(globalThis?.localStorage?.getItem('consent') ?? null)
|
||||
);
|
||||
consent.subscribe((value) => {
|
||||
if (browser) {
|
||||
globalThis.localStorage.setItem('consent', JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { Modal } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const key = new Date('2023-11-07');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let selected = {};
|
||||
|
||||
$: if ($settings) {
|
||||
selected = $consent?.accepted ?? {};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($consent) {
|
||||
const date = new Date($consent.key);
|
||||
if (key > date) {
|
||||
show.set(true);
|
||||
}
|
||||
} else {
|
||||
show.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
function saveSettings(obj: Consent) {
|
||||
consent.set(obj);
|
||||
}
|
||||
|
||||
function confirmChoices(choices: Consent['accepted']) {
|
||||
const consent = {
|
||||
key: key.toISOString(),
|
||||
accepted: choices
|
||||
};
|
||||
saveSettings(consent);
|
||||
dispatch('confirm', consent);
|
||||
show.set(false);
|
||||
settings.set(false);
|
||||
}
|
||||
|
||||
function acceptAll() {
|
||||
confirmChoices({
|
||||
analytics: true
|
||||
});
|
||||
}
|
||||
|
||||
function rejectAll() {
|
||||
confirmChoices({});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $show}
|
||||
<div class="card is-consent">
|
||||
<p>
|
||||
By clicking "Accept all", you agree to the storing of cookies on your device to analyze
|
||||
site usage.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="is-consent-buttons u-flex u-margin-block-start-16 u-main-space-between u-cross-center">
|
||||
<Button class="u-padding-inline-0" text on:click={() => settings.set(true)}>
|
||||
Cookie settings
|
||||
</Button>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button secondary on:click={rejectAll}>Only required</Button>
|
||||
<Button secondary on:click={acceptAll}>Accept all</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:show={$settings} title="Cookie Preferences">
|
||||
<p>
|
||||
We use cookies to improve your site experience. The "strictly necessary" cookies are
|
||||
required for Appwrite to function.
|
||||
</p>
|
||||
<div class="u-flex-vertical u-gap-24 u-width-full-line" style:margin-block-end="24px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<input type="checkbox" checked disabled />
|
||||
<div>
|
||||
<span class="text u-bold">Strictly necessary cookies</span>
|
||||
<p class="text u-margin-block-start-8">
|
||||
These are the cookies required for Appwrite to function.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-8">
|
||||
<input id="analytics" type="checkbox" bind:checked={selected['analytics']} />
|
||||
<div>
|
||||
<label for="analytics" class="text u-bold">Product analytics</label>
|
||||
<span class="">(optional)</span>
|
||||
<p class="text u-margin-block-start-8">
|
||||
We include analytics cookies to understand how you use our product and design
|
||||
better experiences.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text external href="https://appwrite.io/privacy">Privacy Policy</Button>
|
||||
<Button on:click={() => confirmChoices(selected)}>Save preferences</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
.card {
|
||||
position: fixed;
|
||||
padding: 1.5rem;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
@media #{$break1} {
|
||||
.card {
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
max-width: 100%;
|
||||
|
||||
.is-consent-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
export let noStyle = false;
|
||||
export let fullWidth = false;
|
||||
export let fixed = false;
|
||||
export let display = 'block';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
blur: undefined;
|
||||
@@ -100,7 +101,11 @@
|
||||
|
||||
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
|
||||
|
||||
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
|
||||
<div
|
||||
class:drop-wrapper={!noStyle}
|
||||
class:u-cross-child-start={childStart}
|
||||
bind:this={element}
|
||||
style:display>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { EmptySearch } from '.';
|
||||
import { queries } from './filters';
|
||||
|
||||
export let resource;
|
||||
</script>
|
||||
|
||||
<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 any {resource}.</b>
|
||||
<p>There are no {resource} that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
@@ -118,10 +118,12 @@
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
options={$columns
|
||||
.filter((c) => c.filter !== false)
|
||||
.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
placeholder="Select column"
|
||||
bind:value={columnId} />
|
||||
<InputSelect
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as filters } from './filters.svelte';
|
||||
export { default as Filters } from './filters.svelte';
|
||||
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
|
||||
|
||||
@@ -15,6 +15,7 @@ export { default as UploadBox } from './uploadBox.svelte';
|
||||
export { default as List } from './list.svelte';
|
||||
export { default as ListItem } from './listItem.svelte';
|
||||
export { default as Empty } from './empty.svelte';
|
||||
export { default as EmptyFilter } from './emptyFilter.svelte';
|
||||
export { default as EmptySearch } from './emptySearch.svelte';
|
||||
export { default as Drop } from './drop.svelte';
|
||||
export { default as DropList } from './dropList.svelte';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let name: string;
|
||||
export let group: string;
|
||||
@@ -7,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let padding = 1;
|
||||
export let icon: string = null;
|
||||
export let imageIcon: string = null;
|
||||
export let fullHeight = true;
|
||||
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
|
||||
export let backgroundColor: string = null;
|
||||
@@ -58,5 +61,12 @@
|
||||
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if imageIcon}
|
||||
<img
|
||||
class="u-margin-inline-start-auto"
|
||||
style:--p-text-size="1.25rem"
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${imageIcon}.svg`}
|
||||
alt={imageIcon} />
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -17,6 +17,7 @@ export enum Dependencies {
|
||||
ACCOUNT_SESSIONS = 'dependency:account_sessions',
|
||||
USER = 'dependency:user',
|
||||
USERS = 'dependency:users',
|
||||
USER_TARGETS = 'dependency:user_targets',
|
||||
SESSIONS = 'dependency:sessions',
|
||||
TEAM = 'dependency:team',
|
||||
TEAMS = 'dependency:teams',
|
||||
@@ -47,7 +48,13 @@ export enum Dependencies {
|
||||
MIGRATIONS = 'dependency:migrations',
|
||||
COLLECTIONS = 'dependency:collections',
|
||||
RUNTIMES = 'dependency:runtimes',
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables'
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables',
|
||||
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
|
||||
MESSAGING_PROVIDER = 'dependency:messaging_provider',
|
||||
MESSAGING_MESSAGE = 'dependency:messaging_message',
|
||||
MESSAGING_TOPICS = 'dependency:messaging_topics',
|
||||
MESSAGING_TOPIC = 'dependency:messaging_topic',
|
||||
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
|
||||
}
|
||||
|
||||
export const scopes: {
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
on:click
|
||||
on:click={track}
|
||||
{href}
|
||||
{download}
|
||||
|
||||
@@ -29,3 +29,4 @@ export { default as Label } from './label.svelte';
|
||||
export { default as InputProjectId } from './inputProjectId.svelte';
|
||||
export { default as InputDate } from './inputDate.svelte';
|
||||
export { default as InputDateRange } from './inputDateRange.svelte';
|
||||
export { default as InputTime } from './inputTime.svelte';
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, Helper } from '.';
|
||||
import type { FormItemTag } from './formItem.svelte';
|
||||
|
||||
interface $$Props extends Partial<HTMLLabelElement> {
|
||||
id: string;
|
||||
label?: string;
|
||||
optionalText?: string;
|
||||
tooltip?: string;
|
||||
showLabel?: boolean;
|
||||
checked?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -18,15 +15,11 @@
|
||||
|
||||
export let id: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let tooltip: string = null;
|
||||
export let showLabel = true;
|
||||
export let checked = false;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let element: HTMLInputElement | undefined = undefined;
|
||||
export let wrapperTag: FormItemTag = 'li';
|
||||
|
||||
let error: string;
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
@@ -44,25 +37,27 @@
|
||||
</script>
|
||||
|
||||
<FormItem tag={wrapperTag}>
|
||||
{#if label}
|
||||
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<label class="choice-item" for={id}>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<div class="choice-item-content">
|
||||
{#if label}
|
||||
<div class="choice-item-title">{label}</div>
|
||||
{/if}
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
@@ -65,6 +67,8 @@
|
||||
{readonly}
|
||||
{required}
|
||||
step=".001"
|
||||
{min}
|
||||
{max}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="date"
|
||||
class="input-text"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -48,7 +50,26 @@
|
||||
|
||||
<FormItem>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if $$slots.popover}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -57,7 +59,26 @@
|
||||
|
||||
<FormItem>
|
||||
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if $$slots.popover}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import { Drop, Trim } from '$lib/components';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { onMount } from 'svelte';
|
||||
import { Helper, Label } from '.';
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let hovering = false;
|
||||
let show = false;
|
||||
|
||||
function setFiles(value: FileList) {
|
||||
if (!value) return;
|
||||
@@ -94,7 +95,26 @@
|
||||
<div>
|
||||
{#if label}
|
||||
<Label {required} {optionalText} {tooltip} hide={!label}>
|
||||
{label}
|
||||
{label}{#if $$slots.popover}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div
|
||||
|
||||
@@ -47,6 +47,24 @@
|
||||
<slot />
|
||||
{/if}
|
||||
</Label>
|
||||
<!-- <label class="choice-item" for={id}>
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{disabled}
|
||||
{required}
|
||||
{value}
|
||||
type="radio"
|
||||
bind:group
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<div
|
||||
class="choice-item-content u-cross-child-center"
|
||||
class:u-width-full-line={fullWidth}>
|
||||
<div class="choice-item-title">{label}</div>
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label> -->
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -25,6 +26,7 @@
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -74,7 +76,26 @@
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if $$slots.popover}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div class="dropped card u-max-width-250" style="--p-card-padding: .75rem">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{min}
|
||||
{max}
|
||||
step="60"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="time"
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
@@ -19,6 +19,13 @@ export type Column = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: ColumnType;
|
||||
/**
|
||||
* Set to false to hide by default
|
||||
*/
|
||||
show: boolean;
|
||||
width?: number;
|
||||
/**
|
||||
* Set to false to disable filtering for this column
|
||||
*/
|
||||
filter?: boolean;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
tierToPlan,
|
||||
getServiceLimit,
|
||||
type PlanServices,
|
||||
showUsageRatesModal,
|
||||
checkForUsageFees,
|
||||
readOnly,
|
||||
checkForProjectLimitation
|
||||
} from '$lib/stores/billing';
|
||||
import { Alert, DropList, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { ContainerButton } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
checkForProjectLimitation,
|
||||
checkForUsageFees,
|
||||
getServiceLimit,
|
||||
readOnly,
|
||||
showUsageRatesModal,
|
||||
tierToPlan,
|
||||
type PlanServices
|
||||
} from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { ContainerButton } from '.';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
@@ -35,7 +35,14 @@
|
||||
|
||||
let showDropdown = false;
|
||||
|
||||
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
|
||||
// TODO: remove the default billing limits when backend is updated with billing code
|
||||
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
|
||||
bandwidth: 1,
|
||||
documents: 1,
|
||||
storage: 1,
|
||||
users: 1,
|
||||
executions: 1
|
||||
};
|
||||
const limitedServices = [
|
||||
{ name: 'bandwidth', value: bandwidth },
|
||||
{ name: 'documents', value: documents },
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import { settings } from '$lib/components/consent.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { version } from '$routes/console/store';
|
||||
|
||||
@@ -39,6 +41,18 @@
|
||||
<span class="text">Privacy</span>
|
||||
</a>
|
||||
</li>
|
||||
{#if isCloud}
|
||||
<li class="inline-links-item">
|
||||
<span
|
||||
style:cursor="pointer"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click={() => settings.set(true)}>
|
||||
<span class="text">Cookies</span>
|
||||
</span>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main-footer-end">
|
||||
|
||||
@@ -124,6 +124,23 @@
|
||||
<span class="text">Functions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/messaging`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_messaging')}
|
||||
href={`${projectPath}/messaging`}
|
||||
use:tooltip={{
|
||||
content: 'Messaging',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-send" aria-hidden="true" />
|
||||
<span class="text">Messaging</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
component: typeof SvelteComponent<unknown>;
|
||||
optional?: boolean;
|
||||
disabled?: boolean;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: () => Promise<void>;
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
</script>
|
||||
@@ -128,6 +132,7 @@
|
||||
|
||||
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
|
||||
$: isLastStep = $wizard.step === steps.size;
|
||||
$: currentStep = steps.get($wizard.step);
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
@@ -176,7 +181,7 @@
|
||||
{/each}
|
||||
<div class="form-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
{#if !isLastStep && sortedSteps[$wizard.step - 1]?.[1]?.optional}
|
||||
{#if !isLastStep && currentStep?.optional}
|
||||
<Button text on:click={() => dispatch('finish')}>
|
||||
Skip optional steps
|
||||
</Button>
|
||||
@@ -188,6 +193,13 @@
|
||||
<Button secondary on:click={previousStep}>Back</Button>
|
||||
{/if}
|
||||
|
||||
{#if currentStep?.actions}
|
||||
{#each currentStep.actions as action}
|
||||
<Button secondary on:click={action.onClick}>
|
||||
{action.label}</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<Button submit disabled={$wizard.nextDisabled}>
|
||||
{isLastStep ? finalAction : 'Next'}
|
||||
</Button>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { cachedStore } from '$lib/helpers/cache';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { headerAlert } from './headerAlert';
|
||||
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
|
||||
import { diffDays } from '$lib/helpers/date';
|
||||
import { addNotification, notifications } from './notifications';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
@@ -161,7 +160,12 @@ export function calculateTrialDay(org: Organization) {
|
||||
if (org?.billingPlan === BillingPlan.STARTER) return false;
|
||||
const endDate = new Date(org?.billingStartDate);
|
||||
const today = new Date();
|
||||
const days = diffDays(today, endDate);
|
||||
|
||||
let diffTime = endDate.getTime() - today.getTime();
|
||||
diffTime = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
|
||||
const days = diffTime < 1 ? 0 : diffTime;
|
||||
|
||||
daysLeftInTrial.set(days);
|
||||
return days;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import Microsoft from '../../routes/console/project-[project]/auth/microsoftOAut
|
||||
import Oidc from '../../routes/console/project-[project]/auth/oidcOAuth.svelte';
|
||||
import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte';
|
||||
|
||||
export type Provider = Models.Provider & {
|
||||
key: string;
|
||||
export type Provider = Models.AuthProvider & {
|
||||
icon: string;
|
||||
docs?: string;
|
||||
component?: typeof SvelteComponent<unknown>;
|
||||
@@ -25,12 +24,11 @@ export type Providers = {
|
||||
const setProviders = (project: Models.Project): Provider[] => {
|
||||
return (
|
||||
project?.oAuthProviders.map((n) => {
|
||||
const p = n as Models.Provider & { key: string };
|
||||
let docs: Provider['docs'];
|
||||
let icon: Provider['icon'] = p.key.toLowerCase();
|
||||
let icon: Provider['icon'] = n.key.toLowerCase();
|
||||
let component: Provider['component'] = Main;
|
||||
|
||||
switch (p.key.toLowerCase()) {
|
||||
switch (n.key.toLowerCase()) {
|
||||
case 'amazon':
|
||||
docs = 'https://developer.amazon.com/apps-and-games/services-and-apis';
|
||||
break;
|
||||
@@ -158,7 +156,7 @@ const setProviders = (project: Models.Project): Provider[] => {
|
||||
}
|
||||
|
||||
return {
|
||||
...p,
|
||||
...n,
|
||||
icon,
|
||||
docs,
|
||||
component
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
{/await}
|
||||
</FormList>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
<FormList class="u-margin-block-start-24">
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
@@ -78,5 +78,5 @@
|
||||
bind:id={$templateConfig.$id}
|
||||
fullWidth />
|
||||
{/if}
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import Loading from './loading.svelte';
|
||||
import { loading, requestedMigration } from './store';
|
||||
import { parseIfString } from '$lib/helpers/object';
|
||||
import Consent, { consent } from '$lib/components/consent.svelte';
|
||||
|
||||
if (browser) {
|
||||
window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false;
|
||||
@@ -51,7 +52,7 @@
|
||||
/**
|
||||
* LogRocket
|
||||
*/
|
||||
if (isCloud && isTrackingAllowed()) {
|
||||
if ($consent?.accepted?.analytics && isCloud && isTrackingAllowed()) {
|
||||
LogRocket.init('rgthvf/appwrite', {
|
||||
dom: {
|
||||
inputSanitizer: true
|
||||
@@ -124,6 +125,9 @@
|
||||
</script>
|
||||
|
||||
<Notifications />
|
||||
{#if isCloud}
|
||||
<Consent />
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
await sdk.forConsole.account.updateStatus();
|
||||
await sdk.forConsole.account.delete();
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<Button
|
||||
external
|
||||
secondary
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/account#createEmailSession">
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/account#createEmailPasswordSession">
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
component: ChoosePlan
|
||||
});
|
||||
$changeTierSteps.set(2, {
|
||||
label: 'Payment details',
|
||||
label: 'Payment',
|
||||
component: PaymentDetails
|
||||
});
|
||||
$changeTierSteps.set(3, {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
component: OrganizationDetails
|
||||
});
|
||||
$createOrgSteps.set(2, {
|
||||
label: 'Payment details',
|
||||
label: 'Payment',
|
||||
component: PaymentDetails
|
||||
});
|
||||
$createOrgSteps.set(3, {
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Project details',
|
||||
label: 'Details',
|
||||
component: Step1
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Select region',
|
||||
label: 'Region',
|
||||
component: Step2
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
import Delete from './deleteOrganization.svelte';
|
||||
import Delete from './deleteOrganizationModal.svelte';
|
||||
import DownloadDPA from './downloadDPA.svelte';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { isCloud } from '$lib/system';
|
||||
|
||||
export let data;
|
||||
let name: string;
|
||||
@@ -63,6 +65,10 @@
|
||||
</CardGrid>
|
||||
</Form>
|
||||
|
||||
{#if isCloud}
|
||||
<DownloadDPA />
|
||||
{/if}
|
||||
|
||||
<CardGrid danger>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Delete organization</Heading>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { Box, CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
async function downloadPdf() {
|
||||
trackEvent(Submit.DownloadDPA);
|
||||
const today = new Date().toISOString();
|
||||
const prefs = await sdk.forConsole.account.getPrefs();
|
||||
const newPrefs = { ...prefs, DPA: today };
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Download DPA document</Heading>
|
||||
</div>
|
||||
<p class="text">
|
||||
After downloading, have the DPA signed by your organization's compliance authority, such as
|
||||
your CEO or Compliance Manager, and submit it to <a
|
||||
class="link"
|
||||
href="mailto:privacy@appwrite.io">privacy@appwrite.io</a
|
||||
>.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<Box>
|
||||
<h6>
|
||||
<b>Data Processing Agreement (DPA) document</b>
|
||||
</h6>
|
||||
<p class="text u-margin-block-start-8">
|
||||
The DPA is a legal document that describes the roles and responsibilities of
|
||||
Appwrite and the organization when personal data is processed. <a
|
||||
class="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://appwrite.io/docs/advanced/security/gdpr#dpa"
|
||||
>Learn more about the DPA</a
|
||||
>.
|
||||
</p>
|
||||
<Button
|
||||
secondary
|
||||
external
|
||||
class="u-margin-block-start-16"
|
||||
on:click={downloadPdf}
|
||||
href="/legal/dpa.pdf"
|
||||
event="download_dpa">
|
||||
<span class="icon-download" aria-hidden="true" />
|
||||
<span class="text">Download</span>
|
||||
</Button>
|
||||
</Box>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -62,7 +62,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep>
|
||||
<svelte:fragment slot="title">Select region</svelte:fragment>
|
||||
<svelte:fragment slot="title">Regions</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Choose a deployment region for your project. This region cannot be changed.
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
title: 'Memberships',
|
||||
event: 'memberships'
|
||||
},
|
||||
{
|
||||
href: `${path}/targets`,
|
||||
title: 'Targets',
|
||||
event: 'targets'
|
||||
},
|
||||
{
|
||||
href: `${path}/sessions`,
|
||||
title: 'Sessions',
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Empty,
|
||||
EmptySearch,
|
||||
PaginationWithLimit,
|
||||
Heading,
|
||||
ViewSelector,
|
||||
EmptyFilter
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import type { PageData } from './$types';
|
||||
import Table from './table.svelte';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import { columns } from './store';
|
||||
import { View } from '$lib/helpers/load';
|
||||
import Create from './create.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
let showAdd = false;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Targets</Heading>
|
||||
<!-- TODO: Remove u-hide to add creating a target -->
|
||||
<div class="is-only-mobile u-hide">
|
||||
<Button on:click={() => (showAdd = true)} event="create_user_target">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add target</span>
|
||||
</Button>
|
||||
</div>
|
||||
<!-- TODO: Remove when searching is added -->
|
||||
<div class="u-flex u-main-end u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<div>
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<div class="u-hide">
|
||||
<Button on:click={() => (showAdd = true)} event="create_user_target">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add target</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: Add searching when API supports it -->
|
||||
<!-- <SearchQuery search={data.search} placeholder="Search by name">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<Button on:click={() => (showAdd = true)} event="create_user_target">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Add target</span>
|
||||
</Button>
|
||||
</div>
|
||||
</SearchQuery> -->
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={View.Table}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.targets.total}
|
||||
<Table {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Targets"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.targets.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="targets" />
|
||||
{:else if data.search}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no targets that match your search.</p>
|
||||
</div>
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/project-${$page.params.project}/auth/user-${$page.params.user}/targets`}>
|
||||
Clear Search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<!-- TODO: update docs link -->
|
||||
<Empty
|
||||
single
|
||||
on:click={() => (showAdd = true)}
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/teams"
|
||||
target="subscriber" />
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<Create bind:show={showAdd} on:close={() => (showAdd = false)} />
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { queryParamToMap, queries } from '$lib/components/filters';
|
||||
|
||||
export const load: PageLoad = async ({ params, url, route, depends }) => {
|
||||
depends(Dependencies.USER_TARGETS);
|
||||
const page = getPage(url);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
const search = getSearch(url);
|
||||
const query = getQuery(url);
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
|
||||
const payload = {
|
||||
queries: [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc(''),
|
||||
...parsedQueries.values()
|
||||
]
|
||||
};
|
||||
|
||||
if (search) {
|
||||
payload['search'] = search;
|
||||
}
|
||||
|
||||
// TODO: remove when the API is ready with data
|
||||
// This allows us to mock w/ data and when search returns 0 results
|
||||
const targets: { targets: Models.Target[]; total: number } = await sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(`${sdk.forProject.client.config.endpoint}/users/${params.user}/targets`),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
const promisesById: Record<string, Promise<any>> = {};
|
||||
targets.targets.forEach((target) => {
|
||||
if (target.providerId && !promisesById[target.providerId]) {
|
||||
promisesById[target.providerId] = sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/${target.providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const providersById: Record<string, Models.Provider> = {};
|
||||
const resolved = await Promise.allSettled(Object.values(promisesById));
|
||||
resolved.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const provider = result.value;
|
||||
providersById[provider.$id] = provider;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
query,
|
||||
targets,
|
||||
providersById
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Modal, CustomId } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button, InputText, FormList, InputSelect, InputPhone } from '$lib/elements/forms';
|
||||
import InputEmail from '$lib/elements/forms/inputEmail.svelte';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ProviderTypes } from '$routes/console/project-[project]/messaging/providerType.svelte';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
|
||||
let providerType = ProviderTypes.Push;
|
||||
let identifier = '';
|
||||
let name = '';
|
||||
let providerId = '';
|
||||
let id: string = null;
|
||||
let showCustomId = false;
|
||||
|
||||
const providerTypeOptions = [
|
||||
{ label: 'Push', value: ProviderTypes.Push },
|
||||
{ label: 'Email', value: ProviderTypes.Email },
|
||||
{ label: 'SMS', value: ProviderTypes.Sms }
|
||||
];
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
targetId: id ? id : ID.unique(),
|
||||
providerType,
|
||||
identifier
|
||||
};
|
||||
|
||||
if (providerId) {
|
||||
payload['providerId'] = providerId;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
payload['name'] = name;
|
||||
}
|
||||
|
||||
await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/users/${$page.params.user}/targets`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
payload
|
||||
);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Target has been created`
|
||||
});
|
||||
name = id = null;
|
||||
invalidate(Dependencies.USER_TARGETS);
|
||||
trackEvent(Submit.UserTargetCreate, {
|
||||
customId: !!id,
|
||||
providerType: providerType
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.UserTargetCreate);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure values are reset when modal is opened
|
||||
$: if (show) {
|
||||
showCustomId = false;
|
||||
providerType = ProviderTypes.Push;
|
||||
identifier = '';
|
||||
name = '';
|
||||
providerId = '';
|
||||
id = null;
|
||||
}
|
||||
|
||||
$: if (providerType) {
|
||||
identifier = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Create target" size="big" bind:show onSubmit={create}>
|
||||
<FormList>
|
||||
<InputSelect
|
||||
id="provider-type"
|
||||
label="Provider Type"
|
||||
bind:value={providerType}
|
||||
options={providerTypeOptions} />
|
||||
{#if providerType === ProviderTypes.Push}
|
||||
<InputText
|
||||
id="provider-id"
|
||||
label="Provider ID"
|
||||
placeholder="Enter provider ID"
|
||||
bind:value={providerId}
|
||||
required />
|
||||
<InputText
|
||||
id="identifier"
|
||||
label="Identifier"
|
||||
placeholder="Enter push token"
|
||||
bind:value={identifier}
|
||||
required />
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter target name"
|
||||
bind:value={name}
|
||||
required />
|
||||
{:else if providerType === ProviderTypes.Email}
|
||||
<InputEmail
|
||||
id="identifier"
|
||||
label="Identifier"
|
||||
placeholder="Enter email"
|
||||
bind:value={identifier}
|
||||
required />
|
||||
{:else if providerType === ProviderTypes.Sms}
|
||||
<InputPhone
|
||||
id="identifier"
|
||||
label="Identifier"
|
||||
placeholder="Enter phone number"
|
||||
bind:value={identifier}
|
||||
required />
|
||||
{/if}
|
||||
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Target ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Target" bind:id autofocus={false} />
|
||||
{/if}
|
||||
</FormList>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Create</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Target ID', type: 'string', show: true, width: 140 },
|
||||
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
|
||||
{ id: 'providerType', title: 'Type', type: 'string', show: true, filter: true, width: 80 },
|
||||
{ id: 'provider', title: 'Provider', type: 'string', show: true, filter: false, width: 80 },
|
||||
{ id: '$createdAt', title: 'Created', type: 'string', show: true, width: 100 }
|
||||
]);
|
||||
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { FloatingActionBar, Id, Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellCheck,
|
||||
TableCellHead,
|
||||
TableCellHeadCheck,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import ProviderType, {
|
||||
ProviderTypes
|
||||
} from '$routes/console/project-[project]/messaging/providerType.svelte';
|
||||
import Provider from '$routes/console/project-[project]/messaging/provider.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { invalidate } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let selectedIds: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
|
||||
async function deleteTarget(id: string) {
|
||||
await sdk.forProject.client.call(
|
||||
'DELETE',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/users/${$page.params.user}/targets/${id}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const promises = selectedIds.map((id) => deleteTarget(id));
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.UserTargetDelete, {
|
||||
total: selectedIds.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedIds.length} target${selectedIds.length > 1 ? 's' : ''} deleted`
|
||||
});
|
||||
invalidate(Dependencies.USER_TARGETS);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.UserTargetDelete);
|
||||
} finally {
|
||||
selectedIds = [];
|
||||
showDelete = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected={selectedIds}
|
||||
pageItemsIds={data.targets.targets.map((d) => d.$id)} />
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.targets.targets as target (target.$id)}
|
||||
{@const provider = data.providersById[target.providerId]}
|
||||
<TableRow>
|
||||
<TableCellCheck bind:selectedIds id={target.$id} />
|
||||
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key $columns}
|
||||
<TableCell title={column.title}>
|
||||
<Id value={target[column.id]}>
|
||||
{target[column.id]}
|
||||
</Id>
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'target'}
|
||||
<TableCell title={column.title}>
|
||||
{#if target.providerType === ProviderTypes.Push}
|
||||
{target.name}
|
||||
{:else}
|
||||
{target.identifier}
|
||||
{/if}
|
||||
</TableCell>
|
||||
{:else if column.id === 'providerType'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<ProviderType type={target.providerType} size="s" />
|
||||
</TableCellText>
|
||||
{:else if column.id === 'provider'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{#if provider}
|
||||
<Provider provider={provider.provider} size="s" />
|
||||
{/if}
|
||||
</TableCellText>
|
||||
{:else if column.id === '$createdAt'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{toLocaleDateTime(target[column.id])}
|
||||
</TableCellText>
|
||||
{:else}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{target[column.id]}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
|
||||
<FloatingActionBar show={selectedIds.length > 0}>
|
||||
<div class="u-flex u-cross-center u-main-space-between actions">
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<span class="indicator body-text-2 u-bold">{selectedIds.length}</span>
|
||||
<p>
|
||||
<span class="is-only-desktop">
|
||||
{selectedIds.length > 1 ? 'subscribers' : 'subscriber'}
|
||||
</span>
|
||||
selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<Button text on:click={() => (selectedIds = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>
|
||||
<p>Delete</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingActionBar>
|
||||
|
||||
<Modal
|
||||
title="Delete target"
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
bind:show={showDelete}
|
||||
onSubmit={handleDelete}
|
||||
headerDivider={false}
|
||||
closable={!deleting}>
|
||||
<p class="text" data-private>
|
||||
Are you sure you want to delete <b>{selectedIds.length}</b>
|
||||
{selectedIds.length > 1 ? 'targets' : 'target'}?
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)} disabled={deleting}>Cancel</Button>
|
||||
<Button secondary submit disabled={deleting}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Empty, EmptySearch, Heading, PaginationWithLimit } from '$lib/components';
|
||||
import Filters from '$lib/components/filters/filters.svelte';
|
||||
import { hasPageQueries, queries } from '$lib/components/filters/store';
|
||||
import { Filters, hasPageQueries, queries } from '$lib/components/filters';
|
||||
import ViewSelector from '$lib/components/viewSelector.svelte';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import type { ColumnType } from '$lib/helpers/types';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { getLimit, getPage, getQuery, getView, pageToOffset, View } from '$lib/h
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Query } from '@appwrite.io/console';
|
||||
import type { PageLoad } from './$types';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters/store';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
|
||||
export const load: PageLoad = async ({ params, depends, url, route }) => {
|
||||
depends(Dependencies.DOCUMENTS);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter';
|
||||
import { project } from '../store';
|
||||
import { showCreate } from './store';
|
||||
|
||||
// TODO: finalize the commands
|
||||
|
||||
$: $registerCommands([
|
||||
{
|
||||
label: 'Create message',
|
||||
callback: () => {
|
||||
if (!$page.url.pathname.endsWith('messaging')) {
|
||||
goto(`/console/project-${$project.$id}/messaging`);
|
||||
}
|
||||
$showCreate = true;
|
||||
},
|
||||
keys: $page.url.pathname.endsWith('messaging') ? ['c'] : ['c', 'm'],
|
||||
icon: 'plus',
|
||||
group: 'messaging'
|
||||
},
|
||||
{
|
||||
label: 'Go to topics',
|
||||
callback() {
|
||||
goto(`/console/project-${$project.$id}/messaging/topics`);
|
||||
},
|
||||
keys: ['g', 't'],
|
||||
disabled:
|
||||
$page.url.pathname.endsWith('topics') || $page.url.pathname.includes('message-'),
|
||||
group: 'navigation',
|
||||
rank: 10
|
||||
},
|
||||
{
|
||||
label: 'Go to providers',
|
||||
callback() {
|
||||
goto(`/console/project-${$project.$id}/messaging/providers`);
|
||||
},
|
||||
keys: ['g', 'p'],
|
||||
disabled:
|
||||
$page.url.pathname.endsWith('topics') || $page.url.pathname.includes('message-'),
|
||||
group: 'navigation',
|
||||
rank: 10
|
||||
}
|
||||
// {
|
||||
// label: 'Find messages',
|
||||
// callback: () => {
|
||||
// addSubPanel(BucketsPanel);
|
||||
// },
|
||||
// group: 'messaging',
|
||||
// rank: -1
|
||||
// }
|
||||
]);
|
||||
|
||||
$: $updateCommandGroupRanks({ messaging: 200, navigation: 100 });
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Messaging - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
@@ -0,0 +1,10 @@
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
Empty,
|
||||
EmptyFilter,
|
||||
EmptySearch,
|
||||
FloatingActionBar,
|
||||
Heading,
|
||||
Id,
|
||||
PaginationWithLimit,
|
||||
SearchQuery,
|
||||
ViewSelector
|
||||
} from '$lib/components';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellCheck,
|
||||
TableCellHead,
|
||||
TableCellHeadCheck,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { Container } from '$lib/layout';
|
||||
import type { PageData } from './$types';
|
||||
import CreateMessageDropdown from './createMessageDropdown.svelte';
|
||||
import FailedModal from './failedModal.svelte';
|
||||
import MessageStatusPill from './messageStatusPill.svelte';
|
||||
import ProviderType, { ProviderTypes } from './providerType.svelte';
|
||||
import { columns, showCreate } from './store';
|
||||
|
||||
export let data: PageData;
|
||||
let selected: string[] = [];
|
||||
let showDelete = false;
|
||||
let showFailed = false;
|
||||
let errors: string[] = [];
|
||||
let showCreateDropdownMobile = false;
|
||||
let showCreateDropdownDesktop = false;
|
||||
let showCreateDropdownEmpty = false;
|
||||
|
||||
const project = $page.params.project;
|
||||
|
||||
$: console.log(showDelete);
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Messages</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
<SearchQuery
|
||||
search={data.search}
|
||||
placeholder="Search by message ID, description, type, or status">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<!-- TODO: make this not database-specific -->
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.messages.total}
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected
|
||||
pageItemsIds={data.messages.messages.map((d) => d.$id)} />
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.messages.messages as message (message.$id)}
|
||||
<TableRowLink
|
||||
href={`${base}/console/project-${project}/messaging/message-${message.$id}`}>
|
||||
<TableCellCheck bind:selectedIds={selected} id={message.$id} />
|
||||
|
||||
{#each $columns as column (column.id)}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key $columns}
|
||||
<TableCell title={column.title} width={column.width}>
|
||||
<Id value={message.$id}>{message.$id}</Id>
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'message'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{#if message.providerType === ProviderTypes.Push}
|
||||
{message.data.title}
|
||||
{:else if message.providerType === ProviderTypes.Sms}
|
||||
{message.data.content}
|
||||
{:else if message.providerType === ProviderTypes.Email}
|
||||
{message.data.subject}
|
||||
{:else}
|
||||
Invalid provider
|
||||
{/if}
|
||||
</TableCellText>
|
||||
{:else if column.id === 'providerType'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<ProviderType type={message.providerType} size="s" />
|
||||
</TableCellText>
|
||||
{:else if column.id === 'status'}
|
||||
<TableCellText onlyDesktop title="Status">
|
||||
<MessageStatusPill
|
||||
status={message.status}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
errors = message.deliveryErrors;
|
||||
showFailed = true;
|
||||
}} />
|
||||
</TableCellText>
|
||||
{:else if column.type === 'datetime'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{#if !message[column.id]}
|
||||
-
|
||||
{:else}
|
||||
{toLocaleDateTime(message[column.id])}
|
||||
{/if}
|
||||
</TableCellText>
|
||||
{:else}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{message[column.id]}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
|
||||
<FloatingActionBar show={selected.length > 0}>
|
||||
<div class="u-flex u-cross-center u-main-space-between actions">
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<span class="indicator body-text-2 u-bold">{selected.length}</span>
|
||||
<p>
|
||||
<span class="is-only-desktop">
|
||||
{selected.length > 1 ? 'messages' : 'message'}
|
||||
</span>
|
||||
selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<Button text on:click={() => (selected = [])}>Cancel</Button>
|
||||
<!-- TODO: handle delete -->
|
||||
<Button secondary on:click={() => (showDelete = true)}>
|
||||
<p>Delete</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingActionBar>
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Messages"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.messages.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="messages" />
|
||||
<!-- TODO: remove data.search != 'empty' when the API is ready with data -->
|
||||
{:else if data.search && data.search != 'empty'}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no messages that match your search.</p>
|
||||
</div>
|
||||
<div class="u-flex u-gap-16">
|
||||
<!-- TODO: update docs link -->
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/products/storage/upload-download"
|
||||
text>
|
||||
Documentation
|
||||
</Button>
|
||||
<Button secondary href={`/console/project-${$page.params.project}/messaging`}>
|
||||
Clear search
|
||||
</Button>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<!-- TODO: update docs link -->
|
||||
<Empty
|
||||
single
|
||||
href="https://appwrite.io/docs"
|
||||
target="message"
|
||||
on:click={() => ($showCreate = true)}>
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
Create your first message to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/messages"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create message`}>
|
||||
Documentation
|
||||
</Button>
|
||||
<CreateMessageDropdown bind:showCreateDropdown={showCreateDropdownEmpty}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showCreateDropdownEmpty = !showCreateDropdownEmpty)}
|
||||
event="create_message">
|
||||
<span class="text">Create message</span>
|
||||
</Button>
|
||||
</CreateMessageDropdown>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<FailedModal bind:show={showFailed} {errors} />
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
View,
|
||||
getLimit,
|
||||
getPage,
|
||||
getQuery,
|
||||
getSearch,
|
||||
getView,
|
||||
pageToOffset
|
||||
} from '$lib/helpers/load';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import type { PageLoad } from './$types';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
|
||||
export const load: PageLoad = async ({ url, route }) => {
|
||||
const page = getPage(url);
|
||||
const search = getSearch(url);
|
||||
const view = getView(url, route, View.Grid);
|
||||
const limit = getLimit(url, route, CARD_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
const query = getQuery(url);
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
|
||||
// TODO: remove when the API is ready with data
|
||||
// This allows us to mock w/ data and when search returns 0 results
|
||||
let messages: { messages: Models.Message[]; total: number } = { messages: [], total: 0 };
|
||||
const params = {
|
||||
queries: [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc(''),
|
||||
...parsedQueries.values()
|
||||
]
|
||||
};
|
||||
|
||||
if (search) {
|
||||
params['search'] = search;
|
||||
}
|
||||
|
||||
const response = await sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(sdk.forProject.client.config.endpoint + '/messaging/messages'),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
params
|
||||
);
|
||||
|
||||
messages = response;
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
query,
|
||||
page,
|
||||
view,
|
||||
messages
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem } from '$lib/components';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { targetsById } from './wizard/store';
|
||||
import UserTargetsModal from './userTargetsModal.svelte';
|
||||
import type { ProviderTypes } from './providerType.svelte';
|
||||
import TopicsModal from './topicsModal.svelte';
|
||||
import { topicsById } from './store';
|
||||
|
||||
export let showDropdown: boolean;
|
||||
export let showUserTargets: boolean;
|
||||
export let showTopics: boolean;
|
||||
export let providerType: ProviderTypes = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: if (showUserTargets || showTopics) {
|
||||
showDropdown = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropList bind:show={showDropdown} placement="bottom-end" fixed>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<DropListItem on:click={() => (showTopics = true)}>Select topics</DropListItem>
|
||||
<DropListItem on:click={() => (showUserTargets = true)}>Select targets</DropListItem>
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
|
||||
<TopicsModal
|
||||
bind:show={showTopics}
|
||||
bind:topicsById={$topicsById}
|
||||
on:update={(e) => {
|
||||
showTopics = false;
|
||||
dispatch('addTopics', e.detail);
|
||||
}} />
|
||||
<UserTargetsModal
|
||||
{providerType}
|
||||
bind:show={showUserTargets}
|
||||
bind:targetsById={$targetsById}
|
||||
on:update={(e) => {
|
||||
showUserTargets = false;
|
||||
dispatch('addTargets', e.detail);
|
||||
}} />
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs } from '$lib/layout';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { project } from '../store';
|
||||
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
href: `/console/organization-${$organization.$id}`,
|
||||
title: $organization.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}`,
|
||||
title: $project.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}/messaging`,
|
||||
title: 'Messaging'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 10.5C4.49857 13.5086 1.66667 16.3333 0 17C6.4 17 10.5 14.8333 11.5 13.5L16.5 15L16 0H5.5V2V4V4.5C5.5 5.5 5.5 7.5 5 10.5Z" fill="#333333"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 10.5C4.49857 13.5086 1.66667 16.3333 0 17C6.4 17 10.5 14.8333 11.5 13.5L16.5 15L16 0H5.5V2V4V4.5C5.5 5.5 5.5 7.5 5 10.5Z" fill="#E9E9EB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providers } from './providers/store';
|
||||
import Wizard from './wizard.svelte';
|
||||
import { messageParams, operation, providerType, targetsById } from './wizard/store';
|
||||
import { ProviderTypes } from './providerType.svelte';
|
||||
import { topicsById } from './store';
|
||||
|
||||
export let showCreateDropdown = false;
|
||||
</script>
|
||||
|
||||
<DropList bind:show={showCreateDropdown} scrollable placement="bottom-end">
|
||||
<slot>
|
||||
<Button on:click={() => (showCreateDropdown = !showCreateDropdown)} event="create_message">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create message</span>
|
||||
</Button>
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
{#each Object.entries(providers) as [type, option]}
|
||||
<DropListItem
|
||||
icon={option.icon}
|
||||
on:click={() => {
|
||||
if (
|
||||
type !== ProviderTypes.Email &&
|
||||
type !== ProviderTypes.Sms &&
|
||||
type !== ProviderTypes.Push
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
$operation = 'create';
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
const common = {
|
||||
topics: [],
|
||||
users: [],
|
||||
targets: []
|
||||
};
|
||||
switch (type) {
|
||||
case ProviderTypes.Email:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
subject: '',
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case ProviderTypes.Sms:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case ProviderTypes.Push:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
title: '',
|
||||
body: ''
|
||||
};
|
||||
break;
|
||||
}
|
||||
showCreateDropdown = false;
|
||||
wizard.start(Wizard);
|
||||
}}>
|
||||
{option.name}
|
||||
</DropListItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Code from '$lib/components/code.svelte';
|
||||
import Modal from '$lib/components/modal.svelte';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
|
||||
export let show: boolean;
|
||||
export let errors: string[];
|
||||
</script>
|
||||
|
||||
<Modal title="Message error" headerDivider={false} bind:show size="big">
|
||||
<div class="box u-flex-vertical u-gap-24">
|
||||
<p>Some messages failed to send.</p>
|
||||
<div style="max-inline-size: 524px">
|
||||
<Code language="html" code={errors.join('\n')} noMargin noBoxPadding allowScroll />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Tab, Tabs } from '$lib/components';
|
||||
import { isTabSelected } from '$lib/helpers/load';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
const path = `/console/project-${projectId}/messaging`;
|
||||
const tabs = [
|
||||
{
|
||||
href: path,
|
||||
title: 'Messages',
|
||||
event: 'messages',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/topics`,
|
||||
title: 'Topics',
|
||||
event: 'topics',
|
||||
hasChildren: true
|
||||
},
|
||||
{
|
||||
href: `${path}/providers`,
|
||||
title: 'Providers',
|
||||
event: 'providers'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle>Messaging</CoverTitle>
|
||||
</svelte:fragment>
|
||||
<Tabs>
|
||||
{#each tabs as tab}
|
||||
<Tab
|
||||
href={tab.href}
|
||||
selected={isTabSelected(tab, $page.url.pathname, path, tabs)}
|
||||
event={tab.event}>
|
||||
{tab.title}
|
||||
</Tab>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</Cover>
|
||||
@@ -0,0 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>Message - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.MESSAGING_MESSAGE);
|
||||
|
||||
try {
|
||||
const response: Models.Message = await sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/messages/${params.message}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
|
||||
const topicsById = {};
|
||||
const topicsPromise = Promise.allSettled(
|
||||
response.topics.map((topicId) => {
|
||||
return sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(`${sdk.forProject.client.config.endpoint}/messaging/topics/${topicId}`),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
})
|
||||
).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
topicsById[result.value.$id] = result.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const targetsById = {};
|
||||
const targetsPromise = sdk.forProject.client
|
||||
.call(
|
||||
'GET',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/messages/${params.message}/targets`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
response.targets.forEach((target) => {
|
||||
targetsById[target.$id] = target;
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.allSettled([topicsPromise, targetsPromise]);
|
||||
|
||||
return {
|
||||
topicsById,
|
||||
targetsById,
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
message: response
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(e.code, e.message);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import Delete from './delete.svelte';
|
||||
import EmailPreview from './emailPreview.svelte';
|
||||
import Overview from './overview.svelte';
|
||||
import { message } from './store';
|
||||
import { ProviderTypes } from '../providerType.svelte';
|
||||
import SMSPreview from './smsPreview.svelte';
|
||||
import PushPreview from './pushPreview.svelte';
|
||||
import {
|
||||
MessageStatuses,
|
||||
messageParams,
|
||||
operation,
|
||||
providerType,
|
||||
targetsById
|
||||
} from '../wizard/store';
|
||||
import { topicsById } from '../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Wizard from '../wizard.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
async function onEdit() {
|
||||
$operation = 'update';
|
||||
$providerType = $message.providerType;
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
|
||||
$topicsById = data.topicsById;
|
||||
$targetsById = data.targetsById;
|
||||
|
||||
$messageParams[$providerType] = {
|
||||
messageId: $message.$id,
|
||||
topics: $message.topics,
|
||||
users: $message.users,
|
||||
targets: $message.targets,
|
||||
description: $message.description,
|
||||
status: MessageStatuses.DRAFT,
|
||||
scheduledAt: $message.scheduledAt
|
||||
};
|
||||
|
||||
switch ($providerType) {
|
||||
case ProviderTypes.Email:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = ['subject', 'content', 'html'];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ProviderTypes.Sms:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = ['content'];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ProviderTypes.Push:
|
||||
{
|
||||
const { data } = $message;
|
||||
const params = [
|
||||
'title',
|
||||
'body',
|
||||
'action',
|
||||
'icon',
|
||||
'sound',
|
||||
'color',
|
||||
'tag',
|
||||
'badge'
|
||||
];
|
||||
params.forEach((key) => {
|
||||
if (typeof data[key] !== 'undefined') {
|
||||
$messageParams[$providerType][key] = data[key];
|
||||
}
|
||||
});
|
||||
const dataEntries: [string, string][] = [];
|
||||
Object.entries(data['data'] ?? {}).forEach(([key, value]) => {
|
||||
dataEntries.push([key, value.toString()]);
|
||||
});
|
||||
$messageParams[$providerType]['data'] = dataEntries || [['', '']];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
wizard.start(Wizard);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<Overview />
|
||||
{#if $message.providerType === ProviderTypes.Email}
|
||||
<EmailPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatuses.DRAFT ? onEdit : null} />
|
||||
{:else if $message.providerType === ProviderTypes.Sms}
|
||||
<SMSPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatuses.DRAFT ? onEdit : null} />
|
||||
{:else if $message.providerType === ProviderTypes.Push}
|
||||
<PushPreview
|
||||
message={$message}
|
||||
onEdit={$message.status === MessageStatuses.DRAFT ? onEdit : null} />
|
||||
{/if}
|
||||
<Delete />
|
||||
</Container>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs } from '$lib/layout';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { project } from '../../store';
|
||||
import { message } from './store';
|
||||
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
href: `/console/organization-${$organization.$id}`,
|
||||
title: $organization.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}`,
|
||||
title: $project.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}/messaging`,
|
||||
title:
|
||||
$message.data.title ?? $message.data.subject ?? $message.data.content ?? 'Message'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { BoxAvatar, CardGrid, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import DeleteModal from './deleteModal.svelte';
|
||||
import { message } from './store';
|
||||
|
||||
let showDelete = false;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Delete message</Heading>
|
||||
<p>
|
||||
The message will be permanently deleted, including all data associated with this message.
|
||||
This action is irreversible.
|
||||
</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1" data-private>
|
||||
{$message.data.title ??
|
||||
$message.data.subject ??
|
||||
$message.data.content ??
|
||||
'Message'}
|
||||
</h6>
|
||||
</svelte:fragment>
|
||||
<p>
|
||||
Last updated: {toLocaleDateTime($message.$updatedAt)}
|
||||
</p>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => (showDelete = true)} event="delete_file">Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<DeleteModal bind:show={showDelete} />
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { message } from './store';
|
||||
|
||||
export let show = false;
|
||||
|
||||
const deleteMessage = async () => {
|
||||
try {
|
||||
await sdk.forProject.client.call(
|
||||
'DELETE',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/messages/${$message.$id}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `Message has been deleted`
|
||||
});
|
||||
trackEvent(Submit.MessagingMessageDelete);
|
||||
await goto(`${base}/console/project-${$page.params.project}/messaging`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingMessageDelete);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Delete message"
|
||||
bind:show
|
||||
onSubmit={deleteMessage}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>Are you sure you want to delete this message?</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string>; };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText
|
||||
id="subject"
|
||||
label="Subject"
|
||||
disabled={true}
|
||||
bind:value={message.data.subject}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { message } from './store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle href={`/console/project-${projectId}/messaging`}>
|
||||
{$message.data.title ?? $message.data.subject ?? $message.data.content ?? 'Message'}
|
||||
</CoverTitle>
|
||||
<Id value={$message.$id} event="message">{$message.$id}</Id>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { message } from './store';
|
||||
import ProviderType, { ProviderTypes } from '../providerType.svelte';
|
||||
import MessageStatusPill from '../messageStatusPill.svelte';
|
||||
|
||||
let scheduledAt: string = '';
|
||||
if ($message.status === 'sent') {
|
||||
scheduledAt = $message.deliveredAt;
|
||||
} else if ($message.status === 'scheduled') {
|
||||
scheduledAt = $message.scheduledAt;
|
||||
}
|
||||
|
||||
let providerType = 'Invalid provider type';
|
||||
switch ($message.providerType) {
|
||||
case ProviderTypes.Email:
|
||||
providerType = 'Email';
|
||||
break;
|
||||
case ProviderTypes.Sms:
|
||||
providerType = 'SMS';
|
||||
break;
|
||||
case ProviderTypes.Push:
|
||||
providerType = 'Push';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex u-cross-center u-gap-16" data-private>
|
||||
<ProviderType type={$message.providerType} size="l">
|
||||
<Heading tag="h6" size="7">{providerType}</Heading>
|
||||
</ProviderType>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div data-private>
|
||||
<p class="title">Created: {toLocaleDateTime($message.$createdAt)}</p>
|
||||
<p class="title">Scheduled at: {toLocaleDateTime(scheduledAt)}</p>
|
||||
</div>
|
||||
<div class="u-flex u-flex-vertical u-cross-end">
|
||||
<MessageStatusPill status={$message.status} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<!-- TODO: Add support for editing draft messages -->
|
||||
<!-- <Button disabled={$message.status !== 'draft'} on:click={() => console.log('click')}
|
||||
>Edit message</Button> -->
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputText, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import PushPhone from '../pushPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string>; };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<div class="u-flex u-main-center u-margin-block-start-24 u-width-full-line">
|
||||
<PushPhone title={message.data.title} body={message.data.body} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputText id="title" label="Title" disabled={true} bind:value={message.data.title}>
|
||||
</InputText>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.body}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, FormList, InputTextarea } from '$lib/elements/forms';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import SMSPhone from '../smsPhone.svelte';
|
||||
|
||||
export let message: Models.Message & { data: Record<string, string>; };
|
||||
export let onEdit: () => void = null;
|
||||
</script>
|
||||
|
||||
<CardGrid>
|
||||
<div class="grid-1-2-col-1 u-flex-vertical u-cross-start u-gap-16">
|
||||
<Heading tag="h6" size="7">Preview</Heading>
|
||||
<SMSPhone content={message.data.content} />
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
label="Message"
|
||||
disabled={true}
|
||||
bind:value={message.data.content}>
|
||||
</InputTextarea>
|
||||
<div class="u-flex u-main-end">
|
||||
<Button secondary disabled={onEdit == null} on:click={onEdit}>Edit message</Button>
|
||||
</div>
|
||||
</FormList>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const message = derived(page, ($page) => $page.data.message as Models.Message);
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Pill } from '$lib/elements';
|
||||
|
||||
export let status: string;
|
||||
</script>
|
||||
|
||||
<Pill
|
||||
success={status === 'sent'}
|
||||
info={status === 'scheduled'}
|
||||
button={status == 'failed'}
|
||||
on:click>
|
||||
{#if status === 'sent'}
|
||||
<span class="icon-check-circle" aria-hidden="true"></span>
|
||||
{:else if status === 'scheduled'}
|
||||
<span class="icon-clock" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="text u-trim">
|
||||
{status}
|
||||
</span>
|
||||
</Pill>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script context="module" lang="ts">
|
||||
export enum Providers {
|
||||
Twilio = 'twilio',
|
||||
Msg91 = 'msg91',
|
||||
Telesign = 'telesign',
|
||||
Textmagic = 'textmagic',
|
||||
Vonage = 'vonage',
|
||||
Mailgun = 'mailgun',
|
||||
Sendgrid = 'sendgrid',
|
||||
FCM = 'fcm',
|
||||
APNS = 'apns'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { app } from '$lib/stores/app';
|
||||
|
||||
export let provider: Providers | string;
|
||||
export let name: string = '';
|
||||
export let noIcon = false;
|
||||
export let size: 's' | 'm' | 'l' = 'm';
|
||||
|
||||
let icon = '';
|
||||
let displayName = name || provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||
|
||||
let textSize = '1.25rem';
|
||||
switch (size) {
|
||||
case 's':
|
||||
textSize = '1rem';
|
||||
break;
|
||||
case 'l':
|
||||
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.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 === ''}
|
||||
Invalid provider
|
||||
{:else}
|
||||
<div class="u-inline-flex u-cross-center u-gap-8">
|
||||
{#if !noIcon}
|
||||
<div
|
||||
class="avatar"
|
||||
class:is-size-large={size === 'l'}
|
||||
class:is-size-small={size === 's'}>
|
||||
<img
|
||||
style:--p-text-size={textSize}
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${icon}.svg`}
|
||||
alt={displayName} />
|
||||
</div>
|
||||
{/if}
|
||||
<slot>
|
||||
{displayName}
|
||||
</slot>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script context="module" lang="ts">
|
||||
export enum ProviderTypes {
|
||||
Email = 'email',
|
||||
Sms = 'sms',
|
||||
Push = 'push'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Models } from "@appwrite.io/console";
|
||||
|
||||
export let type: ProviderTypes | Models.Provider['type'];
|
||||
export let noIcon = false;
|
||||
export let size: 's' | 'm' | 'l' = 'm';
|
||||
|
||||
let icon = '';
|
||||
let text = '';
|
||||
|
||||
switch (type) {
|
||||
case ProviderTypes.Email:
|
||||
icon = 'icon-mail';
|
||||
text = 'Email';
|
||||
break;
|
||||
case ProviderTypes.Sms:
|
||||
icon = 'icon-annotation';
|
||||
text = 'SMS';
|
||||
break;
|
||||
case ProviderTypes.Push:
|
||||
icon = 'icon-device-mobile';
|
||||
text = 'Push';
|
||||
break;
|
||||
}
|
||||
|
||||
let textSize = '1.25rem';
|
||||
switch (size) {
|
||||
case 's':
|
||||
textSize = '1rem';
|
||||
break;
|
||||
case 'l':
|
||||
textSize = '1.5rem';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if text === ''}
|
||||
Invalid provider type
|
||||
{:else}
|
||||
<div class="u-inline-flex u-cross-center u-gap-8">
|
||||
{#if !noIcon}
|
||||
<div
|
||||
class="avatar"
|
||||
class:is-size-large={size === 'l'}
|
||||
class:is-size-small={size === 's'}>
|
||||
<span class={icon} style:font-size={textSize} aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot>
|
||||
{text}
|
||||
</slot>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Empty,
|
||||
EmptySearch,
|
||||
SearchQuery,
|
||||
PaginationWithLimit,
|
||||
Heading,
|
||||
ViewSelector,
|
||||
EmptyFilter
|
||||
} from '$lib/components';
|
||||
import { Container } from '$lib/layout';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { Filters, hasPageQueries } from '$lib/components/filters';
|
||||
import CreateProviderDropdown from './createProviderDropdown.svelte';
|
||||
import Table from './table.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let showCreateDropdownMobile = false;
|
||||
let showCreateDropdownDesktop = false;
|
||||
let showCreateDropdownEmpty = false;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<div class="u-flex u-flex-vertical">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<Heading tag="h2" size="5">Providers</Heading>
|
||||
<div class="is-only-mobile">
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownMobile} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: fix width of search input in mobile -->
|
||||
<SearchQuery search={data.search} placeholder="Search provider">
|
||||
<div class="u-flex u-gap-16 is-not-mobile">
|
||||
<Filters query={data.query} {columns} />
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownDesktop} />
|
||||
</div>
|
||||
</SearchQuery>
|
||||
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<ViewSelector
|
||||
view={data.view}
|
||||
{columns}
|
||||
hideView
|
||||
allowNoColumns
|
||||
showColsTextMobile />
|
||||
</div>
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<!-- TODO: fix width -->
|
||||
<Filters query={data.query} {columns} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.providers.total}
|
||||
<Table {data} />
|
||||
|
||||
<PaginationWithLimit
|
||||
name="Providers"
|
||||
limit={data.limit}
|
||||
offset={data.offset}
|
||||
total={data.providers.total} />
|
||||
{:else if $hasPageQueries}
|
||||
<EmptyFilter resource="providers" />
|
||||
{:else if data.search && data.search != 'empty'}
|
||||
<EmptySearch>
|
||||
<div class="u-text-center">
|
||||
<b>Sorry, we couldn't find '{data.search}'</b>
|
||||
<p>There are no providers that match your search.</p>
|
||||
</div>
|
||||
<Button secondary href={`/console/project-${$page.params.project}/messaging/providers`}>
|
||||
Clear search
|
||||
</Button>
|
||||
</EmptySearch>
|
||||
{:else}
|
||||
<!-- TODO: Update docs links -->
|
||||
<Empty single target="provider">
|
||||
<div class="u-text-center">
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
Create your first provider to get started.
|
||||
</Heading>
|
||||
<p class="body-text-2 u-bold u-margin-block-start-4">
|
||||
Need a hand? Learn more in our documentation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex u-flex-wrap u-gap-16 u-main-center">
|
||||
<Button
|
||||
external
|
||||
href="https://appwrite.io/docs/references/cloud/client-web/providers"
|
||||
text
|
||||
event="empty_documentation"
|
||||
ariaLabel={`create provider`}>
|
||||
Documentation
|
||||
</Button>
|
||||
<CreateProviderDropdown bind:showCreateDropdown={showCreateDropdownEmpty}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showCreateDropdownEmpty = !showCreateDropdownEmpty)}
|
||||
event="create_provider">
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
</CreateProviderDropdown>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</Container>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import {
|
||||
View,
|
||||
getLimit,
|
||||
getPage,
|
||||
getQuery,
|
||||
getSearch,
|
||||
getView,
|
||||
pageToOffset
|
||||
} from '$lib/helpers/load';
|
||||
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
|
||||
import { queries, queryParamToMap } from '$lib/components/filters';
|
||||
|
||||
export const load = async ({ depends, url, route }) => {
|
||||
depends(Dependencies.MESSAGING_PROVIDERS);
|
||||
|
||||
const page = getPage(url);
|
||||
const search = getSearch(url);
|
||||
const view = getView(url, route, View.Grid);
|
||||
const limit = getLimit(url, route, PAGE_LIMIT);
|
||||
const offset = pageToOffset(page, limit);
|
||||
const query = getQuery(url);
|
||||
|
||||
const parsedQueries = queryParamToMap(query || '[]');
|
||||
queries.set(parsedQueries);
|
||||
|
||||
// TODO: get rid of demo data
|
||||
let providers: { providers: Models.Provider[]; total: number } = { providers: [], total: 0 };
|
||||
const params = {
|
||||
queries: [
|
||||
Query.limit(limit),
|
||||
Query.offset(offset),
|
||||
Query.orderDesc(''),
|
||||
...parsedQueries.values()
|
||||
]
|
||||
};
|
||||
|
||||
if (search) {
|
||||
params['search'] = search;
|
||||
}
|
||||
|
||||
const response = await sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(sdk.forProject.client.config.endpoint + '/messaging/providers'),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
params
|
||||
);
|
||||
|
||||
providers = response;
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
search,
|
||||
query,
|
||||
page,
|
||||
view,
|
||||
providers
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
<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 { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { project } from '../../store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { provider, providerParams } from './wizard/store';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { Providers } from '../provider.svelte';
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let response = { $id: '', name: '' };
|
||||
const providerId = $providerParams[$provider].providerId || ID.unique();
|
||||
switch ($provider) {
|
||||
case Providers.Twilio:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/twilio'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
accountSid: $providerParams[$provider].accountSid,
|
||||
authToken: $providerParams[$provider].authToken,
|
||||
from: $providerParams[$provider].from
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/msg91'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
senderId: $providerParams[$provider].senderId,
|
||||
authKey: $providerParams[$provider].authKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/telesign'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
password: $providerParams[$provider].password
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/textmagic'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/vonage'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
apiSecret: $providerParams[$provider].apiSecret
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/mailgun'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
isEuRegion: $providerParams[$provider].isEuRegion,
|
||||
fromEmail: $providerParams[$provider].fromEmail,
|
||||
fromName: $providerParams[$provider].fromName,
|
||||
replyToEmail: $providerParams[$provider].replyToEmail,
|
||||
replyToName: $providerParams[$provider].replyToName,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
domain: $providerParams[$provider].domain
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/sendgrid'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
fromEmail: $providerParams[$provider].fromEmail,
|
||||
fromName: $providerParams[$provider].fromName,
|
||||
replyToEmail: $providerParams[$provider].replyToEmail,
|
||||
replyToName: $providerParams[$provider].replyToName,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.FCM:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(sdk.forProject.client.config.endpoint + '/messaging/providers/fcm'),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
serverKey: $providerParams[$provider].serverKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.APNS:
|
||||
response = await sdk.forProject.client.call(
|
||||
'POST',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/apns'
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
authKey: $providerParams[$provider].authKey,
|
||||
authKeyId: $providerParams[$provider].authKeyId,
|
||||
teamId: $providerParams[$provider].teamId,
|
||||
bundleId: $providerParams[$provider].bundleId
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
wizard.hide();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${response.name} has been created`
|
||||
});
|
||||
trackEvent(Submit.MessagingProviderCreate, {
|
||||
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.MessagingProviderCreate);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
console.log('destroy');
|
||||
});
|
||||
|
||||
const stepsComponents: WizardStepsType = new Map();
|
||||
stepsComponents.set(1, {
|
||||
label: 'Proivder',
|
||||
component: Provider
|
||||
});
|
||||
stepsComponents.set(2, {
|
||||
label: 'Configure',
|
||||
component: Configure
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wizard title="Create provider" steps={stepsComponents} on:finish={create} />
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { providers } from './store';
|
||||
import Create from './create.svelte';
|
||||
import { providerType, provider } from './wizard/store';
|
||||
import { ProviderTypes } from '../providerType.svelte';
|
||||
import { Providers } from '../provider.svelte';
|
||||
|
||||
export let showCreateDropdown = false;
|
||||
|
||||
const isValueOfStringEnum = <T extends Record<string, string>>(
|
||||
enumType: T,
|
||||
value: string
|
||||
): value is T[keyof T] => Object.values<string>(enumType).includes(value);
|
||||
</script>
|
||||
|
||||
<DropList bind:show={showCreateDropdown} scrollable placement="bottom-end">
|
||||
<slot>
|
||||
<Button on:click={() => (showCreateDropdown = !showCreateDropdown)} event="create_provider">
|
||||
<span class="icon-plus" aria-hidden="true" />
|
||||
<span class="text">Create provider</span>
|
||||
</Button>
|
||||
</slot>
|
||||
<svelte:fragment slot="list">
|
||||
{#each Object.entries(providers) as [type, option]}
|
||||
<DropListItem
|
||||
icon={option.icon}
|
||||
on:click={() => {
|
||||
if (
|
||||
type !== ProviderTypes.Email &&
|
||||
type !== ProviderTypes.Sms &&
|
||||
type !== ProviderTypes.Push
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
const p = Object.keys(providers[type].providers).shift();
|
||||
if (p && isValueOfStringEnum(Providers, p)) {
|
||||
$provider = p;
|
||||
}
|
||||
showCreateDropdown = false;
|
||||
wizard.start(Create);
|
||||
}}>
|
||||
{option.name}
|
||||
</DropListItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<svelte:head>
|
||||
<title>Provider - Appwrite</title>
|
||||
</svelte:head>
|
||||
|
||||
<slot />
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import Breadcrumbs from './breadcrumbs.svelte';
|
||||
import Header from './header.svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: LayoutLoad = async ({ params, depends }) => {
|
||||
depends(Dependencies.MESSAGING_PROVIDER);
|
||||
|
||||
const response = await sdk.forProject.client.call(
|
||||
'GET',
|
||||
new URL(sdk.forProject.client.config.endpoint + '/messaging/providers/' + params.provider),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
return {
|
||||
header: Header,
|
||||
breadcrumbs: Breadcrumbs,
|
||||
provider: response
|
||||
};
|
||||
} catch (e) {
|
||||
throw error(e.code, e.message);
|
||||
}
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Container } from '$lib/layout';
|
||||
import DangerZone from './dangerZone.svelte';
|
||||
import UpdateName from './updateName.svelte';
|
||||
import UpdateStatus from './updateStatus.svelte';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
<UpdateStatus />
|
||||
<UpdateName />
|
||||
<DangerZone />
|
||||
</Container>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs } from '$lib/layout';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { provider } from './store';
|
||||
|
||||
$: breadcrumbs = [
|
||||
{
|
||||
href: `/console/organization-${$organization.$id}`,
|
||||
title: $organization.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}`,
|
||||
title: $project.name
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}/messaging`,
|
||||
title: 'Messaging'
|
||||
},
|
||||
{
|
||||
href: `/console/project-${$project.$id}/messaging/providers/provider-${$provider?.$id}`,
|
||||
title: $provider?.name
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Breadcrumbs {breadcrumbs} />
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" context="module">
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let showDelete = writable(false);
|
||||
|
||||
export const promptDeleteUser = (id: string) => {
|
||||
showDelete.set(true);
|
||||
goto(`/console/project-${get(project).$id}/auth/user-${id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { CardGrid, BoxAvatar, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { writable } from 'svelte/store';
|
||||
import { provider } from './store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import DeleteProvider from './deleteProvider.svelte';
|
||||
</script>
|
||||
|
||||
<CardGrid danger>
|
||||
<div>
|
||||
<Heading tag="h6" size="7">Delete provider</Heading>
|
||||
</div>
|
||||
<p>The provider's instance will be permanently deleted. This action is irreversible.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<BoxAvatar>
|
||||
<svelte:fragment slot="title">
|
||||
<h6 class="u-bold u-trim-1">{$provider.name}</h6>
|
||||
</svelte:fragment>
|
||||
<p>
|
||||
Last updated: {toLocaleDateTime($provider.$updatedAt)}
|
||||
</p>
|
||||
</BoxAvatar>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button secondary on:click={() => ($showDelete = true)} event="delete_messaging_provider"
|
||||
>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
<DeleteProvider bind:showDelete={$showDelete} />
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { provider } from './store';
|
||||
import { project } from '../../../store';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
|
||||
export let showDelete = false;
|
||||
|
||||
const deleteProvider = async () => {
|
||||
try {
|
||||
await sdk.forProject.client.call(
|
||||
'DELETE',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint + '/messaging/providers/' + $provider.$id
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
showDelete = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${$provider.name} has been deleted`
|
||||
});
|
||||
trackEvent(Submit.MessagingProviderDelete);
|
||||
await goto(`${base}/console/project-${$page.params.project}/messaging/providers`);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingProviderDelete);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Delete provider"
|
||||
bind:show={showDelete}
|
||||
onSubmit={deleteProvider}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p data-private>
|
||||
Are you sure you want to delete <b>{$provider.name}</b> from '{$project.name}'?
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
|
||||
<Button secondary submit>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Id } from '$lib/components';
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { provider } from './store';
|
||||
|
||||
const projectId = $page.params.project;
|
||||
</script>
|
||||
|
||||
<Cover>
|
||||
<svelte:fragment slot="header">
|
||||
<CoverTitle href={`/console/project-${projectId}/messaging/providers`}>
|
||||
{$provider?.name ? $provider?.name : '-'}
|
||||
</CoverTitle>
|
||||
<Id value={$provider?.$id} event="provider">{$provider?.$id}</Id>
|
||||
</svelte:fragment>
|
||||
</Cover>
|
||||
@@ -0,0 +1,9 @@
|
||||
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
|
||||
);
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { provider } from './store';
|
||||
|
||||
let providerName: string = null;
|
||||
onMount(async () => {
|
||||
providerName ??= $provider.name;
|
||||
});
|
||||
|
||||
async function updateName() {
|
||||
// TODO: switch on provider and update name
|
||||
// try {
|
||||
// await sdk.forProject.users.updateName($provider.$id, providerName);
|
||||
// await invalidate(Dependencies.USER);
|
||||
// addNotification({
|
||||
// message: 'Name has been updated',
|
||||
// type: 'success'
|
||||
// });
|
||||
// trackEvent(Submit.UserUpdateName);
|
||||
// } catch (error) {
|
||||
// addNotification({
|
||||
// message: error.message,
|
||||
// type: 'error'
|
||||
// });
|
||||
// trackError(error, Submit.UserUpdateName);
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form onSubmit={updateName}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Name</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<ul data-private>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter name"
|
||||
autocomplete={false}
|
||||
bind:value={providerName} />
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<Button disabled={providerName === $provider.name} submit>Update</Button>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
</Form>
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
<script lang="ts">
|
||||
import { CardGrid, Heading } from '$lib/components';
|
||||
import { Button, InputSwitch } from '$lib/elements/forms';
|
||||
import { toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { onMount } from 'svelte';
|
||||
import { provider } from './store';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import Provider, { Providers } from '../../provider.svelte';
|
||||
import ProviderType from '../../providerType.svelte';
|
||||
import { provider as wizardProvider, providerType, providerParams } from '../wizard/store';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Update from '../update.svelte';
|
||||
|
||||
let enabled: boolean = null;
|
||||
|
||||
onMount(() => {
|
||||
enabled ??= $provider.enabled;
|
||||
});
|
||||
|
||||
function configure() {
|
||||
$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['from'],
|
||||
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['from']
|
||||
};
|
||||
break;
|
||||
case Providers.FCM:
|
||||
$providerParams[$wizardProvider] = {
|
||||
providerId: $provider.$id,
|
||||
name: $provider.name,
|
||||
enabled: $provider.enabled,
|
||||
serverKey: $provider.credentials['serverKey']
|
||||
};
|
||||
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) {
|
||||
case Providers.Twilio:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/twilio/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/msg91/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/telesign/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/textmagic/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/vonage/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/mailgun/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/sendgrid/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.FCM:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/fcm/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.APNS:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
sdk.forProject.client.config.endpoint +
|
||||
'/messaging/providers/apns/' +
|
||||
providerId
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
enabled: enabled
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
await invalidate(Dependencies.MESSAGING_PROVIDER);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${response.name} has been ${enabled ? 'enabled' : 'disabled'}`
|
||||
});
|
||||
trackEvent(Submit.MessagingProviderUpdate, {
|
||||
provider: $provider
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingProviderUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="u-flex u-main-space-between">
|
||||
<div data-private>
|
||||
<ul>
|
||||
<InputSwitch
|
||||
id="enabled"
|
||||
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>
|
||||
</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()}
|
||||
>Update</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
@@ -0,0 +1,418 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { Providers } from '../provider.svelte';
|
||||
import { ProviderTypes } from '../providerType.svelte';
|
||||
|
||||
export let showCreate = writable(false);
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Provider ID', type: 'string', show: true },
|
||||
{ id: 'name', title: 'Name', type: 'string', show: true },
|
||||
{ id: 'provider', title: 'Provider', type: 'string', show: true },
|
||||
{ id: 'type', title: 'Type', type: 'string', show: true },
|
||||
{ id: 'enabled', title: 'Status', type: 'boolean', show: true }
|
||||
]);
|
||||
|
||||
type ProvidersMap = {
|
||||
[key in ProviderTypes]: {
|
||||
name: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
providers: {
|
||||
[key in Providers]?: {
|
||||
imageIcon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
configure: {
|
||||
label: string;
|
||||
name: string;
|
||||
type: 'text' | 'phone' | 'email' | 'domain' | 'file' | 'switch';
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
popover?: string[];
|
||||
allowedFileExtensions?: string[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const providers: ProvidersMap = {
|
||||
[ProviderTypes.Push]: {
|
||||
name: 'Push notification',
|
||||
text: 'notifications',
|
||||
icon: 'device-mobile',
|
||||
providers: {
|
||||
[Providers.FCM]: {
|
||||
imageIcon: 'firebase',
|
||||
title: 'FCM',
|
||||
description: 'Firebase Cloud Messaging',
|
||||
configure: [
|
||||
{
|
||||
label: 'Server key (.json file)',
|
||||
name: 'serverKey',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['json'],
|
||||
placeholder: 'Enter server key',
|
||||
popover: [
|
||||
'<b>How to get the FCM server key?</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.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.APNS]: {
|
||||
imageIcon: 'apple',
|
||||
title: 'APNS',
|
||||
description: 'Apple Push Notification Service',
|
||||
configure: [
|
||||
{
|
||||
label: 'Team ID',
|
||||
name: 'teamId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter team ID',
|
||||
popover: [
|
||||
'<b>How to get the team ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Membership details -> Team ID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Bundle ID',
|
||||
name: 'bundleId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter bundle ID',
|
||||
popover: [
|
||||
'<b>How to get the bundle ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Identifiers.</b>',
|
||||
`<a
|
||||
href="/images/apns-bundle-id.png"
|
||||
class="file-preview is-with-image"
|
||||
target="_blank"
|
||||
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>`
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Authentication key ID',
|
||||
name: 'authKeyId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter key ID',
|
||||
popover: [
|
||||
'<b>How to get the auth key ID?</b>',
|
||||
'Head to <b>Apple Developer Member Center -> Certificates, Identifiers & Profiles -> Keys.</b>',
|
||||
'Click on your key to view details.'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Auth key (.p8 file)',
|
||||
name: 'authKey',
|
||||
type: 'file',
|
||||
allowedFileExtensions: ['p8'],
|
||||
popover: [
|
||||
'<b>How to get the authentication key?</b>',
|
||||
'Head to <b>Apple Developer Member Center</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.'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// [Providers.MQTT]: {
|
||||
// imageIcon: 'mqtt',
|
||||
// title: 'MQTT',
|
||||
// description: 'Message Queuing Telemetry Transport'
|
||||
// }
|
||||
}
|
||||
},
|
||||
[ProviderTypes.Email]: {
|
||||
name: 'Email',
|
||||
text: 'emails',
|
||||
icon: 'mail',
|
||||
providers: {
|
||||
[Providers.Mailgun]: {
|
||||
imageIcon: 'mailgun',
|
||||
title: 'Mailgun',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Mailgun.',
|
||||
'Head to <b>Profile -> API Security -> Add new key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Domain',
|
||||
name: 'domain',
|
||||
type: 'domain',
|
||||
placeholder: 'Enter domain',
|
||||
popover: [
|
||||
'<b>How to create a domain?</b>',
|
||||
'Head to <b>Sending -> Domains -> Add new domain.</b>',
|
||||
'Follow <b>Mailgun instructions</b> to verify the domain name.'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'EU region',
|
||||
name: 'isEuRegion',
|
||||
type: 'switch',
|
||||
description:
|
||||
'Enable the EU region setting if your domain is within the European Union.'
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Sendgrid]: {
|
||||
imageIcon: 'sendgrid',
|
||||
title: 'Sendgrid',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Mailgun.',
|
||||
'Head to <b>Profile -> API Security -> Add new key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender email',
|
||||
name: 'fromEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Sender name',
|
||||
name: 'fromName',
|
||||
type: 'text',
|
||||
placeholder: 'Enter name'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to email',
|
||||
name: 'replyToEmail',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
},
|
||||
{
|
||||
label: 'Reply-to name',
|
||||
name: 'replyToName',
|
||||
type: 'text',
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
[ProviderTypes.Sms]: {
|
||||
name: 'SMS',
|
||||
text: 'SMS',
|
||||
icon: 'annotation',
|
||||
providers: {
|
||||
[Providers.Twilio]: {
|
||||
imageIcon: 'twilio',
|
||||
title: 'Twilio',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Account SID',
|
||||
name: 'accountSid',
|
||||
type: 'text',
|
||||
placeholder: 'Enter Account SID',
|
||||
popover: [
|
||||
'<b>How to get the Account SID?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> Account SID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Auth token',
|
||||
name: 'authToken',
|
||||
type: 'text',
|
||||
placeholder: 'Enter Auth token',
|
||||
popover: [
|
||||
'<b>How to get the Auth token?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> Auth Token.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone',
|
||||
popover: [
|
||||
'<b>How to get sender number?</b>',
|
||||
'Head to <b>Twilio console -> Account info -> My Twilio phone number.</b>',
|
||||
'If you have multiple Twilio phone numbers, you can select one as the default number.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Msg91]: {
|
||||
imageIcon: 'msg91',
|
||||
title: 'MSG91',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Auth key',
|
||||
name: 'authKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter auth key',
|
||||
popover: [
|
||||
'<b>How to get the Auth key?</b>',
|
||||
'Create an account in MSG91.',
|
||||
'Click to open the <b>Username dropdown -> Authkey -> Verify your mobile number -> Create Authkey.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender ID',
|
||||
name: 'senderId',
|
||||
type: 'text',
|
||||
placeholder: 'Enter sender ID',
|
||||
popover: [
|
||||
'<b>How to create a Sender ID?</b>',
|
||||
'Head to <b>MSG91 dashboard -> SMS -> Sender ID -> Create sender ID.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Telesign]: {
|
||||
imageIcon: 'telesign',
|
||||
title: 'Telesign',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'Enter username'
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
placeholder: 'Enter password'
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Textmagic]: {
|
||||
imageIcon: 'textmagic',
|
||||
title: 'Textmagic',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Textmagic.',
|
||||
'Head to <b>TextMagic dashboard -> API Settings -> Add new API key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Username',
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
placeholder: 'Enter username'
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
[Providers.Vonage]: {
|
||||
imageIcon: 'vonage',
|
||||
title: 'Vonage',
|
||||
description: '',
|
||||
configure: [
|
||||
{
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key',
|
||||
popover: [
|
||||
'<b>How to get the API key?</b>',
|
||||
'Create an account in Vonage.',
|
||||
'Head to <b>Vonage dashboard and copy the API key.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'API secret',
|
||||
name: 'apiSecret',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API secret',
|
||||
popover: [
|
||||
'<b>How to get the API secret?</b>',
|
||||
'Head to <b>Vonage dashboard and copy the API secret.</b>'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Sender number',
|
||||
name: 'from',
|
||||
type: 'phone',
|
||||
placeholder: 'Enter phone'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { FloatingActionBar, Id, Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellCheck,
|
||||
TableCellHead,
|
||||
TableCellHeadCheck,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRowLink,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { PageData } from './$types';
|
||||
import { columns } from './store';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import Provider from '../provider.svelte';
|
||||
import ProviderType from '../providerType.svelte';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let selectedIds: string[] = [];
|
||||
let showDelete = false;
|
||||
let deleting = false;
|
||||
|
||||
async function handleDelete() {
|
||||
showDelete = false;
|
||||
|
||||
function deleteProvider(providerId: string) {
|
||||
return sdk.forProject.client.call(
|
||||
'DELETE',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const promises = selectedIds.map((id) => deleteProvider(id));
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
trackEvent(Submit.MessagingProviderDelete, {
|
||||
total: selectedIds.length
|
||||
});
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: `${selectedIds.length} provider${
|
||||
selectedIds.length > 1 ? 's' : ''
|
||||
} deleted`
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.MessagingProviderDelete);
|
||||
} finally {
|
||||
invalidate(Dependencies.MESSAGING_PROVIDERS);
|
||||
selectedIds = [];
|
||||
showDelete = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TableScroll>
|
||||
<TableHeader>
|
||||
<TableCellHeadCheck
|
||||
bind:selected={selectedIds}
|
||||
pageItemsIds={data.providers.providers.map((d) => d.$id)} />
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
<TableCellHead width={column.width}>{column.title}</TableCellHead>
|
||||
{/if}
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each data.providers.providers as provider (provider.$id)}
|
||||
<TableRowLink
|
||||
href={`${base}/console/project-${$project.$id}/messaging/providers/provider-${provider.$id}`}>
|
||||
<TableCellCheck bind:selectedIds id={provider.$id} />
|
||||
{#each $columns as column}
|
||||
{#if column.show}
|
||||
{#if column.id === '$id'}
|
||||
{#key $columns}
|
||||
<TableCell title={column.title} width={column.width}>
|
||||
<Id value={provider.$id}>{provider.$id}</Id>
|
||||
</TableCell>
|
||||
{/key}
|
||||
{:else if column.id === 'provider'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<Provider provider={provider.provider} size="s" />
|
||||
</TableCellText>
|
||||
{:else if column.id === 'type'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<ProviderType type={provider.type} size="s" />
|
||||
</TableCellText>
|
||||
{:else if column.id === 'enabled'}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
<Pill success={provider.enabled}>
|
||||
{#if provider.enabled}
|
||||
<span class="icon-check-circle" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="text u-trim">
|
||||
{provider.enabled ? 'enabled' : 'disabled'}
|
||||
</span>
|
||||
</Pill>
|
||||
</TableCellText>
|
||||
{:else}
|
||||
<TableCellText title={column.title} width={column.width}>
|
||||
{provider[column.id]}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</TableRowLink>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
|
||||
<FloatingActionBar show={selectedIds.length > 0}>
|
||||
<div class="u-flex u-cross-center u-main-space-between actions">
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<span class="indicator body-text-2 u-bold">{selectedIds.length}</span>
|
||||
<p>
|
||||
<span class="is-only-desktop">
|
||||
{selectedIds.length > 1 ? 'providers' : 'provider'}
|
||||
</span>
|
||||
selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-cross-center u-gap-8">
|
||||
<Button text on:click={() => (selectedIds = [])}>Cancel</Button>
|
||||
<Button secondary on:click={() => (showDelete = true)}>
|
||||
<p>Delete</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingActionBar>
|
||||
|
||||
<Modal
|
||||
title="Delete providers"
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
bind:show={showDelete}
|
||||
onSubmit={handleDelete}
|
||||
headerDivider={false}
|
||||
closable={!deleting}>
|
||||
<p class="text" data-private>
|
||||
Are you sure you want to delete <b>{selectedIds.length}</b>
|
||||
{selectedIds.length > 1 ? 'providers' : 'provider'}?
|
||||
</p>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (showDelete = false)} disabled={deleting}>Cancel</Button>
|
||||
<Button secondary submit disabled={deleting}>Delete</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -0,0 +1,244 @@
|
||||
<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';
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
let response = { $id: '', name: '' };
|
||||
const providerId = $providerParams[$provider].providerId;
|
||||
switch ($provider) {
|
||||
case Providers.Twilio:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/twilio/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
accountSid: $providerParams[$provider].accountSid,
|
||||
authToken: $providerParams[$provider].authToken,
|
||||
from: $providerParams[$provider].from
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/msg91/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
senderId: $providerParams[$provider].senderId,
|
||||
authKey: $providerParams[$provider].authKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/telesign/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
password: $providerParams[$provider].password
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/textmagic/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
username: $providerParams[$provider].username,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/vonage/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
from: $providerParams[$provider].from,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
apiSecret: $providerParams[$provider].apiSecret
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/mailgun/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
isEuRegion: $providerParams[$provider].isEuRegion,
|
||||
fromEmail: $providerParams[$provider].fromEmail,
|
||||
fromName: $providerParams[$provider].fromName,
|
||||
replyToEmail: $providerParams[$provider].replyToEmail,
|
||||
replyToName: $providerParams[$provider].replyToName,
|
||||
apiKey: $providerParams[$provider].apiKey,
|
||||
domain: $providerParams[$provider].domain
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/sendgrid/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
fromEmail: $providerParams[$provider].fromEmail,
|
||||
fromName: $providerParams[$provider].fromName,
|
||||
replyToEmail: $providerParams[$provider].replyToEmail,
|
||||
replyToName: $providerParams[$provider].replyToName,
|
||||
apiKey: $providerParams[$provider].apiKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.FCM:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/fcm')/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
serverKey: $providerParams[$provider].serverKey
|
||||
}
|
||||
);
|
||||
break;
|
||||
case Providers.APNS:
|
||||
response = await sdk.forProject.client.call(
|
||||
'PATCH',
|
||||
new URL(
|
||||
`${sdk.forProject.client.config.endpoint}/messaging/providers/apns/${providerId}`
|
||||
),
|
||||
{
|
||||
'X-Appwrite-Project': sdk.forProject.client.config.project,
|
||||
'content-type': 'application/json',
|
||||
'X-Appwrite-Mode': 'admin'
|
||||
},
|
||||
{
|
||||
providerId: providerId,
|
||||
name: $providerParams[$provider].name,
|
||||
enabled: $providerParams[$provider].enabled,
|
||||
authKey: $providerParams[$provider].authKey,
|
||||
authKeyId: $providerParams[$provider].authKeyId,
|
||||
teamId: $providerParams[$provider].teamId,
|
||||
bundleId: $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" />
|
||||
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FormList,
|
||||
InputDomain,
|
||||
InputEmail,
|
||||
InputFile,
|
||||
InputSwitch,
|
||||
InputText
|
||||
} 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';
|
||||
|
||||
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}
|
||||
bind:value={$providerParams[$provider][input.name]}>
|
||||
<svelte:fragment slot="popover">
|
||||
{@html input.popover?.join('<br/><br/>')}
|
||||
</svelte:fragment>
|
||||
</InputText>
|
||||
{:else if input.type === 'email'}
|
||||
<InputEmail
|
||||
id={input.name}
|
||||
label={input.label}
|
||||
placeholder={input.placeholder}
|
||||
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}
|
||||
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}
|
||||
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}
|
||||
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>
|
||||
{/if}
|
||||
{/each}
|
||||
</FormList>
|
||||
|
||||
<p class="body-text-2 u-bold u-margin-block-start-48">Need a hand?</p>
|
||||
|
||||
<div
|
||||
class="u-flex u-cross-center u-main-space-between u-padding-block-16"
|
||||
style="border-block-end: solid .0625rem hsl(var(--color-border))">
|
||||
<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>
|
||||
|
||||
<div class="u-flex u-cross-center u-main-space-between u-padding-block-16">
|
||||
<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>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import { WizardStep } from '$lib/layout';
|
||||
import { providerType, provider, providerParams } from './store';
|
||||
import { CustomId, LabelCard } from '$lib/components';
|
||||
import { providers } from '../store';
|
||||
import { FormList, InputText } from '$lib/elements/forms';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Providers } from '../../provider.svelte';
|
||||
|
||||
let name = '';
|
||||
let showCustomId = false;
|
||||
let id: string = null;
|
||||
|
||||
async function beforeSubmit() {
|
||||
console.log($provider);
|
||||
|
||||
switch ($provider) {
|
||||
case Providers.Twilio:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
accountSid: '',
|
||||
authToken: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Msg91:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
from: '',
|
||||
senderId: '',
|
||||
authKey: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Telesign:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
username: '',
|
||||
password: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Textmagic:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
username: '',
|
||||
apiKey: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Vonage:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
apiKey: '',
|
||||
apiSecret: '',
|
||||
from: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Mailgun:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
isEuRegion: false,
|
||||
fromEmail: '',
|
||||
fromName: '',
|
||||
replyToEmail: '',
|
||||
replyToName: '',
|
||||
apiKey: '',
|
||||
domain: ''
|
||||
};
|
||||
break;
|
||||
case Providers.Sendgrid:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
apiKey: '',
|
||||
fromEmail: '',
|
||||
fromName: '',
|
||||
replyToEmail: '',
|
||||
replyToName: ''
|
||||
};
|
||||
break;
|
||||
case Providers.FCM:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
serverKey: ''
|
||||
};
|
||||
break;
|
||||
case Providers.APNS:
|
||||
$providerParams[$provider] = {
|
||||
providerId: id,
|
||||
name: name,
|
||||
enabled: true,
|
||||
authKey: '',
|
||||
authKeyId: '',
|
||||
teamId: '',
|
||||
bundleId: ''
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WizardStep {beforeSubmit}>
|
||||
<svelte:fragment slot="title">Provider</svelte:fragment>
|
||||
<FormList>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Provider name"
|
||||
bind:value={name}
|
||||
autofocus
|
||||
required />
|
||||
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}
|
||||
><span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Provider ID
|
||||
</span></Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Provider" bind:id autofocus={false} />
|
||||
{/if}
|
||||
<p class="u-margin-block-start-24">
|
||||
Select a provider you would like to enable for sending {providers[$providerType].text}.
|
||||
</p>
|
||||
<div class="grid-box">
|
||||
{#each Object.entries(providers[$providerType].providers) as [value, option]}
|
||||
<LabelCard
|
||||
name="provider"
|
||||
{value}
|
||||
bind:group={$provider}
|
||||
imageIcon={option.imageIcon}>
|
||||
<svelte:fragment slot="title">{option.title}</svelte:fragment>
|
||||
{#if option.description}
|
||||
{option.description}
|
||||
{/if}
|
||||
</LabelCard>
|
||||
{/each}
|
||||
</div>
|
||||
</FormList>
|
||||
</WizardStep>
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Providers } from '../../provider.svelte';
|
||||
import type { ProviderTypes } from '../../providerType.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Push providers
|
||||
*/
|
||||
|
||||
export type FCMProviderParams = ProviderParams & {
|
||||
serverKey: string;
|
||||
};
|
||||
|
||||
export type APNSProviderParams = ProviderParams & {
|
||||
authKey: string;
|
||||
authKeyId: string;
|
||||
teamId: string;
|
||||
bundleId: string;
|
||||
};
|
||||
|
||||
export type MQTTProviderParams = ProviderParams & {
|
||||
serverKey: string;
|
||||
};
|
||||
|
||||
export const providerType = writable<Models.Provider['type']>(null);
|
||||
export const provider = writable<Models.Provider['provider']>(null);
|
||||
export const providerParams = writable<{
|
||||
twilio: Partial<TwilioProviderParams>;
|
||||
msg91: Partial<Msg91ProviderParams>;
|
||||
telesign: Partial<TelesignProviderParams>;
|
||||
textmagic: Partial<TextmagicProviderParams>;
|
||||
vonage: Partial<VonageProviderParams>;
|
||||
mailgun: Partial<MailgunProviderParams>;
|
||||
sendgrid: Partial<SendgridProviderParams>;
|
||||
fcm: Partial<FCMProviderParams>;
|
||||
apns: Partial<APNSProviderParams>;
|
||||
}>({
|
||||
twilio: null,
|
||||
msg91: null,
|
||||
telesign: null,
|
||||
textmagic: null,
|
||||
vonage: null,
|
||||
mailgun: null,
|
||||
sendgrid: null,
|
||||
fcm: null,
|
||||
apns: null
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Column } from '$lib/components/viewSelector.svelte';
|
||||
|
||||
export let showCreate = writable(false);
|
||||
|
||||
export const columns = writable<Column[]>([
|
||||
{ id: '$id', title: 'Provider ID', show: true },
|
||||
{ id: 'name', title: 'Name', show: true },
|
||||
{ id: 'provider', title: 'Provider', show: true },
|
||||
{ id: 'channel', title: 'Channel', show: true },
|
||||
{ id: 'status', title: 'Status', show: true }
|
||||
]);
|
||||
|
||||
export type Instruction = {
|
||||
text: string;
|
||||
input: {
|
||||
label: string;
|
||||
name: string;
|
||||
type: 'text' | 'domain' | 'email';
|
||||
placeholder: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const providers = {
|
||||
sms: {
|
||||
name: 'SMS',
|
||||
text: 'SMS',
|
||||
icon: 'annotation',
|
||||
providers: {
|
||||
twilio: {
|
||||
imageIcon: 'twilio',
|
||||
title: 'Twilio',
|
||||
description: ''
|
||||
},
|
||||
msg91: {
|
||||
imageIcon: 'msg91',
|
||||
title: 'MSG91',
|
||||
description: ''
|
||||
},
|
||||
telesign: {
|
||||
imageIcon: 'telesign',
|
||||
title: 'Telesign',
|
||||
description: ''
|
||||
},
|
||||
textmagic: {
|
||||
imageIcon: 'textmagic',
|
||||
title: 'Textmagic',
|
||||
description: ''
|
||||
},
|
||||
vonage: {
|
||||
imageIcon: 'vonage',
|
||||
title: 'Vonage',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
email: {
|
||||
name: 'Email',
|
||||
text: 'emails',
|
||||
icon: 'mail',
|
||||
providers: {
|
||||
mailgun: {
|
||||
imageIcon: 'mailgun',
|
||||
title: 'Mailgun',
|
||||
description: '',
|
||||
initialize: [
|
||||
{
|
||||
text: 'Before you can create a Mailgun provider, you need to first create a <a href="https://www.mailgun.com/" class="link" target="_blank" rel="noopener noreferrer">Mailgun</a> account.'
|
||||
},
|
||||
{
|
||||
text: 'Head to your <b>Profile > API Security</b>.'
|
||||
},
|
||||
{
|
||||
text: '<b>Generate a key</b> and give it a name. Copy and paste it in the field below.',
|
||||
input: {
|
||||
label: 'API key',
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
placeholder: 'Enter API key'
|
||||
}
|
||||
},
|
||||
{
|
||||
// TODO: Update link to domain verification
|
||||
text: 'Head to <b>Sending > Domains</b> and click on \'Add New Domain\'. Verify your domain by following the <a href="https://www.mailgun.com/" class="link" target="_blank" rel="noopener noreferrer">instructions</a>.',
|
||||
input: {
|
||||
label: 'Base URL',
|
||||
name: 'baseUrl',
|
||||
type: 'text',
|
||||
placeholder: 'Enter base URL'
|
||||
}
|
||||
}
|
||||
],
|
||||
configure: [
|
||||
{
|
||||
text: 'Provide a display name your recipient will see when they receive your emails.',
|
||||
input: {
|
||||
label: 'From',
|
||||
name: 'from',
|
||||
type: 'text',
|
||||
placeholder: 'Enter name'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Provide an email address that will be visible to the recipient as the senders email address for this message.',
|
||||
input: {
|
||||
label: 'From email address',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Provide an email address for users to use when replying to your emails.',
|
||||
input: {
|
||||
label: 'Reply to',
|
||||
name: 'replyTo',
|
||||
type: 'email',
|
||||
placeholder: 'Enter email'
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Provide the domain as it is registered on Mailgun.',
|
||||
input: {
|
||||
label: 'Domain',
|
||||
name: 'domain',
|
||||
type: 'domain',
|
||||
placeholder: 'Enter domain'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
sendgrid: {
|
||||
imageIcon: 'sendgrid',
|
||||
title: 'Sendgrid',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
push: {
|
||||
name: 'Push notification',
|
||||
text: 'notifications',
|
||||
icon: 'device-mobile',
|
||||
providers: {
|
||||
fcm: {
|
||||
imageIcon: 'firebase',
|
||||
title: 'FCM',
|
||||
description: 'Firebase Cloud Messaging'
|
||||
},
|
||||
apns: {
|
||||
imageIcon: 'apple',
|
||||
title: 'APNS',
|
||||
description: 'Apple Push Notification Service'
|
||||
},
|
||||
mqtt: {
|
||||
imageIcon: 'mqtt',
|
||||
title: 'MQTT',
|
||||
description: 'Message Queuing Telemtry Transport'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 21 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { project } from '../store';
|
||||
|
||||
export let title = '';
|
||||
export let body = '';
|
||||
export let classes = '';
|
||||
</script>
|
||||
|
||||
<div class="phone {classes}">
|
||||
<div
|
||||
class="u-flex u-flex-vertical card u-margin-inline-start-24 u-margin-inline-end-24 u-gap-8">
|
||||
<div class="u-flex u-main-space-between header">
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
<span class="u-icon icon-bell u-line-height-1" /><span class=""
|
||||
>{$project.name}</span>
|
||||
</div>
|
||||
<div class="u-flex u-cross-center">now</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="body-text-1 u-small u-bold">{title || 'Message Title'}</p>
|
||||
<p class="body-text-2 u-x-small">
|
||||
{body || 'Enter your message in the input field on the left to see it here'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.phone {
|
||||
width: 320px;
|
||||
height: 539px;
|
||||
background-image: url('./push-notification-preview-light.svg');
|
||||
background-repeat: no-repeat;
|
||||
padding-top: 197px;
|
||||
--p-body-text-color: hsl(var(--color-neutral-120));
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 329px;
|
||||
--card-padding: 1rem;
|
||||
|
||||
.header {
|
||||
--p-body-text-color: hsl(var(--color-neutral-100));
|
||||
|
||||
.icon-bell {
|
||||
--p-body-text-color: hsl(var(--color-neutral-50));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .phone {
|
||||
background-image: url('./push-notification-preview-dark.svg');
|
||||
|
||||
.card {
|
||||
--p-card-bg-color: 240, 3%, 14%, 0.9;
|
||||
|
||||
.header {
|
||||
.icon-bell {
|
||||
--p-body-text-color: hsl(var(--color-neutral-150));
|
||||
}
|
||||
}
|
||||
|
||||
.body-text-1 {
|
||||
--p-body-text-color: #e4e4e7;
|
||||
}
|
||||
|
||||
.body-text-2 {
|
||||
--p-body-text-color: #adadb0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<svg width="320" height="529" viewBox="0 0 320 529" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_10_77" fill="white">
|
||||
<path d="M0 60C0 26.8629 26.8629 0 60 0H260C293.137 0 320 26.8629 320 60V529H0V60Z"/>
|
||||
</mask>
|
||||
<path d="M0 60C0 26.8629 26.8629 0 60 0H260C293.137 0 320 26.8629 320 60V529H0V60Z" fill="url(#paint0_linear_10_77)"/>
|
||||
<path d="M-1.25 60C-1.25 26.1726 26.1726 -1.25 60 -1.25H260C293.827 -1.25 321.25 26.1726 321.25 60H318.75C318.75 27.5533 292.447 1.25 260 1.25H60C27.5533 1.25 1.25 27.5533 1.25 60H-1.25ZM320 529H0H320ZM-1.25 529V60C-1.25 26.1726 26.1726 -1.25 60 -1.25V1.25C27.5533 1.25 1.25 27.5533 1.25 60V529H-1.25ZM260 -1.25C293.827 -1.25 321.25 26.1726 321.25 60V529H318.75V60C318.75 27.5533 292.447 1.25 260 1.25V-1.25Z" fill="url(#paint1_linear_10_77)" fill-opacity="0.08" mask="url(#path-1-inside-1_10_77)"/>
|
||||
<rect x="7" y="7" width="305" height="522" rx="57" fill="url(#paint2_linear_10_77)"/>
|
||||
<mask id="path-4-inside-2_10_77" fill="white">
|
||||
<path d="M7 65C7 32.9675 32.9675 7 65 7H254C286.033 7 312 32.9675 312 65V139H7V65Z"/>
|
||||
</mask>
|
||||
<path d="M7 65C7 32.9675 32.9675 7 65 7H254C286.033 7 312 32.9675 312 65V139H7V65Z" fill="#17171A"/>
|
||||
<path d="M7 7H312H7ZM312 140H7V138H312V140ZM7 139V7V139ZM312 7V139V7Z" fill="#3C3C43" fill-opacity="0.06" mask="url(#path-4-inside-2_10_77)"/>
|
||||
<path d="M25 95.939C25 96.3208 25.146 96.6465 25.4492 96.9385L34.209 105.507C34.4448 105.754 34.7593 105.878 35.1187 105.878C35.8486 105.878 36.4214 105.316 36.4214 104.575C36.4214 104.216 36.2754 103.89 36.0283 103.643L28.1333 95.939L36.0283 88.2349C36.2754 87.9766 36.4214 87.6509 36.4214 87.2915C36.4214 86.5615 35.8486 86 35.1187 86C34.7593 86 34.4448 86.1235 34.209 86.3706L25.4492 94.9395C25.146 95.2314 25.0112 95.5571 25 95.939Z" fill="#007AFF"/>
|
||||
<rect x="135.5" y="63" width="48" height="48" rx="24" fill="url(#paint3_linear_10_77)"/>
|
||||
<path d="M274.674 104.412H285.368C287.865 104.412 289.359 102.959 289.359 100.462V99.0597L293.227 102.334C293.636 102.672 294.086 102.907 294.506 102.907C295.406 102.907 296 102.242 296 101.291V90.7095C296 89.7578 295.406 89.0926 294.506 89.0926C294.086 89.0926 293.636 89.328 293.227 89.6657L289.359 92.9403V91.5281C289.359 89.0415 287.865 87.5884 285.368 87.5884H274.674C272.29 87.5884 270.683 89.0415 270.683 91.5281V100.462C270.683 102.959 272.177 104.412 274.674 104.412ZM274.961 102.866C273.283 102.866 272.331 101.997 272.331 100.226V91.7737C272.331 89.9932 273.283 89.1233 274.961 89.1233H285.081C286.749 89.1233 287.711 89.9932 287.711 91.7737V100.226C287.711 101.997 286.749 102.866 285.081 102.866H274.961ZM293.994 100.943L289.359 97.1154V94.8846L293.994 91.0574C294.086 90.9858 294.148 90.9346 294.24 90.9346C294.363 90.9346 294.414 91.0369 294.414 91.1802V100.82C294.414 100.963 294.363 101.055 294.24 101.055C294.148 101.055 294.086 101.004 293.994 100.943Z" fill="#007AFF"/>
|
||||
<path d="M59.2984 28.4162C59.72 28.4193 60.1355 28.4962 60.5449 28.647C60.9542 28.7978 61.3235 29.044 61.6528 29.3857C61.9852 29.7273 62.2499 30.1889 62.4468 30.7706C62.6469 31.3492 62.7485 32.0694 62.7515 32.9311C62.7515 33.759 62.6684 34.4961 62.5022 35.1424C62.336 35.7856 62.0975 36.3288 61.7867 36.772C61.4789 37.2152 61.105 37.5522 60.6649 37.783C60.2248 38.0138 59.7293 38.1293 59.1784 38.1293C58.6152 38.1293 58.115 38.0185 57.678 37.7969C57.241 37.5753 56.8855 37.2691 56.6116 36.8782C56.3377 36.4843 56.1669 36.0318 56.0992 35.521H57.5072C57.5995 35.9272 57.7873 36.2565 58.0704 36.5089C58.3566 36.7582 58.726 36.8828 59.1784 36.8828C59.8708 36.8828 60.411 36.5812 60.7988 35.978C61.1865 35.3717 61.382 34.5253 61.3851 33.4389H61.3112C61.1511 33.7036 60.9511 33.9313 60.711 34.1222C60.4741 34.313 60.2078 34.4607 59.9124 34.5653C59.6169 34.67 59.3015 34.7223 58.966 34.7223C58.4213 34.7223 57.9258 34.5884 57.4795 34.3207C57.0333 34.0529 56.6778 33.6851 56.4131 33.2173C56.1484 32.7495 56.0161 32.2156 56.0161 31.6154C56.0161 31.0183 56.1515 30.4767 56.4223 29.9904C56.6962 29.5041 57.0779 29.1194 57.5672 28.8363C58.0596 28.5501 58.6367 28.41 59.2984 28.4162ZM59.303 29.6165C58.9429 29.6165 58.6182 29.7057 58.3289 29.8842C58.0427 30.0597 57.8165 30.2982 57.6503 30.5998C57.4841 30.8983 57.401 31.2307 57.401 31.5969C57.401 31.9632 57.4811 32.2956 57.6411 32.5941C57.8042 32.8896 58.0258 33.125 58.3059 33.3004C58.589 33.4728 58.9122 33.5589 59.2753 33.5589C59.5462 33.5589 59.7985 33.5066 60.0324 33.402C60.2663 33.2973 60.471 33.1527 60.6464 32.968C60.8218 32.7803 60.9588 32.5679 61.0573 32.331C61.1558 32.094 61.205 31.8447 61.205 31.5831C61.205 31.2353 61.1219 30.9122 60.9557 30.6136C60.7926 30.3151 60.5679 30.075 60.2817 29.8935C59.9955 29.7088 59.6693 29.6165 59.303 29.6165ZM65.0271 36.8274C64.7748 36.8274 64.5578 36.7382 64.3762 36.5597C64.1946 36.3781 64.1038 36.1596 64.1038 35.9041C64.1038 35.6518 64.1946 35.4363 64.3762 35.2578C64.5578 35.0762 64.7748 34.9854 65.0271 34.9854C65.2795 34.9854 65.4965 35.0762 65.6781 35.2578C65.8596 35.4363 65.9504 35.6518 65.9504 35.9041C65.9504 36.0734 65.9074 36.2288 65.8212 36.3704C65.7381 36.5089 65.6273 36.6197 65.4888 36.7028C65.3503 36.7859 65.1964 36.8274 65.0271 36.8274ZM65.0271 32.0909C64.7748 32.0909 64.5578 32.0017 64.3762 31.8232C64.1946 31.6416 64.1038 31.4231 64.1038 31.1676C64.1038 30.9152 64.1946 30.6998 64.3762 30.5213C64.5578 30.3397 64.7748 30.2489 65.0271 30.2489C65.2795 30.2489 65.4965 30.3397 65.6781 30.5213C65.8596 30.6998 65.9504 30.9152 65.9504 31.1676C65.9504 31.3369 65.9074 31.4923 65.8212 31.6339C65.7381 31.7724 65.6273 31.8832 65.4888 31.9663C65.3503 32.0494 65.1964 32.0909 65.0271 32.0909ZM67.2624 36.1534V34.9993L71.3479 28.5455H72.2574V30.2443H71.6803L68.7581 34.87V34.9439H74.3579V36.1534H67.2624ZM71.745 38V35.8026L71.7542 35.2763V28.5455H73.1068V38H71.745ZM79.1068 28.5455V38H77.6757V29.9766H77.6203L75.3582 31.4538V30.0874L77.7172 28.5455H79.1068Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<rect x="115" y="20" width="89" height="24" rx="12" fill="#DBDBDB" fill-opacity="0.04"/>
|
||||
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M238.025 27.7143H238.875C239.344 27.7143 239.725 28.0981 239.725 28.5714V35.4286C239.725 35.902 239.344 36.2857 238.875 36.2857H238.025C237.556 36.2857 237.175 35.902 237.175 35.4286V28.5714C237.175 28.0981 237.556 27.7143 238.025 27.7143Z" fill="#F9F9F9" fill-opacity="0.18"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M234.2 29.4286H235.05C235.519 29.4286 235.9 29.8123 235.9 30.2857V35.4286C235.9 35.9019 235.519 36.2857 235.05 36.2857H234.2C233.731 36.2857 233.35 35.9019 233.35 35.4286V30.2857C233.35 29.8123 233.731 29.4286 234.2 29.4286Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M230.375 31.1429H231.225C231.694 31.1429 232.075 31.5266 232.075 32V35.4286C232.075 35.902 231.694 36.2857 231.225 36.2857H230.375C229.906 36.2857 229.525 35.902 229.525 35.4286V32C229.525 31.5266 229.906 31.1429 230.375 31.1429Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M226.55 32.4286H227.4C227.869 32.4286 228.25 32.8123 228.25 33.2857V35.4286C228.25 35.9019 227.869 36.2857 227.4 36.2857H226.55C226.081 36.2857 225.7 35.9019 225.7 35.4286V33.2857C225.7 32.8123 226.081 32.4286 226.55 32.4286Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path d="M253.116 33.6625C253.676 33.6625 254.219 33.8065 254.698 34.0812L254.893 34.1925C255.04 34.2768 255.066 34.4743 254.945 34.5921L253.288 36.2111C253.186 36.3106 253.021 36.3106 252.919 36.2111L251.272 34.6026C251.152 34.4852 251.177 34.2888 251.323 34.2038L251.515 34.092C251.999 33.8104 252.549 33.6625 253.116 33.6625Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path d="M253.116 30.6884C254.508 30.6884 255.845 31.1408 256.933 31.9785L257.087 32.0969C257.21 32.1913 257.221 32.3691 257.11 32.477L256.121 33.4435C256.029 33.5329 255.884 33.5432 255.781 33.4677L255.66 33.3798C254.925 32.8445 254.038 32.5573 253.116 32.5573C252.188 32.5573 251.297 32.8481 250.559 33.3896L250.438 33.4782C250.334 33.5543 250.189 33.5443 250.097 33.4546L249.108 32.4885C248.998 32.3808 249.008 32.2034 249.13 32.1089L249.283 31.9904C250.375 31.1452 251.717 30.6884 253.116 30.6884Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path d="M253.116 27.7143C255.325 27.7143 257.438 28.4776 259.11 29.8768L259.253 29.9962C259.368 30.0928 259.375 30.2648 259.267 30.3697L258.281 31.3331C258.186 31.4264 258.033 31.433 257.929 31.3482L257.807 31.2481C256.491 30.1698 254.841 29.5832 253.116 29.5832C251.386 29.5832 249.73 30.1738 248.412 31.2589L248.29 31.3595C248.186 31.4447 248.033 31.4383 247.937 31.3448L246.951 30.3815C246.844 30.2768 246.851 30.105 246.966 30.0084L247.108 29.8889C248.782 28.4822 250.9 27.7143 253.116 27.7143Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<path opacity="0.4" d="M283 31V31C283.552 31 284 31.4477 284 32V33C284 33.5523 283.552 34 283 34V34V31Z" fill="#F9F9F9" fill-opacity="0.18"/>
|
||||
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M267.217 28H279.783C281.007 28 282 29.0074 282 30.25V34.75C282 35.9926 281.007 37 279.783 37H267.217C265.993 37 265 35.9926 265 34.75V30.25C265 29.0074 265.993 28 267.217 28ZM267.217 28.75C266.401 28.75 265.739 29.4216 265.739 30.25V34.75C265.739 35.5784 266.401 36.25 267.217 36.25H279.783C280.599 36.25 281.261 35.5784 281.261 34.75V30.25C281.261 29.4216 280.599 28.75 279.783 28.75H267.217Z" fill="#F9F9F9" fill-opacity="0.18"/>
|
||||
<path d="M266 30.5C266 29.6716 266.672 29 267.5 29H279.5C280.328 29 281 29.6716 281 30.5V34.5C281 35.3284 280.328 36 279.5 36H267.5C266.672 36 266 35.3284 266 34.5V30.5Z" fill="#DBDBDB" fill-opacity="0.7"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_10_77" x1="160" y1="0" x2="160" y2="529" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1E1E22"/>
|
||||
<stop offset="1" stop-color="#1E1E22" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_10_77" x1="160" y1="0" x2="160" y2="673" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.262534" stop-color="#EDEDF0"/>
|
||||
<stop offset="1" stop-color="#EDEDF0" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_10_77" x1="159.5" y1="7" x2="159.5" y2="529" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#19191C"/>
|
||||
<stop offset="1" stop-color="#1D1D21"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_10_77" x1="142.5" y1="70.5" x2="173.5" y2="106.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9FA5B2"/>
|
||||
<stop offset="1" stop-color="#8D929D"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user