@stacksjs/defaults 0.70.357 → 0.70.362
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 +10 -5
- package/ai/skills/stacks-browse/scripts/browse.ts +109 -13
- package/app/Actions/AI/AskAction.ts +12 -17
- package/app/Actions/AI/SummaryAction.ts +12 -17
- 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/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/app/Actions/LogAction.ts +11 -11
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
- package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
- package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +51 -53
- package/resources/components/Dashboard/UI/Modal.stx +83 -29
- package/views/dashboard/stores/auth.ts +18 -2
|
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
2
2
|
import { Order, Post, Product, Request, User } from '@stacksjs/orm'
|
|
3
3
|
import { checkApplicationHealth, type ApplicationHealthCheck } from '@stacksjs/router'
|
|
4
4
|
import { formatRelative, safeGet } from '../../../resources/functions/dashboard/data'
|
|
5
|
+
import { dashboardOperationalIssue } from './dashboard-response'
|
|
5
6
|
|
|
6
7
|
interface HttpRequestSample {
|
|
7
8
|
duration: number
|
|
@@ -28,7 +29,7 @@ export function serializeHealthCheck(name: string, check: ApplicationHealthCheck
|
|
|
28
29
|
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
29
30
|
status: check.ok ? 'healthy' : 'critical',
|
|
30
31
|
latency: `${check.ms}ms`,
|
|
31
|
-
detail: check.
|
|
32
|
+
detail: check.ok ? '' : 'Dependency probe failed.',
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -43,7 +44,11 @@ function issue(source: string, result: PromiseSettledResult<unknown>) {
|
|
|
43
44
|
return result.status === 'rejected'
|
|
44
45
|
? {
|
|
45
46
|
source,
|
|
46
|
-
message:
|
|
47
|
+
message: dashboardOperationalIssue(
|
|
48
|
+
result.reason,
|
|
49
|
+
`${source} data could not be loaded.`,
|
|
50
|
+
`DashboardHomeAction.${source.toLowerCase().replaceAll(' ', '-')}`,
|
|
51
|
+
),
|
|
47
52
|
}
|
|
48
53
|
: null
|
|
49
54
|
}
|
|
@@ -65,7 +70,7 @@ export default new Action({
|
|
|
65
70
|
Request.count(),
|
|
66
71
|
Request.orderBy('created_at', 'desc').limit(1000).get(),
|
|
67
72
|
])
|
|
68
|
-
const
|
|
73
|
+
const healthResult = await Promise.allSettled([checkApplicationHealth()])
|
|
69
74
|
|
|
70
75
|
const [
|
|
71
76
|
userCount,
|
|
@@ -93,7 +98,19 @@ export default new Action({
|
|
|
93
98
|
})))
|
|
94
99
|
: summarizeHttpRequests(0, [])
|
|
95
100
|
|
|
96
|
-
const
|
|
101
|
+
const health = healthResult[0]
|
|
102
|
+
const services = health.status === 'fulfilled'
|
|
103
|
+
? Object.entries(health.value.checks).map(([name, check]) => {
|
|
104
|
+
if (!check.ok) {
|
|
105
|
+
dashboardOperationalIssue(
|
|
106
|
+
check.message,
|
|
107
|
+
'Dependency probe failed.',
|
|
108
|
+
`DashboardHomeAction.health.${name}`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return serializeHealthCheck(name, check)
|
|
112
|
+
})
|
|
113
|
+
: []
|
|
97
114
|
|
|
98
115
|
const activities = [
|
|
99
116
|
...(recentOrders.status === 'fulfilled' ? recentOrders.value : []).map((order: any) => ({
|
|
@@ -127,6 +144,16 @@ export default new Action({
|
|
|
127
144
|
const issues = modelResults
|
|
128
145
|
.map((result, index) => issue(sources[index], result))
|
|
129
146
|
.filter((entry): entry is { source: string, message: string } => entry !== null)
|
|
147
|
+
if (health.status === 'rejected') {
|
|
148
|
+
issues.push({
|
|
149
|
+
source: 'System health',
|
|
150
|
+
message: dashboardOperationalIssue(
|
|
151
|
+
health.reason,
|
|
152
|
+
'System health could not be loaded.',
|
|
153
|
+
'DashboardHomeAction.health',
|
|
154
|
+
),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
130
157
|
|
|
131
158
|
return { stats, httpMetrics, services, activities, issues }
|
|
132
159
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { Customer, Order, Post, User } from '@stacksjs/orm'
|
|
3
|
+
import { dashboardOperationalIssue } from './dashboard-response'
|
|
3
4
|
|
|
4
5
|
interface DashboardStatDefinition {
|
|
5
6
|
title: string
|
|
@@ -61,7 +62,11 @@ export default new Action({
|
|
|
61
62
|
const issues = results.flatMap((result, index) => result.status === 'rejected'
|
|
62
63
|
? [{
|
|
63
64
|
source: definitions[index].title,
|
|
64
|
-
message:
|
|
65
|
+
message: dashboardOperationalIssue(
|
|
66
|
+
result.reason,
|
|
67
|
+
`${definitions[index].title} data could not be loaded.`,
|
|
68
|
+
`DashboardStatsAction.${definitions[index].title.toLowerCase().replaceAll(' ', '-')}`,
|
|
69
|
+
),
|
|
65
70
|
}]
|
|
66
71
|
: [])
|
|
67
72
|
|
|
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { notify } from '@stacksjs/notifications'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { schema } from '@stacksjs/validation'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
7
|
|
|
7
8
|
export default new Action({
|
|
8
9
|
name: 'InboxSendAction',
|
|
@@ -29,15 +30,21 @@ export default new Action({
|
|
|
29
30
|
const subject = String(request.get('subject') || '').trim()
|
|
30
31
|
const body = String(request.get('body') || '').trim()
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
let result
|
|
34
|
+
try {
|
|
35
|
+
[result] = await notify(
|
|
36
|
+
{ email: to },
|
|
37
|
+
{ subject, body, data: { source: 'dashboard-inbox' } },
|
|
38
|
+
['email'],
|
|
39
|
+
{ ignorePreferences: true },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return dashboardOperationalError(error, 'The email could not be sent.', 'InboxSendAction.provider', 502)
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
if (!result?.success)
|
|
40
|
-
return
|
|
47
|
+
return dashboardOperationalError(result?.error, 'The email could not be sent.', 'InboxSendAction.result', 502)
|
|
41
48
|
|
|
42
49
|
return response.json({ success: true })
|
|
43
50
|
},
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
|
3
3
|
import { join, relative } from 'node:path'
|
|
4
4
|
import process from 'node:process'
|
|
5
5
|
import { parseCommandSource } from '../Source/source-inventory'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
7
|
|
|
7
8
|
interface CommandConfig {
|
|
8
9
|
file: string
|
|
@@ -15,35 +16,40 @@ export default new Action({
|
|
|
15
16
|
description: 'Lists commands registered by the application.',
|
|
16
17
|
method: 'GET',
|
|
17
18
|
async handle() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
try {
|
|
20
|
+
const projectRoot = process.cwd()
|
|
21
|
+
const registryPath = join(projectRoot, 'app/Commands.ts')
|
|
22
|
+
const registryModule = await import(registryPath)
|
|
23
|
+
const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
|
|
24
|
+
const items = Object.entries(registry).flatMap(([signature, value]) => {
|
|
25
|
+
const config = typeof value === 'string'
|
|
26
|
+
? { file: value, enabled: true, aliases: [] }
|
|
27
|
+
: { enabled: true, aliases: [], ...value }
|
|
28
|
+
const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
|
|
29
|
+
if (!existsSync(file))
|
|
30
|
+
return []
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
return [parseCommandSource(
|
|
33
|
+
readFileSync(file, 'utf8'),
|
|
34
|
+
relative(projectRoot, file),
|
|
35
|
+
signature,
|
|
36
|
+
config.aliases,
|
|
37
|
+
statSync(file).mtime.toISOString(),
|
|
38
|
+
)]
|
|
39
|
+
})
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
return {
|
|
42
|
+
items,
|
|
43
|
+
stats: {
|
|
44
|
+
total: items.length,
|
|
45
|
+
aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
|
|
46
|
+
options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
|
|
47
|
+
registered: Object.keys(registry).length,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return dashboardOperationalError(error, 'Command sources could not be loaded.', 'CommandIndexAction')
|
|
47
53
|
}
|
|
48
54
|
},
|
|
49
55
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
2
3
|
import { readEnvironmentFile } from './environment-file'
|
|
3
4
|
|
|
4
5
|
export default new Action({
|
|
@@ -8,9 +9,14 @@ export default new Action({
|
|
|
8
9
|
apiResponse: true,
|
|
9
10
|
|
|
10
11
|
async handle() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
try {
|
|
13
|
+
return Response.json(
|
|
14
|
+
{ environment: await readEnvironmentFile() },
|
|
15
|
+
{ headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return dashboardOperationalError(error, 'Environment file could not be loaded.', 'EnvironmentIndexAction')
|
|
20
|
+
}
|
|
15
21
|
},
|
|
16
22
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
5
|
import { updateEnvironmentFile } from './environment-file'
|
|
5
6
|
|
|
6
7
|
export default new Action({
|
|
@@ -18,7 +19,13 @@ export default new Action({
|
|
|
18
19
|
if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))
|
|
19
20
|
return response.json({ message: 'A valid environment revision is required.' }, 422)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
let result
|
|
23
|
+
try {
|
|
24
|
+
result = await updateEnvironmentFile(content, revision)
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return dashboardOperationalError(error, 'Environment file could not be saved.', 'EnvironmentUpdateAction', 500)
|
|
28
|
+
}
|
|
22
29
|
if (result.issues?.length) {
|
|
23
30
|
const errors = Object.fromEntries(result.issues.map((issue, index) => [
|
|
24
31
|
issue.line ? String(issue.line) : `file-${index}`,
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { Request } from '@stacksjs/orm'
|
|
3
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
3
4
|
|
|
4
5
|
export default new Action({
|
|
5
6
|
name: 'RequestIndexAction',
|
|
6
7
|
description: 'Returns request history data for the dashboard.',
|
|
7
8
|
method: 'GET',
|
|
8
9
|
async handle() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
try {
|
|
11
|
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
|
|
12
|
+
const [allRequests, total, errorCount, averageDurationMs, requestsLastHour] = await Promise.all([
|
|
13
|
+
Request.orderByDesc('id').limit(100).get(),
|
|
14
|
+
Request.count(),
|
|
15
|
+
Request.where('status_code', '>=', 400).count(),
|
|
16
|
+
Request.avg('duration_ms'),
|
|
17
|
+
Request.where('created_at', '>=', oneHourAgo).count(),
|
|
18
|
+
])
|
|
17
19
|
|
|
18
20
|
const requests = allRequests.map(request => ({
|
|
19
21
|
id: Number(request.get('id') || 0),
|
|
@@ -28,16 +30,20 @@ export default new Action({
|
|
|
28
30
|
createdAt: String(request.get('created_at') || ''),
|
|
29
31
|
}))
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
return {
|
|
34
|
+
requests,
|
|
35
|
+
stats: {
|
|
36
|
+
total,
|
|
37
|
+
errorCount,
|
|
38
|
+
averageDurationMs: Number(averageDurationMs || 0),
|
|
39
|
+
successRate: total > 0 ? ((total - errorCount) / total) * 100 : 0,
|
|
40
|
+
requestsLastHour,
|
|
41
|
+
requestsPerMinute: requestsLastHour / 60,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return dashboardOperationalError(error, 'Request history could not be loaded.', 'RequestIndexAction')
|
|
41
47
|
}
|
|
42
48
|
},
|
|
43
49
|
})
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { tsCloud } from '~/config/cloud'
|
|
3
3
|
import { getDashboardCloudSnapshot } from '../Cloud/cloud-overview'
|
|
4
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
5
|
|
|
5
6
|
export default new Action({
|
|
6
7
|
name: 'ServerIndexAction',
|
|
7
8
|
description: 'Returns configured servers and persisted deployment state.',
|
|
8
9
|
method: 'GET',
|
|
9
10
|
async handle() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
try {
|
|
12
|
+
const snapshot = await getDashboardCloudSnapshot(tsCloud)
|
|
13
|
+
return {
|
|
14
|
+
project: snapshot.project,
|
|
15
|
+
environments: snapshot.environments,
|
|
16
|
+
servers: snapshot.serverDefinitions,
|
|
17
|
+
deployments: snapshot.deployments,
|
|
18
|
+
network: snapshot.resources.filter(resource => resource.category === 'network'),
|
|
19
|
+
events: snapshot.events,
|
|
20
|
+
generatedAt: snapshot.generatedAt,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return dashboardOperationalError(error, 'Server state could not be loaded.', 'ServerIndexAction')
|
|
19
25
|
}
|
|
20
26
|
},
|
|
21
27
|
})
|
|
@@ -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
|
})
|