Skip to content

Commit 36793e1

Browse files
committed
Export subscribers panel
1 parent 10480a9 commit 36793e1

7 files changed

Lines changed: 285 additions & 122 deletions

File tree

assets/vue/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Client, ListClient, SubscribersClient, SubscriptionClient} from '@tatevikgr/rest-api-client';
1+
import {Client, ListClient, SubscribersClient, SubscriptionClient, SubscriberAttributesClient} from '@tatevikgr/rest-api-client';
22

33
const appElement = document.getElementById('vue-app');
44
const apiToken = appElement?.dataset.apiToken;
@@ -17,5 +17,6 @@ if (apiToken) {
1717
export const subscribersClient = new SubscribersClient(client);
1818
export const listClient = new ListClient(client);
1919
export const subscriptionClient = new SubscriptionClient(client);
20+
export const subscriberAttributesClient = new SubscriberAttributesClient(client);
2021

2122
export default client;
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<template>
2+
<div class="bg-white rounded-xl border border-slate-200 shadow-sm p-4 sm:p-6 space-y-5">
3+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
4+
<div>
5+
<h3 class="text-lg font-semibold text-slate-900">Export subscribers on: {{ listName || `List #${listId}` }}</h3>
6+
</div>
7+
</div>
8+
9+
<div class="space-y-3">
10+
<p class="text-sm font-medium text-slate-800">What date needs to be used:</p>
11+
12+
<label
13+
v-for="option in dateTypeOptions"
14+
:key="option.value"
15+
class="flex items-start gap-2 text-sm text-slate-700"
16+
>
17+
<input
18+
v-model="form.dateType"
19+
type="radio"
20+
name="list-export-date-type"
21+
class="mt-0.5 h-4 w-4 border-slate-300 text-ext-wf1 focus:ring-ext-wf1"
22+
:value="option.value"
23+
>
24+
<span>{{ option.label }}</span>
25+
</label>
26+
</div>
27+
28+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
29+
<div>
30+
<label class="block text-sm font-medium text-slate-700" for="list-export-date-from">Date From:</label>
31+
<input
32+
id="list-export-date-from"
33+
v-model="form.dateFrom"
34+
type="date"
35+
class="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 disabled:bg-slate-100"
36+
:disabled="usesAnyDate"
37+
>
38+
</div>
39+
40+
<div>
41+
<label class="block text-sm font-medium text-slate-700" for="list-export-date-to">Date To:</label>
42+
<input
43+
id="list-export-date-to"
44+
v-model="form.dateTo"
45+
type="date"
46+
class="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 disabled:bg-slate-100"
47+
:disabled="usesAnyDate"
48+
>
49+
</div>
50+
</div>
51+
52+
<div class="space-y-3">
53+
<p class="text-sm font-medium text-slate-800">Select the columns to include in the export</p>
54+
55+
<label class="inline-flex items-center gap-2 text-sm font-semibold text-slate-900">
56+
<input
57+
ref="selectAllColumnsCheckbox"
58+
type="checkbox"
59+
class="h-4 w-4 rounded border-slate-300 text-ext-wf1 focus:ring-ext-wf1"
60+
:checked="allColumnsSelected"
61+
@change="toggleAllColumns"
62+
>
63+
Select all
64+
</label>
65+
66+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2">
67+
<label
68+
v-for="column in columnOptions"
69+
:key="column.value"
70+
class="inline-flex items-center gap-2 text-sm text-slate-700"
71+
>
72+
<input
73+
v-model="form.columns"
74+
type="checkbox"
75+
class="h-4 w-4 rounded border-slate-300 text-ext-wf1 focus:ring-ext-wf1"
76+
:value="column.value"
77+
>
78+
<span>{{ column.label }}</span>
79+
</label>
80+
</div>
81+
</div>
82+
83+
<p v-if="exportError" class="text-sm text-red-600">{{ exportError }}</p>
84+
85+
<button
86+
type="button"
87+
class="inline-flex items-center justify-center rounded-md border border-transparent bg-ext-wf1 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-ext-wf3 disabled:opacity-50"
88+
:disabled="!canExport"
89+
@click="exportSubscribers"
90+
>
91+
Export
92+
</button>
93+
</div>
94+
</template>
95+
96+
<script setup>
97+
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
98+
// todo: check why subscriberAttributesClient was not working
99+
import client, { subscriberAttributesClient } from '../../api'
100+
101+
onMounted(async () => {
102+
try {
103+
const queryParams = { limit: 5 };
104+
const data = await client.get('attributes', queryParams)
105+
const dynamicColumns = data.items.map(attr => ({
106+
value: attr.name,
107+
label: capitalizeFirst(attr.name)
108+
}))
109+
110+
columnOptions.value = [
111+
...columnOptions.value,
112+
...dynamicColumns
113+
]
114+
115+
form.value.columns = columnOptions.value.map(c => c.value)
116+
} catch (err) {
117+
console.error('Failed to load attribute definitions', err)
118+
}
119+
})
120+
const capitalizeFirst = (s) => s ? s[0].toUpperCase() + s.slice(1) : ''
121+
122+
const props = defineProps({
123+
listId: {
124+
type: Number,
125+
required: true
126+
},
127+
listName: {
128+
type: String,
129+
default: ''
130+
}
131+
})
132+
133+
const dateTypeOptions = [
134+
{ value: 'any', label: 'Any date (Export all subscribers)' },
135+
{ value: 'signup', label: 'When they signed up' },
136+
{ value: 'changed', label: 'When the record was changed' },
137+
{ value: 'changelog', label: 'Based on changelog' },
138+
{ value: 'subscribed', label: 'When they subscribed to new list' }
139+
]
140+
141+
const columnOptions = ref([
142+
{ value: 'id', label: 'ID' },
143+
{ value: 'email', label: 'Email' },
144+
{ value: 'confirmed', label: 'Is this subscriber confirmed' },
145+
{ value: 'blacklisted', label: 'Is this subscriber blacklisted' },
146+
// { value: 'manualConfirm', label: 'Did this subscriber manually confirm' },
147+
{ value: 'bounceCount', label: 'Number of bounces' },
148+
{ value: 'createdAt', label: 'Entered' },
149+
{ value: 'updatedAt', label: 'Last Modified' },
150+
{ value: 'uniqueId', label: 'Unique ID' },
151+
{ value: 'htmlEmail', label: 'Send this subscriber HTML emails' },
152+
{ value: 'rssFrequency', label: 'RSS Frequency' },
153+
{ value: 'disabled', label: 'Is this account disabled?' },
154+
{ value: 'extraData', label: 'Additional data' },
155+
{ value: 'foreignKey', label: 'Foreign Key' },
156+
])
157+
158+
const form = ref({
159+
dateType: 'any',
160+
dateFrom: '',
161+
dateTo: '',
162+
columns: columnOptions.value.map((column) => column.value)
163+
})
164+
165+
const exportError = ref('')
166+
const selectAllColumnsCheckbox = ref(null)
167+
168+
const usesAnyDate = computed(() => form.value.dateType === 'any')
169+
170+
const allColumnsSelected = computed(() => {
171+
return form.value.columns.length === columnOptions.length
172+
})
173+
174+
const someColumnsSelected = computed(() => {
175+
return form.value.columns.length > 0 && !allColumnsSelected.value
176+
})
177+
178+
const canExport = computed(() => {
179+
return Number.isInteger(props.listId) && props.listId > 0 && form.value.columns.length > 0
180+
})
181+
182+
watchEffect(() => {
183+
if (!selectAllColumnsCheckbox.value) return
184+
selectAllColumnsCheckbox.value.indeterminate = someColumnsSelected.value
185+
})
186+
187+
watch(() => form.value.dateType, (dateType) => {
188+
exportError.value = ''
189+
if (dateType === 'any') {
190+
form.value.dateFrom = ''
191+
form.value.dateTo = ''
192+
}
193+
})
194+
195+
watch(() => [form.value.dateFrom, form.value.dateTo], () => {
196+
exportError.value = ''
197+
})
198+
199+
const toggleAllColumns = (event) => {
200+
if (event.target.checked) {
201+
form.value.columns = columnOptions.value.map((column) => column.value)
202+
return
203+
}
204+
205+
form.value.columns = []
206+
}
207+
208+
const exportSubscribers = () => {
209+
if (!canExport.value) {
210+
exportError.value = 'Please select at least one column to export.'
211+
return
212+
}
213+
214+
if (!usesAnyDate.value && form.value.dateFrom && form.value.dateTo && form.value.dateFrom > form.value.dateTo) {
215+
exportError.value = 'Date From cannot be after Date To.'
216+
return
217+
}
218+
219+
const params = new URLSearchParams()
220+
params.set('list_id', String(props.listId))
221+
params.set('date_type', form.value.dateType)
222+
223+
if (!usesAnyDate.value) {
224+
if (form.value.dateFrom) {
225+
params.set('date_from', form.value.dateFrom)
226+
}
227+
228+
if (form.value.dateTo) {
229+
params.set('date_to', form.value.dateTo)
230+
}
231+
}
232+
233+
form.value.columns.forEach((column) => {
234+
params.append('columns[]', column)
235+
})
236+
237+
window.location.href = `/subscribers/export?${params.toString()}`
238+
}
239+
</script>

assets/vue/components/subscribers/SubscribersTable.vue

Lines changed: 0 additions & 64 deletions
This file was deleted.

assets/vue/views/ListSubscribersView.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
</div>
2020
</div>
2121

22+
<ListSubscribersExportPanel
23+
:list-id="listId"
24+
:list-name="listName"
25+
/>
26+
2227
<div class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
2328
<div class="p-4 sm:p-6 border-b border-slate-200 flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
2429
<h3 class="text-lg font-semibold text-slate-900">Subscribers</h3>
@@ -253,6 +258,7 @@
253258
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
254259
import { useRoute } from 'vue-router'
255260
import AdminLayout from '../layouts/AdminLayout.vue'
261+
import ListSubscribersExportPanel from '../components/lists/ListSubscribersExportPanel.vue'
256262
import client, { subscriptionClient } from '../api'
257263
258264
const route = useRoute()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"webpack-notifier": "^1.15.0"
2020
},
2121
"dependencies": {
22-
"@tatevikgr/rest-api-client": "^1.2.0",
22+
"@tatevikgr/rest-api-client": "^1.3.0",
2323
"apexcharts": "^5.10.4",
2424
"vue": "^3.5.16",
2525
"vue-router": "4",

0 commit comments

Comments
 (0)