Skip to content

Commit f646097

Browse files
committed
Add a route to show user targets
There's some code for creating a target, but it must go through design first.
1 parent 205b99e commit f646097

File tree

6 files changed

+539
-0
lines changed

6 files changed

+539
-0
lines changed

src/routes/console/project-[project]/auth/user-[user]/header.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
title: 'Memberships',
2020
event: 'memberships'
2121
},
22+
{
23+
href: `${path}/targets`,
24+
title: 'Targets',
25+
event: 'targets'
26+
},
2227
{
2328
href: `${path}/sessions`,
2429
title: 'Sessions',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<script lang="ts">
2+
import { page } from '$app/stores';
3+
import { Button } from '$lib/elements/forms';
4+
import {
5+
Empty,
6+
EmptySearch,
7+
PaginationWithLimit,
8+
Heading,
9+
ViewSelector,
10+
EmptyFilter
11+
} from '$lib/components';
12+
import { Container } from '$lib/layout';
13+
import type { PageData } from './$types';
14+
import Table from './table.svelte';
15+
import { Filters, hasPageQueries } from '$lib/components/filters';
16+
import { columns } from './store';
17+
import { View } from '$lib/helpers/load';
18+
import Create from './create.svelte';
19+
20+
export let data: PageData;
21+
let showAdd = false;
22+
</script>
23+
24+
<Container>
25+
<div class="u-flex u-flex-vertical">
26+
<div class="u-flex u-main-space-between">
27+
<Heading tag="h2" size="5">Targets</Heading>
28+
<div class="is-only-mobile u-hide">
29+
<Button on:click={() => (showAdd = true)} event="create_user_target">
30+
<span class="icon-plus" aria-hidden="true" />
31+
<span class="text">Add target</span>
32+
</Button>
33+
</div>
34+
</div>
35+
<!-- TODO: Add searching when API supports it -->
36+
<!-- <SearchQuery search={data.search} placeholder="Search by name">
37+
<div class="u-flex u-gap-16 is-not-mobile">
38+
<Filters query={data.query} {columns} />
39+
<ViewSelector
40+
view={View.Table}
41+
{columns}
42+
hideView
43+
allowNoColumns
44+
showColsTextMobile />
45+
<Button on:click={() => (showAdd = true)} event="create_user_target">
46+
<span class="icon-plus" aria-hidden="true" />
47+
<span class="text">Add target</span>
48+
</Button>
49+
</div>
50+
</SearchQuery> -->
51+
<!-- TODO: Remove when searching is added -->
52+
<div class="u-flex u-main-end u-gap-16 is-not-mobile">
53+
<Filters query={data.query} {columns} />
54+
<div>
55+
<ViewSelector
56+
view={View.Table}
57+
{columns}
58+
hideView
59+
allowNoColumns
60+
showColsTextMobile />
61+
<div class="u-hide">
62+
<Button on:click={() => (showAdd = true)} event="create_user_target">
63+
<span class="icon-plus" aria-hidden="true" />
64+
<span class="text">Add target</span>
65+
</Button>
66+
</div>
67+
</div>
68+
</div>
69+
<div class="u-flex u-gap-16 is-only-mobile u-margin-block-start-16">
70+
<div class="u-flex-basis-50-percent">
71+
<!-- TODO: fix width -->
72+
<ViewSelector
73+
view={View.Table}
74+
{columns}
75+
hideView
76+
allowNoColumns
77+
showColsTextMobile />
78+
</div>
79+
<div class="u-flex-basis-50-percent">
80+
<!-- TODO: fix width -->
81+
<Filters query={data.query} {columns} />
82+
</div>
83+
</div>
84+
</div>
85+
{#if data.targets.total}
86+
<Table {data} />
87+
88+
<PaginationWithLimit
89+
name="Targets"
90+
limit={data.limit}
91+
offset={data.offset}
92+
total={data.targets.total} />
93+
{:else if $hasPageQueries}
94+
<EmptyFilter resource="targets" />
95+
{:else if data.search}
96+
<EmptySearch>
97+
<div class="u-text-center">
98+
<b>Sorry, we couldn't find '{data.search}'</b>
99+
<p>There are no targets that match your search.</p>
100+
</div>
101+
<Button
102+
secondary
103+
href={`/console/project-${$page.params.project}/auth/user-${$page.params.user}/targets`}>
104+
Clear Search
105+
</Button>
106+
</EmptySearch>
107+
{:else}
108+
<!-- TODO: update docs link -->
109+
<Empty
110+
single
111+
on:click={() => (showAdd = true)}
112+
href="https://appwrite.io/docs/references/cloud/client-web/teams"
113+
target="subscriber" />
114+
{/if}
115+
</Container>
116+
117+
<Create bind:show={showAdd} on:close={() => (showAdd = false)} />
118+
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Query } from '@appwrite.io/console';
2+
import { sdk } from '$lib/stores/sdk';
3+
import { getLimit, getPage, getQuery, getSearch, pageToOffset } from '$lib/helpers/load';
4+
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
5+
import type { PageLoad } from './$types';
6+
import { queryParamToMap, queries } from '$lib/components/filters';
7+
import type { Provider, Target } from '$routes/console/project-[project]/messaging/store';
8+
9+
export const load: PageLoad = async ({ params, url, route, depends }) => {
10+
depends(Dependencies.USER_TARGETS);
11+
const page = getPage(url);
12+
const limit = getLimit(url, route, PAGE_LIMIT);
13+
const offset = pageToOffset(page, limit);
14+
const search = getSearch(url);
15+
const query = getQuery(url);
16+
17+
const parsedQueries = queryParamToMap(query || '[]');
18+
queries.set(parsedQueries);
19+
20+
const payload = {
21+
queries: [
22+
Query.limit(limit),
23+
Query.offset(offset),
24+
Query.orderDesc(''),
25+
...parsedQueries.values()
26+
]
27+
};
28+
29+
if (search) {
30+
payload['search'] = search;
31+
}
32+
33+
// TODO: remove when the API is ready with data
34+
// This allows us to mock w/ data and when search returns 0 results
35+
const targets: { targets: Target[]; total: number } =
36+
await sdk.forProject.client.call(
37+
'GET',
38+
new URL(
39+
`${sdk.forProject.client.config.endpoint}/users/${params.user}/targets`
40+
),
41+
{
42+
'X-Appwrite-Project': sdk.forProject.client.config.project,
43+
'content-type': 'application/json',
44+
'X-Appwrite-Mode': 'admin'
45+
},
46+
payload
47+
);
48+
49+
const promisesById: Record<string, Promise<any>> = {};
50+
targets.targets.forEach((target) => {
51+
if (target.providerId && !promisesById[target.providerId]) {
52+
promisesById[target.providerId] = sdk.forProject.client.call(
53+
'GET',
54+
new URL(
55+
`${sdk.forProject.client.config.endpoint}/messaging/providers/${target.providerId}`
56+
),
57+
{
58+
'X-Appwrite-Project': sdk.forProject.client.config.project,
59+
'content-type': 'application/json',
60+
'X-Appwrite-Mode': 'admin'
61+
}
62+
);
63+
}
64+
});
65+
66+
const providersById: Record<string, Provider> = {};
67+
const resolved = await Promise.allSettled(Object.values(promisesById));
68+
resolved.forEach((result) => {
69+
if (result.status === 'fulfilled') {
70+
const provider = result.value;
71+
providersById[provider.$id] = provider;
72+
}
73+
});
74+
75+
return {
76+
offset,
77+
limit,
78+
search,
79+
query,
80+
targets,
81+
providersById,
82+
};
83+
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<script lang="ts">
2+
import { invalidate } from '$app/navigation';
3+
import { page } from '$app/stores';
4+
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
5+
import { Modal, CustomId } from '$lib/components';
6+
import { Dependencies } from '$lib/constants';
7+
import { Pill } from '$lib/elements';
8+
import { Button, InputText, FormList, InputSelect, InputPhone } from '$lib/elements/forms';
9+
import InputEmail from '$lib/elements/forms/inputEmail.svelte';
10+
import { addNotification } from '$lib/stores/notifications';
11+
import { sdk } from '$lib/stores/sdk';
12+
import { ProviderTypes } from '$routes/console/project-[project]/messaging/providerType.svelte';
13+
import { ID } from '@appwrite.io/console';
14+
15+
export let show = false;
16+
17+
let providerType = ProviderTypes.Push;
18+
let identifier = '';
19+
let name = '';
20+
let providerId = '';
21+
let id: string = null;
22+
let showCustomId = false;
23+
24+
const providerTypeOptions = [
25+
{ label: 'Push', value: ProviderTypes.Push },
26+
{ label: 'Email', value: ProviderTypes.Email },
27+
{ label: 'SMS', value: ProviderTypes.Sms },
28+
];
29+
30+
const create = async () => {
31+
try {
32+
const payload = {
33+
targetId: id ? id : ID.unique(),
34+
providerType,
35+
identifier
36+
};
37+
38+
if (providerId) {
39+
payload['providerId'] = providerId;
40+
}
41+
42+
if (name) {
43+
payload['name'] = name;
44+
}
45+
46+
await sdk.forProject.client.call(
47+
'POST',
48+
new URL(
49+
`${sdk.forProject.client.config.endpoint}/users/${$page.params.user}/targets`
50+
),
51+
{
52+
'X-Appwrite-Project': sdk.forProject.client.config.project,
53+
'content-type': 'application/json',
54+
'X-Appwrite-Mode': 'admin'
55+
},
56+
payload,
57+
);
58+
show = false;
59+
addNotification({
60+
type: 'success',
61+
message: `Target has been created`
62+
});
63+
name = id = null;
64+
invalidate(Dependencies.USER_TARGETS);
65+
trackEvent(Submit.UserTargetCreate, {
66+
customId: !!id,
67+
providerType: providerType,
68+
});
69+
} catch (error) {
70+
addNotification({
71+
type: 'error',
72+
message: error.message
73+
});
74+
trackError(error, Submit.UserTargetCreate);
75+
}
76+
};
77+
78+
// Ensure values are reset when modal is opened
79+
$: if (show) {
80+
showCustomId = false;
81+
providerType = ProviderTypes.Push;
82+
identifier = '';
83+
name = '';
84+
providerId = '';
85+
id = null;
86+
}
87+
88+
$: if (providerType) {
89+
identifier = '';
90+
}
91+
</script>
92+
93+
<Modal title="Create target" size="big" bind:show onSubmit={create}>
94+
<FormList>
95+
<InputSelect id="provider-type" label="Provider Type" bind:value={providerType} options={providerTypeOptions} />
96+
{#if providerType === ProviderTypes.Push}
97+
<InputText
98+
id="provider-id"
99+
label="Provider ID"
100+
placeholder='Enter provider ID'
101+
bind:value={providerId}
102+
required />
103+
<InputText
104+
id="identifier"
105+
label="Identifier"
106+
placeholder='Enter push token'
107+
bind:value={identifier}
108+
required />
109+
<InputText
110+
id="name"
111+
label="Name"
112+
placeholder="Enter target name"
113+
bind:value={name}
114+
required />
115+
{:else if providerType === ProviderTypes.Email}
116+
<InputEmail
117+
id="identifier"
118+
label="Identifier"
119+
placeholder='Enter email'
120+
bind:value={identifier}
121+
required />
122+
{:else if providerType === ProviderTypes.Sms}
123+
<InputPhone
124+
id="identifier"
125+
label="Identifier"
126+
placeholder='Enter phone number'
127+
bind:value={identifier}
128+
required />
129+
{/if}
130+
131+
{#if !showCustomId}
132+
<div>
133+
<Pill button on:click={() => (showCustomId = !showCustomId)}
134+
><span class="icon-pencil" aria-hidden="true" /><span class="text">
135+
Target ID
136+
</span></Pill>
137+
</div>
138+
{:else}
139+
<CustomId bind:show={showCustomId} name="Target" bind:id autofocus={false} />
140+
{/if}
141+
</FormList>
142+
<svelte:fragment slot="footer">
143+
<Button secondary on:click={() => (show = false)}>Cancel</Button>
144+
<Button submit>Create</Button>
145+
</svelte:fragment>
146+
</Modal>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Column } from '$lib/helpers/types';
2+
import { writable } from 'svelte/store';
3+
4+
export const columns = writable<Column[]>([
5+
{ id: '$id', title: 'Target ID', type: 'string', show: true, width: 140 },
6+
{ id: 'target', title: 'Target', type: 'string', show: true, filter: false, width: 140 },
7+
{ id: 'type', title: 'Type', type: 'string', show: true, filter: false, width: 80 },
8+
{ id: 'provider', title: 'Provider', type: 'string', show: true, filter: false, width: 80 },
9+
{ id: '$createdAt', title: 'Created', type: 'string', show: true, width: 100 }
10+
]);

0 commit comments

Comments
 (0)