@stacksjs/defaults 0.70.356 → 0.70.358
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/skills/stacks-browse/SKILL.md +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/ai/skills/stacks-orm/SKILL.md +6 -0
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
- package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
- package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
- package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
- package/app/Actions/Dashboard/Content/content-input.ts +4 -4
- package/app/Actions/Dashboard/Content/post-input.ts +60 -8
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- package/views/dashboard/stores/auth.ts +18 -2
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { tsCloud } from '~/config/cloud'
|
|
5
5
|
import { getDashboardCloudSnapshot } from '../Cloud/cloud-overview'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
7
|
import { resolveDashboardServer } from './server-detail'
|
|
7
8
|
|
|
8
9
|
export default new Action({
|
|
@@ -12,10 +13,16 @@ export default new Action({
|
|
|
12
13
|
apiResponse: true,
|
|
13
14
|
async handle(request: RequestInstance) {
|
|
14
15
|
const identifier = request.getParam('id')
|
|
15
|
-
if (!identifier)
|
|
16
|
+
if (!identifier || !/^[A-Za-z0-9:_-]{1,160}$/.test(identifier))
|
|
16
17
|
return response.json({ error: 'A server identifier is required.' }, 400)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
let snapshot
|
|
20
|
+
try {
|
|
21
|
+
snapshot = await getDashboardCloudSnapshot(tsCloud)
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return dashboardOperationalError(error, 'Server state could not be loaded.', 'ServerShowAction')
|
|
25
|
+
}
|
|
19
26
|
const detail = resolveDashboardServer(snapshot, identifier)
|
|
20
27
|
if (!detail)
|
|
21
28
|
return response.json({ error: 'Server state was not found.' }, 404)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
-
import { campaigns } from '@stacksjs/newsletter'
|
|
3
|
+
import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
|
|
4
4
|
import { Campaign } from '@stacksjs/orm'
|
|
5
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
7
|
import { validateCampaignDelivery } from './campaign-delivery'
|
|
8
|
+
import { marketingRecordId } from './marketing-response'
|
|
7
9
|
|
|
8
10
|
export default new Action({
|
|
9
11
|
name: 'CampaignCancelAction',
|
|
@@ -11,8 +13,17 @@ export default new Action({
|
|
|
11
13
|
method: 'POST',
|
|
12
14
|
|
|
13
15
|
async handle(request: RequestInstance) {
|
|
14
|
-
const id =
|
|
15
|
-
|
|
16
|
+
const id = marketingRecordId(request)
|
|
17
|
+
if (!id)
|
|
18
|
+
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
19
|
+
|
|
20
|
+
let campaign
|
|
21
|
+
try {
|
|
22
|
+
campaign = await Campaign.find(id)
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return dashboardOperationalError(error, 'Campaign could not be loaded.', 'CampaignCancelAction.read')
|
|
26
|
+
}
|
|
16
27
|
if (!campaign)
|
|
17
28
|
return response.json({ message: 'Campaign not found.' }, 404)
|
|
18
29
|
|
|
@@ -20,7 +31,14 @@ export default new Action({
|
|
|
20
31
|
if (validationError)
|
|
21
32
|
return response.json({ message: validationError }, 422)
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
try {
|
|
35
|
+
await campaigns.cancel(id)
|
|
36
|
+
return response.json({ id, status: 'cancelled' })
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error instanceof CampaignStateConflictError)
|
|
40
|
+
return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
|
|
41
|
+
return dashboardOperationalError(error, 'Campaign could not be cancelled.', 'CampaignCancelAction.cancel', 500)
|
|
42
|
+
}
|
|
25
43
|
},
|
|
26
44
|
})
|
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
4
|
import { Campaign } from '@stacksjs/orm'
|
|
5
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { marketingModelError, marketingRecordId } from './marketing-response'
|
|
6
7
|
|
|
7
8
|
export default new Action({
|
|
8
9
|
name: 'CampaignDestroyAction',
|
|
@@ -10,24 +11,32 @@ export default new Action({
|
|
|
10
11
|
method: 'DELETE',
|
|
11
12
|
|
|
12
13
|
async handle(request: RequestInstance) {
|
|
13
|
-
const id =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return response.json({ message: 'Campaign not found.' }, 404)
|
|
14
|
+
const id = marketingRecordId(request)
|
|
15
|
+
if (!id)
|
|
16
|
+
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
try {
|
|
19
|
+
const campaign = await Campaign.find(id)
|
|
20
|
+
if (!campaign)
|
|
21
|
+
return response.json({ message: 'Campaign not found.' }, 404)
|
|
22
|
+
|
|
23
|
+
const status = String(campaign.get('status') || '')
|
|
24
|
+
const sendRow = await db
|
|
25
|
+
.selectFrom('campaign_sends')
|
|
26
|
+
.select(db.fn.count('id').as('count'))
|
|
27
|
+
.where('campaign_id', '=', id)
|
|
28
|
+
.executeTakeFirst()
|
|
29
|
+
if (Number(sendRow?.count || 0) > 0 || ['scheduled', 'sending', 'sent'].includes(status)) {
|
|
30
|
+
return response.json({
|
|
31
|
+
message: 'Scheduled campaigns and campaigns with delivery history cannot be deleted.',
|
|
32
|
+
}, 409)
|
|
33
|
+
}
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
await campaign.delete()
|
|
36
|
+
return response.noContent()
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return marketingModelError(error, 'Campaign could not be deleted.', 'CampaignDestroyAction')
|
|
40
|
+
}
|
|
32
41
|
},
|
|
33
42
|
})
|
|
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
2
2
|
import { config } from '@stacksjs/config'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
4
|
import { Campaign, EmailList } from '@stacksjs/orm'
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
6
|
import { normalizeCampaigns } from './campaign-records'
|
|
6
7
|
|
|
7
8
|
export default new Action({
|
|
@@ -11,49 +12,54 @@ export default new Action({
|
|
|
11
12
|
apiResponse: true,
|
|
12
13
|
|
|
13
14
|
async handle() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
15
|
+
try {
|
|
16
|
+
const [
|
|
17
|
+
campaigns,
|
|
18
|
+
lists,
|
|
19
|
+
membershipRows,
|
|
20
|
+
sendRows,
|
|
21
|
+
openedRows,
|
|
22
|
+
clickedRows,
|
|
23
|
+
] = await Promise.all([
|
|
24
|
+
Campaign.orderByDesc('id').limit(500).get(),
|
|
25
|
+
EmailList.orderBy('name', 'asc').get(),
|
|
26
|
+
db
|
|
27
|
+
.selectFrom('email_list_subscribers')
|
|
28
|
+
.select(['email_list_id', db.fn.count('id').as('count')])
|
|
29
|
+
.where('status', '=', 'subscribed')
|
|
30
|
+
.groupBy('email_list_id')
|
|
31
|
+
.execute(),
|
|
32
|
+
db
|
|
33
|
+
.selectFrom('campaign_sends')
|
|
34
|
+
.select(['campaign_id', 'status', db.fn.count('id').as('count')])
|
|
35
|
+
.groupBy(['campaign_id', 'status'])
|
|
36
|
+
.execute(),
|
|
37
|
+
db
|
|
38
|
+
.selectFrom('campaign_sends')
|
|
39
|
+
.select(['campaign_id', db.fn.count('id').as('count')])
|
|
40
|
+
.whereNotNull('opened_at')
|
|
41
|
+
.groupBy('campaign_id')
|
|
42
|
+
.execute(),
|
|
43
|
+
db
|
|
44
|
+
.selectFrom('campaign_sends')
|
|
45
|
+
.select(['campaign_id', db.fn.count('id').as('count')])
|
|
46
|
+
.whereNotNull('clicked_at')
|
|
47
|
+
.groupBy('campaign_id')
|
|
48
|
+
.execute(),
|
|
49
|
+
])
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
return normalizeCampaigns(
|
|
52
|
+
campaigns,
|
|
53
|
+
lists,
|
|
54
|
+
membershipRows,
|
|
55
|
+
sendRows,
|
|
56
|
+
openedRows,
|
|
57
|
+
clickedRows,
|
|
58
|
+
String((config as any).commerce?.currency || 'USD').toUpperCase(),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return dashboardOperationalError(error, 'Campaigns could not be loaded.', 'CampaignIndexAction')
|
|
63
|
+
}
|
|
58
64
|
},
|
|
59
65
|
})
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
-
import {
|
|
4
|
-
import { campaigns } from '@stacksjs/newsletter'
|
|
5
|
-
import { Campaign, EmailList } from '@stacksjs/orm'
|
|
3
|
+
import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
|
|
6
4
|
import { response } from '@stacksjs/router'
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
7
6
|
import {
|
|
8
7
|
campaignScheduleIso,
|
|
8
|
+
loadCampaignDeliveryContext,
|
|
9
9
|
validateCampaignDelivery,
|
|
10
10
|
} from './campaign-delivery'
|
|
11
|
+
import { marketingRecordId } from './marketing-response'
|
|
11
12
|
|
|
12
13
|
export default new Action({
|
|
13
14
|
name: 'CampaignScheduleAction',
|
|
@@ -15,35 +16,40 @@ export default new Action({
|
|
|
15
16
|
method: 'POST',
|
|
16
17
|
|
|
17
18
|
async handle(request: RequestInstance) {
|
|
18
|
-
const id =
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const id = marketingRecordId(request)
|
|
20
|
+
if (!id)
|
|
21
|
+
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
22
|
+
const schedule = campaignScheduleIso((await request.all()).scheduledAt)
|
|
23
|
+
if (schedule.error)
|
|
24
|
+
return response.json({ message: schedule.error }, 422)
|
|
25
|
+
|
|
26
|
+
let context
|
|
27
|
+
try {
|
|
28
|
+
context = await loadCampaignDeliveryContext(id)
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return dashboardOperationalError(error, 'Campaign delivery prerequisites could not be loaded.', 'CampaignScheduleAction.prerequisites')
|
|
32
|
+
}
|
|
33
|
+
if (!context.campaign)
|
|
21
34
|
return response.json({ message: 'Campaign not found.' }, 404)
|
|
22
35
|
|
|
23
|
-
const listId = Number(campaign.get('email_list_id') || 0)
|
|
24
|
-
const list = listId ? await EmailList.find(listId) : null
|
|
25
|
-
const memberRow = listId
|
|
26
|
-
? await db
|
|
27
|
-
.selectFrom('email_list_subscribers')
|
|
28
|
-
.select(db.fn.count('id').as('count'))
|
|
29
|
-
.where('email_list_id', '=', listId)
|
|
30
|
-
.where('status', '=', 'subscribed')
|
|
31
|
-
.executeTakeFirst()
|
|
32
|
-
: null
|
|
33
36
|
const validationError = validateCampaignDelivery(
|
|
34
|
-
campaign,
|
|
37
|
+
context.campaign,
|
|
35
38
|
'schedule',
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
context.activeMembers,
|
|
40
|
+
context.listStatus,
|
|
38
41
|
)
|
|
39
42
|
if (validationError)
|
|
40
43
|
return response.json({ message: validationError }, 422)
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return response.json({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
try {
|
|
46
|
+
await campaigns.schedule(id, schedule.value)
|
|
47
|
+
return response.json({ id, status: 'scheduled', scheduledAt: schedule.value }, 202)
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof CampaignStateConflictError)
|
|
51
|
+
return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
|
|
52
|
+
return dashboardOperationalError(error, 'Campaign could not be scheduled.', 'CampaignScheduleAction.queue', 500)
|
|
53
|
+
}
|
|
48
54
|
},
|
|
49
55
|
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
-
import {
|
|
4
|
-
import { campaigns } from '@stacksjs/newsletter'
|
|
5
|
-
import { Campaign, EmailList } from '@stacksjs/orm'
|
|
3
|
+
import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
|
|
6
4
|
import { response } from '@stacksjs/router'
|
|
7
|
-
import {
|
|
5
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
|
+
import { loadCampaignDeliveryContext, validateCampaignDelivery } from './campaign-delivery'
|
|
7
|
+
import { marketingRecordId } from './marketing-response'
|
|
8
8
|
|
|
9
9
|
export default new Action({
|
|
10
10
|
name: 'CampaignSendAction',
|
|
@@ -12,31 +12,37 @@ export default new Action({
|
|
|
12
12
|
method: 'POST',
|
|
13
13
|
|
|
14
14
|
async handle(request: RequestInstance) {
|
|
15
|
-
const id =
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const id = marketingRecordId(request)
|
|
16
|
+
if (!id)
|
|
17
|
+
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
18
|
+
|
|
19
|
+
let context
|
|
20
|
+
try {
|
|
21
|
+
context = await loadCampaignDeliveryContext(id)
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return dashboardOperationalError(error, 'Campaign delivery prerequisites could not be loaded.', 'CampaignSendAction.prerequisites')
|
|
25
|
+
}
|
|
26
|
+
if (!context.campaign)
|
|
18
27
|
return response.json({ message: 'Campaign not found.' }, 404)
|
|
19
28
|
|
|
20
|
-
const listId = Number(campaign.get('email_list_id') || 0)
|
|
21
|
-
const list = listId ? await EmailList.find(listId) : null
|
|
22
|
-
const memberRow = listId
|
|
23
|
-
? await db
|
|
24
|
-
.selectFrom('email_list_subscribers')
|
|
25
|
-
.select(db.fn.count('id').as('count'))
|
|
26
|
-
.where('email_list_id', '=', listId)
|
|
27
|
-
.where('status', '=', 'subscribed')
|
|
28
|
-
.executeTakeFirst()
|
|
29
|
-
: null
|
|
30
29
|
const validationError = validateCampaignDelivery(
|
|
31
|
-
campaign,
|
|
30
|
+
context.campaign,
|
|
32
31
|
'send',
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
context.activeMembers,
|
|
33
|
+
context.listStatus,
|
|
35
34
|
)
|
|
36
35
|
if (validationError)
|
|
37
36
|
return response.json({ message: validationError }, 422)
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
try {
|
|
39
|
+
await campaigns.sendNow(id)
|
|
40
|
+
return response.json({ id, status: 'sending' }, 202)
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof CampaignStateConflictError)
|
|
44
|
+
return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
|
|
45
|
+
return dashboardOperationalError(error, 'Campaign could not be queued.', 'CampaignSendAction.queue', 500)
|
|
46
|
+
}
|
|
41
47
|
},
|
|
42
48
|
})
|
|
@@ -4,6 +4,7 @@ import { config } from '@stacksjs/config'
|
|
|
4
4
|
import { Campaign } from '@stacksjs/orm'
|
|
5
5
|
import { response } from '@stacksjs/router'
|
|
6
6
|
import { campaignWriteData, validateCampaignWriteData } from './campaign-records'
|
|
7
|
+
import { marketingModelError } from './marketing-response'
|
|
7
8
|
|
|
8
9
|
export default new Action({
|
|
9
10
|
name: 'CampaignStoreAction',
|
|
@@ -12,7 +13,6 @@ export default new Action({
|
|
|
12
13
|
model: Campaign,
|
|
13
14
|
|
|
14
15
|
async handle(request: RequestInstance) {
|
|
15
|
-
await request.validate()
|
|
16
16
|
const data = campaignWriteData(
|
|
17
17
|
await request.all(),
|
|
18
18
|
String((config as any).commerce?.currency || 'USD').toUpperCase(),
|
|
@@ -23,14 +23,19 @@ export default new Action({
|
|
|
23
23
|
if (['sending', 'sent'].includes(data.status))
|
|
24
24
|
return response.json({ message: 'Campaign delivery status is managed by the send pipeline.' }, 422)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
try {
|
|
27
|
+
const campaign = await Campaign.create({
|
|
28
|
+
...data,
|
|
29
|
+
audience_size: 0,
|
|
30
|
+
sent_count: 0,
|
|
31
|
+
open_rate: null,
|
|
32
|
+
click_rate: null,
|
|
33
|
+
conversion_rate: null,
|
|
34
|
+
})
|
|
35
|
+
return response.json({ id: campaign.get('id') }, 201)
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return marketingModelError(error, 'Campaign could not be created.', 'CampaignStoreAction')
|
|
39
|
+
}
|
|
35
40
|
},
|
|
36
41
|
})
|
|
@@ -4,6 +4,7 @@ import { config } from '@stacksjs/config'
|
|
|
4
4
|
import { Campaign } from '@stacksjs/orm'
|
|
5
5
|
import { response } from '@stacksjs/router'
|
|
6
6
|
import { campaignWriteData, validateCampaignWriteData } from './campaign-records'
|
|
7
|
+
import { marketingModelError, marketingRecordId } from './marketing-response'
|
|
7
8
|
|
|
8
9
|
export default new Action({
|
|
9
10
|
name: 'CampaignUpdateAction',
|
|
@@ -12,16 +13,9 @@ export default new Action({
|
|
|
12
13
|
model: Campaign,
|
|
13
14
|
|
|
14
15
|
async handle(request: RequestInstance) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!campaign)
|
|
19
|
-
return response.json({ message: 'Campaign not found.' }, 404)
|
|
20
|
-
|
|
21
|
-
const currentStatus = String(campaign.get('status') || '')
|
|
22
|
-
if (['sending', 'sent'].includes(currentStatus))
|
|
23
|
-
return response.json({ message: 'Campaigns that entered delivery cannot be edited.' }, 409)
|
|
24
|
-
|
|
16
|
+
const id = marketingRecordId(request)
|
|
17
|
+
if (!id)
|
|
18
|
+
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
25
19
|
const data = campaignWriteData(
|
|
26
20
|
await request.all(),
|
|
27
21
|
String((config as any).commerce?.currency || 'USD').toUpperCase(),
|
|
@@ -32,7 +26,20 @@ export default new Action({
|
|
|
32
26
|
if (['sending', 'sent'].includes(data.status))
|
|
33
27
|
return response.json({ message: 'Campaign delivery status is managed by the send pipeline.' }, 422)
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
try {
|
|
30
|
+
const campaign = await Campaign.find(id)
|
|
31
|
+
if (!campaign)
|
|
32
|
+
return response.json({ message: 'Campaign not found.' }, 404)
|
|
33
|
+
|
|
34
|
+
const currentStatus = String(campaign.get('status') || '')
|
|
35
|
+
if (['sending', 'sent'].includes(currentStatus))
|
|
36
|
+
return response.json({ message: 'Campaigns that entered delivery cannot be edited.' }, 409)
|
|
37
|
+
|
|
38
|
+
await campaign.update(data)
|
|
39
|
+
return response.json({ id })
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return marketingModelError(error, 'Campaign could not be updated.', 'CampaignUpdateAction')
|
|
43
|
+
}
|
|
37
44
|
},
|
|
38
45
|
})
|
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
4
|
import { EmailList } from '@stacksjs/orm'
|
|
5
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { marketingModelError, marketingRecordId } from './marketing-response'
|
|
6
7
|
|
|
7
8
|
export default new Action({
|
|
8
9
|
name: 'MarketingListDestroyAction',
|
|
@@ -10,32 +11,40 @@ export default new Action({
|
|
|
10
11
|
method: 'DELETE',
|
|
11
12
|
|
|
12
13
|
async handle(request: RequestInstance) {
|
|
13
|
-
const id =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return response.json({ message: 'Email list not found.' }, 404)
|
|
14
|
+
const id = marketingRecordId(request)
|
|
15
|
+
if (!id)
|
|
16
|
+
return response.json({ message: 'A valid email list id is required.' }, 400)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
try {
|
|
19
|
+
const list = await EmailList.find(id)
|
|
20
|
+
if (!list)
|
|
21
|
+
return response.json({ message: 'Email list not found.' }, 404)
|
|
22
|
+
|
|
23
|
+
const [membership, campaign] = await Promise.all([
|
|
24
|
+
db
|
|
25
|
+
.selectFrom('email_list_subscribers')
|
|
26
|
+
.select(db.fn.count('id').as('count'))
|
|
27
|
+
.where('email_list_id', '=', id)
|
|
28
|
+
.executeTakeFirst(),
|
|
29
|
+
db
|
|
30
|
+
.selectFrom('campaigns')
|
|
31
|
+
.select(db.fn.count('id').as('count'))
|
|
32
|
+
.where('email_list_id', '=', id)
|
|
33
|
+
.executeTakeFirst(),
|
|
34
|
+
])
|
|
35
|
+
const membershipCount = Number(membership?.count || 0)
|
|
36
|
+
const campaignCount = Number(campaign?.count || 0)
|
|
37
|
+
if (membershipCount > 0 || campaignCount > 0) {
|
|
38
|
+
return response.json({
|
|
39
|
+
message: 'Archive this list instead. Lists with memberships or campaigns cannot be deleted.',
|
|
40
|
+
}, 409)
|
|
41
|
+
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
await list.delete()
|
|
44
|
+
return response.noContent()
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return marketingModelError(error, 'Email list could not be deleted.', 'ListDestroyAction')
|
|
48
|
+
}
|
|
40
49
|
},
|
|
41
50
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
3
|
import { EmailList } from '@stacksjs/orm'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
5
|
import { normalizeMarketingLists } from './marketing-list-records'
|
|
5
6
|
|
|
6
7
|
export default new Action({
|
|
@@ -15,32 +16,37 @@ export default new Action({
|
|
|
15
16
|
.slice(0, 19)
|
|
16
17
|
.replace('T', ' ')
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
try {
|
|
20
|
+
const [lists, membershipRows, newMembershipRows, campaignRows] = await Promise.all([
|
|
21
|
+
EmailList.orderBy('name', 'asc').get(),
|
|
22
|
+
db
|
|
23
|
+
.selectFrom('email_list_subscribers')
|
|
24
|
+
.select(['email_list_id', 'status', db.fn.count('id').as('count')])
|
|
25
|
+
.groupBy(['email_list_id', 'status'])
|
|
26
|
+
.execute(),
|
|
27
|
+
db
|
|
28
|
+
.selectFrom('email_list_subscribers')
|
|
29
|
+
.select(['email_list_id', db.fn.count('id').as('count')])
|
|
30
|
+
.where('status', '=', 'subscribed')
|
|
31
|
+
.where('subscribed_at', '>=', weekStart)
|
|
32
|
+
.groupBy('email_list_id')
|
|
33
|
+
.execute(),
|
|
34
|
+
db
|
|
35
|
+
.selectFrom('campaigns')
|
|
36
|
+
.select([
|
|
37
|
+
'email_list_id',
|
|
38
|
+
db.fn.count('id').as('count'),
|
|
39
|
+
db.fn.max('sent_at').as('last_sent_at'),
|
|
40
|
+
])
|
|
41
|
+
.whereNotNull('email_list_id')
|
|
42
|
+
.groupBy('email_list_id')
|
|
43
|
+
.execute(),
|
|
44
|
+
])
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
return normalizeMarketingLists(lists, membershipRows, newMembershipRows, campaignRows)
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return dashboardOperationalError(error, 'Email lists could not be loaded.', 'ListIndexAction')
|
|
50
|
+
}
|
|
45
51
|
},
|
|
46
52
|
})
|