Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TemplateEditView from '../vue/views/TemplateEditView.vue'
import BouncesView from '../vue/views/BouncesView.vue'
import PublicPagesView from '../vue/views/PublicPagesView.vue'
import PublicPageEditView from '../vue/views/PublicPageEditView.vue'
import SettingsView from '../vue/views/SettingsView.vue'

export const router = createRouter({
history: createWebHistory(),
Expand All @@ -28,6 +29,7 @@ export const router = createRouter({
{ path: '/public', name: 'public-pages', component: PublicPagesView, meta: { title: 'Public Pages' } },
{ path: '/public/create', name: 'public-page-create', component: PublicPageEditView, meta: { title: 'Create Public Page' } },
{ path: '/public/:pageId/edit', name: 'public-page-edit', component: PublicPageEditView, meta: { title: 'Edit Public Page' } },
{ path: '/settings', name: 'settings', component: SettingsView, meta: { title: 'Settings' } },
{ path: '/:pathMatch(.*)*', redirect: '/' },
],
});
Expand Down
5 changes: 4 additions & 1 deletion assets/vue/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SubscriberAttributesClient,
TemplatesClient,
BouncesClient,
ConfigClient, AdminAttributeClient,
} from '@tatevikgr/rest-api-client';

const AUTHENTICATION_REDIRECT_PATH = '/login';
Expand Down Expand Up @@ -44,7 +45,6 @@ if (!apiBaseUrl) {

const client = new Client(apiBaseUrl || '', {
onAuthenticationError: redirectToLogin,
onAuthorizationError: redirectToLogin,
});

if (apiToken) {
Expand All @@ -64,6 +64,7 @@ client.axiosInstance?.interceptors?.response?.use(

export const subscribersClient = new SubscribersClient(client);
export const adminClient = new AdminClient(client);
export const adminAttributeClient = new AdminAttributeClient(client);
export const listClient = new ListClient(client);
export const campaignClient = new CampaignClient(client);
export const listMessagesClient = new ListMessagesClient(client);
Expand All @@ -73,6 +74,8 @@ export const subscribePagesClient = new SubscribePagesClient(client);
export const subscriberAttributesClient = new SubscriberAttributesClient(client);
export const templateClient = new TemplatesClient(client);
export const bouncesClient = new BouncesClient(client);
export const configClient = new ConfigClient(client);


export const backendFetch = async (input, init = undefined) => {
const response = await fetch(input, init);
Expand Down
2 changes: 1 addition & 1 deletion assets/vue/components/bounces/BouncesActionsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
v-for="tab in tabs"
:key="tab.id"
type="button"
class="px-4 py-2.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap flex-shrink-0"
class="px-4 py-2.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap shrink-0"
:class="activeTab === tab.id
? 'bg-white text-slate-900 shadow-sm border border-slate-300'
: 'text-slate-500 hover:text-slate-700 hover:bg-white/60'"
Expand Down
119 changes: 69 additions & 50 deletions assets/vue/components/campaigns/CampaignDirectory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@
<th class="px-6 py-4">Status</th>
<th class="px-6 py-4">Lists</th>
<th class="px-6 py-4">Processed</th>
<th class="px-6 py-4">Statistics</th>
<th class="px-6 py-4" v-if="showStatistics">Statistics</th>
<th class="px-6 py-4 text-right">Actions</th>
</tr>
</thead>

<tbody class="divide-y divide-slate-200">
<tr v-if="isLoading">
<td colspan="6" class="px-6 py-8 text-center text-slate-500">Loading campaigns...</td>
<td :colspan="showStatistics ? 6 : 5" class="px-6 py-8 text-center text-slate-500">Loading campaigns...</td>
</tr>

<tr v-else-if="errorMessage">
<td colspan="6" class="px-6 py-8 text-center text-red-600">{{ errorMessage }}</td>
<td :colspan="showStatistics ? 6 : 5" class="px-6 py-8 text-center text-red-600">{{ errorMessage }}</td>
</tr>

<tr v-else-if="paginatedCampaigns.length === 0">
<td colspan="6" class="px-6 py-8 text-center text-slate-500">No campaigns for this filter.</td>
<td :colspan="showStatistics ? 6 : 5" class="px-6 py-8 text-center text-slate-500">No campaigns for this filter.</td>
</tr>

<tr
Expand Down Expand Up @@ -80,7 +80,7 @@
<p class="text-xs leading-5"><span class="font-medium text-slate-700">Text:</span> {{ campaign.processedText }}</p>
<p class="text-xs leading-5"><span class="font-medium text-slate-700">HTML:</span> {{ campaign.processedHtml }}</p>
</td>
<td class="px-6 py-4 text-slate-600 align-top">
<td class="px-6 py-4 text-slate-600 align-top" v-if="showStatistics">
<p class="text-xs leading-5"><span class="font-medium text-slate-700">Total views:</span> {{ campaign.totalViews }}</p>
<p class="text-xs leading-5"><span class="font-medium text-slate-700">Unique views:</span> {{ campaign.uniqueViews }}</p>
<p class="text-xs leading-5"><span class="font-medium text-slate-700">Bounced:</span> {{ campaign.bounced }}</p>
Expand Down Expand Up @@ -223,7 +223,10 @@
<p><span class="font-medium text-slate-700">Started:</span> {{ campaign.startedAt }}</p>
<p><span class="font-medium text-slate-700">Time to send:</span> {{ campaign.timeToSend }}</p>
<p><span class="font-medium text-slate-700">Processed:</span> {{ campaign.processedTotal }} (Text: {{ campaign.processedText }}, HTML: {{ campaign.processedHtml }})</p>
<p><span class="font-medium text-slate-700">Statistics:</span> Total views {{ campaign.totalViews }}, Unique views {{ campaign.uniqueViews }}, Bounced {{ campaign.bounced }}</p>
<p v-if="showStatistics">
<span class="font-medium text-slate-700">Statistics:</span>
Total views {{ campaign.totalViews }}, Unique views {{ campaign.uniqueViews }}, Bounced {{ campaign.bounced }}
</p>
</div>

<div class="pt-2 flex flex-wrap gap-2">
Expand Down Expand Up @@ -400,6 +403,7 @@ const selectedCampaign = ref(null)
const viewErrorMessage = ref('')
const isResending = ref(false)
const resendErrorMessage = ref('')
const showStatistics = ref(true)

const filterOptions = [
{ id: 'all', label: 'All' },
Expand Down Expand Up @@ -732,62 +736,77 @@ const fetchAllCampaigns = async () => {
const fetchCampaignStatistics = async () => {
const statisticsMap = {}

let cursor = null
let guard = 0
try {
let cursor = null
let guard = 0

// Base campaign statistics
while (guard < 200) {
const response = await statisticsClient.getCampaignStatistics(cursor, 100)
const items = Array.isArray(response?.items) ? response.items : []
while (guard < 200) {
const response = await statisticsClient.getCampaignStatistics(cursor, 100)
const items = Array.isArray(response?.items) ? response.items : []

items.forEach((item) => {
statisticsMap[item.campaignId] = {
bounces: Number(item.bounces ?? 0),
sent: Number(item.sent ?? 0),
uniqueViews: Number(item.uniqueViews ?? 0),
}
})
items.forEach((item) => {
statisticsMap[item.campaignId] = {
bounces: Number(item.bounces ?? 0),
sent: Number(item.sent ?? 0),
uniqueViews: Number(item.uniqueViews ?? 0),
}
})

const hasMore = Boolean(response?.pagination?.hasMore)
const nextCursor = response?.pagination?.nextCursor ?? null
if (!hasMore || nextCursor === null) break
const hasMore = Boolean(response?.pagination?.hasMore)
const nextCursor = response?.pagination?.nextCursor ?? null

cursor = nextCursor
guard += 1
}
if (!hasMore || nextCursor === null) break

cursor = null
guard = 0
cursor = nextCursor
guard++
}

// View-open statistics
while (guard < 200) {
const response = await statisticsClient.getStatisticsOfViewOpens(cursor, 100)
const items = Array.isArray(response?.items) ? response.items : []
cursor = null
guard = 0

items.forEach((item) => {
const existing = statisticsMap[item.campaignId] || {
bounces: 0,
sent: 0,
uniqueViews: 0,
}
while (guard < 200) {
const response = await statisticsClient.getStatisticsOfViewOpens(cursor, 100)
const items = Array.isArray(response?.items) ? response.items : []

statisticsMap[item.campaignId] = {
...existing,
// only merge fields that really belong here
sent: Number(item.sent ?? existing.sent),
}
})
items.forEach((item) => {
const existing = statisticsMap[item.campaignId] ?? {
bounces: 0,
sent: 0,
uniqueViews: 0,
}

const hasMore = Boolean(response?.pagination?.hasMore)
const nextCursor = response?.pagination?.nextCursor ?? null
if (!hasMore || nextCursor === null) break
statisticsMap[item.campaignId] = {
...existing,
sent: Number(item.sent ?? existing.sent),
}
})

cursor = nextCursor
guard += 1
}
const hasMore = Boolean(response?.pagination?.hasMore)
const nextCursor = response?.pagination?.nextCursor ?? null

statisticsByCampaignId.value = statisticsMap
if (!hasMore || nextCursor === null) break

cursor = nextCursor
guard++
}

statisticsByCampaignId.value = statisticsMap
showStatistics.value = true
} catch (error) {
if (
error?.name === 'AuthorizationException' ||
error?.code === 'AuthorizationException' ||
error?.status === 403
) {
showStatistics.value = false
statisticsByCampaignId.value = {}
return
}

throw error
}
}

const fetchListsForVisibleCampaigns = async () => {
const pending = paginatedCampaigns.value
.map((campaign) => campaign.id)
Expand Down
144 changes: 144 additions & 0 deletions assets/vue/components/settings/CreateAdminAttributeModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
>
<div class="w-full max-w-lg rounded-xl bg-white shadow-xl">
<div class="border-b border-slate-200 px-6 py-4 flex justify-between items-center">
<h2 class="text-lg font-semibold text-slate-900">
Create Attribute
</h2>

<button
class="text-slate-400 hover:text-slate-700"
@click="$emit('close')"
>
</button>
</div>

<form
class="p-6 space-y-5"
@submit.prevent="submit"
>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">
Name
</label>

<input
v-model="form.name"
class="w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ext-wf1"
required
>
</div>

<div>
<label class="block text-sm font-medium text-slate-700 mb-1">
Type
</label>

<select
v-model="form.type"
class="w-full rounded-lg border border-slate-300 px-3 py-2"
>
<option value="textline">Text</option>
<option value="hidden">Hidden</option>

</select>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>

<label class="flex items-center gap-2">
<input
type="checkbox"
v-model="form.required"
>

<span class="text-sm text-slate-700">
Required
</span>
</label>

<div
v-if="error"
class="text-sm text-red-600"
>
{{ error }}
</div>

<div class="flex justify-end gap-3 pt-2">
<button
type="button"
class="px-4 py-2 rounded-lg border border-slate-300"
@click="$emit('close')"
>
Cancel
</button>

<button
class="px-4 py-2 rounded-lg bg-ext-wf1 text-white"
:disabled="saving"
>
{{ saving ? 'Creating...' : 'Create' }}
</button>
</div>
</form>
</div>
</div>
</Teleport>
</template>

<script setup>
import { reactive, ref, watch } from 'vue'
import { adminAttributeClient } from '../../api'

const props = defineProps({
isOpen: Boolean
})

const emit = defineEmits([
'close',
'created'
])

const saving = ref(false)
const error = ref('')

const form = reactive({
name: '',
type: 'string',
required: false
})

watch(
() => props.isOpen,
(open) => {
if (!open) return

form.name = ''
form.type = 'textline'
form.required = false
error.value = ''
}
)

const submit = async () => {
saving.value = true
error.value = ''

try {
await adminAttributeClient.createAttributeDefinition({
name: form.name,
type: form.type,
required: form.required
})

emit('created')
} catch (e) {
error.value = e?.message || 'Unable to create attribute.'
} finally {
saving.value = false
}
}
</script>
Loading
Loading