@stacksjs/defaults 0.75.262 → 0.75.264
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-api/SKILL.md +5 -4
- package/ai/skills/stacks-browse/SKILL.md +1 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +40 -4
- package/ai/skills/stacks-email/SKILL.md +12 -8
- package/ai/skills/stacks-env/SKILL.md +1 -1
- package/ai/skills/stacks-lint/SKILL.md +9 -0
- package/ai/skills/stacks-mail/SKILL.md +1 -1
- package/ai/skills/stacks-migrations/SKILL.md +32 -1
- package/ai/skills/stacks-models/SKILL.md +20 -0
- package/ai/skills/stacks-native/CATALOG.md +19 -19
- package/ai/skills/stacks-scheduler/SKILL.md +11 -0
- package/ai/skills/stacks-testing/SKILL.md +14 -0
- package/app/Models/Content/Author.ts +3 -3
- package/app/Models/User.ts +4 -4
- package/bootstrap.ts +122 -102
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
- package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
- package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
- package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
- package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
package/bootstrap.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
|
|
24
24
|
import { config, feature } from '@stacksjs/config'
|
|
25
25
|
import { frameworkPath } from '@stacksjs/path'
|
|
26
|
-
import {
|
|
26
|
+
import { log } from '@stacksjs/logging/runtime'
|
|
27
|
+
import { bundleMounts, DEFAULT_ROUTE_BUNDLE_FEATURES, route, withRouteAdmission } from '@stacksjs/router'
|
|
27
28
|
import MaintenanceMiddleware from './app/Middleware/Maintenance'
|
|
28
29
|
|
|
29
30
|
// Global maintenance / coming-soon gate. Registered first so the
|
|
@@ -71,116 +72,135 @@ function mounts(bundle: keyof typeof DEFAULT_ROUTE_BUNDLE_FEATURES): boolean {
|
|
|
71
72
|
return bundleMounts(selection, bundle, feature(DEFAULT_ROUTE_BUNDLE_FEATURES[bundle]))
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// currently running with `dashboard` off. Widening an app's public surface on
|
|
88
|
-
// upgrade is not something a refactor gets to do silently.
|
|
89
|
-
if (mounts('auth'))
|
|
90
|
-
await route.register(frameworkPath('defaults/routes/auth.ts'))
|
|
91
|
-
|
|
92
|
-
// The rest of dashboard.ts: email subscribe, storefront cart/checkout,
|
|
93
|
-
// reviews, sitemap, AI, voice, and the admin dashboard's REST surface. Still
|
|
94
|
-
// one file and still one gate — splitting auth out was the case with a
|
|
95
|
-
// reporter behind it; marketing.ts / commerce.ts / monitoring.ts remain the
|
|
96
|
-
// obvious next cuts.
|
|
97
|
-
//
|
|
98
|
-
// Apps that need only a slice can also define the routes they want directly
|
|
99
|
-
// in `routes/api.ts` — first-registration-wins means the user version takes
|
|
100
|
-
// priority.
|
|
101
|
-
if (mounts('dashboard')) {
|
|
102
|
-
await route.register(frameworkPath('defaults/routes/dashboard.ts'))
|
|
103
|
-
// JSON endpoints for the dev dashboard UI. Kept separate from the view
|
|
104
|
-
// routes above so the data layer is one obvious file to grep.
|
|
105
|
-
await route.register(frameworkPath('defaults/routes/dashboard-api.ts'))
|
|
106
|
-
// The dev dashboard boots through this file rather than the application
|
|
107
|
-
// router's importRoutes() path, so load model-declared useApi routes here too.
|
|
75
|
+
/**
|
|
76
|
+
* Every default bundle this app mounts, registered in one place so the
|
|
77
|
+
* admission check below covers all of them.
|
|
78
|
+
*/
|
|
79
|
+
async function registerDefaultBundles(): Promise<void> {
|
|
80
|
+
// Auth: login, registration, logout, refresh/revoke, passkeys, TOTP 2FA and
|
|
81
|
+
// password reset. Split out of dashboard.ts so it can be mounted on its own
|
|
82
|
+
// (stacksjs/stacks#2229) — previously the only way to get `/login` was to
|
|
83
|
+
// activate `dashboard` and take the storefront, reviews, AI and voice surface
|
|
84
|
+
// with it.
|
|
85
|
+
//
|
|
86
|
+
// Registered BEFORE dashboard.ts, which is where these lived, so the
|
|
87
|
+
// first-registration-wins order among framework routes is unchanged.
|
|
108
88
|
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
// Deliberately NOT gated on `feature('auth')`, despite the issue asking for
|
|
90
|
+
// it: `config/auth.ts` ships in every app with `enabled: true`, so that gate
|
|
91
|
+
// is true everywhere and would mount the auth surface — including
|
|
92
|
+
// `/generate-two-factor-secret`, `/logout-all` and `/auth/tokens` — in apps
|
|
93
|
+
// currently running with `dashboard` off. Widening an app's public surface on
|
|
94
|
+
// upgrade is not something a refactor gets to do silently.
|
|
95
|
+
if (mounts('auth'))
|
|
96
|
+
await route.register(frameworkPath('defaults/routes/auth.ts'))
|
|
97
|
+
|
|
98
|
+
// The rest of dashboard.ts: email subscribe, storefront cart/checkout,
|
|
99
|
+
// reviews, sitemap, AI, voice, and the admin dashboard's REST surface. Still
|
|
100
|
+
// one file and still one gate — splitting auth out was the case with a
|
|
101
|
+
// reporter behind it; marketing.ts / commerce.ts / monitoring.ts remain the
|
|
102
|
+
// obvious next cuts.
|
|
118
103
|
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
104
|
+
// Apps that need only a slice can also define the routes they want directly
|
|
105
|
+
// in `routes/api.ts` — first-registration-wins means the user version takes
|
|
106
|
+
// priority.
|
|
107
|
+
if (mounts('dashboard')) {
|
|
108
|
+
await route.register(frameworkPath('defaults/routes/dashboard.ts'))
|
|
109
|
+
// JSON endpoints for the dev dashboard UI. Kept separate from the view
|
|
110
|
+
// routes above so the data layer is one obvious file to grep.
|
|
111
|
+
await route.register(frameworkPath('defaults/routes/dashboard-api.ts'))
|
|
112
|
+
// The dev dashboard boots through this file rather than the application
|
|
113
|
+
// router's importRoutes() path, so load model-declared useApi routes here too.
|
|
114
|
+
//
|
|
115
|
+
// Package first, vendored second, matching importRoutes() and start.ts. This
|
|
116
|
+
// line used to import the vendored copy only, and nothing re-vendors that
|
|
117
|
+
// file: `@stacksjs/orm` publishes `dist/routes.js` and no `routes.ts`, so an
|
|
118
|
+
// app kept whatever generator its copy froze at however often it upgraded.
|
|
119
|
+
// An old enough copy compares route paths literally, so a generated
|
|
120
|
+
// `PATCH /api/sites/{id}` does not recognise a hand-written
|
|
121
|
+
// `/api/sites/{siteId}` as the same endpoint and registers alongside it,
|
|
122
|
+
// and the hand-written handler's authorization check stops running
|
|
123
|
+
// (stacksjs/stacks#2364).
|
|
124
|
+
//
|
|
125
|
+
// Held in a variable so the specifier resolves at runtime rather than while
|
|
126
|
+
// transpiling, where an unresolvable literal would fail this module instead
|
|
127
|
+
// of throwing where it can be caught.
|
|
128
|
+
const ormRoutesPackage = '@stacksjs/orm/routes'
|
|
129
|
+
try {
|
|
130
|
+
await import(ormRoutesPackage)
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
await import(frameworkPath('orm/routes.ts'))
|
|
134
|
+
}
|
|
125
135
|
}
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
|
|
137
|
+
// Email webhook + unsubscribe routes (stacksjs/stacks#1881, #1880).
|
|
138
|
+
// Always mounted when the `email` feature is on — the underlying
|
|
139
|
+
// handlers self-disable when their provider credentials aren't
|
|
140
|
+
// configured (they 401 rather than processing). Apps that need a
|
|
141
|
+
// non-default mount path register their own routes in `routes/api.ts`
|
|
142
|
+
// and the framework's mount silently no-ops since user routes
|
|
143
|
+
// register first.
|
|
144
|
+
if (mounts('email')) {
|
|
145
|
+
await route.register(frameworkPath('defaults/routes/email.ts'))
|
|
128
146
|
}
|
|
129
|
-
}
|
|
130
147
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// register first.
|
|
138
|
-
if (mounts('email')) {
|
|
139
|
-
await route.register(frameworkPath('defaults/routes/email.ts'))
|
|
140
|
-
}
|
|
148
|
+
// Form-builder public endpoints (`@stacksjs/forms`). Gated on the `forms`
|
|
149
|
+
// feature, which defaults OFF - an app opts in with config/forms.ts
|
|
150
|
+
// (`buddy forms:install`) and only then do the public submit routes exist.
|
|
151
|
+
if (mounts('forms')) {
|
|
152
|
+
await route.register(frameworkPath('defaults/routes/forms.ts'))
|
|
153
|
+
}
|
|
141
154
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
if (mounts('
|
|
146
|
-
|
|
147
|
-
}
|
|
155
|
+
// Courier delivery endpoints: position ingest and the stop/route lifecycle a
|
|
156
|
+
// courier's device drives. Gated with the rest of the commerce surface, since
|
|
157
|
+
// they are meaningless without the delivery models behind them.
|
|
158
|
+
if (mounts('delivery')) {
|
|
159
|
+
await route.register(frameworkPath('defaults/routes/delivery.ts'))
|
|
160
|
+
}
|
|
148
161
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
if (mounts('
|
|
153
|
-
|
|
154
|
-
}
|
|
162
|
+
// The payment provider's webhook, which applies payments and refunds to
|
|
163
|
+
// orders. Gated with the commerce surface; it answers 401 until the driver's
|
|
164
|
+
// webhook secret is configured.
|
|
165
|
+
if (mounts('payments')) {
|
|
166
|
+
await route.register(frameworkPath('defaults/routes/payments.ts'))
|
|
167
|
+
}
|
|
155
168
|
|
|
156
|
-
// The payment
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
// The page a Paddle payment link opens: Paddle has no hosted checkout, so
|
|
170
|
+
// its default payment link has to point at a page of the app's that loads
|
|
171
|
+
// Paddle.js. Mounted while the app pays through Paddle, as part of the
|
|
172
|
+
// `payments` bundle when the app names its bundles.
|
|
173
|
+
const paymentDriverName = (config as { payment?: { driver?: string } }).payment?.driver
|
|
174
|
+
if (paymentDriverName === 'paddle' && (selection?.explicit ? selection.bundles.has('payments') : true)) {
|
|
175
|
+
await route.register(frameworkPath('defaults/routes/paddle.ts'))
|
|
176
|
+
}
|
|
162
177
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// `
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// Social sign-in: `/auth/{provider}` + `/auth/{provider}/callback`
|
|
179
|
+
// (stacksjs/stacks#2276). An opt-in bundle, NOT part of the implicit default
|
|
180
|
+
// set or `all` — OAuth callback URLs in an app that configured no provider
|
|
181
|
+
// are surface for nothing. So `mounts()` does not apply here: an app that
|
|
182
|
+
// NAMED its bundles decides outright (`social` listed → on, absent → off),
|
|
183
|
+
// and an app that said nothing gets it exactly when a provider is actually
|
|
184
|
+
// configured in config/services.ts — configuring GitHub and finding
|
|
185
|
+
// /auth/github dead would be the puzzle, not the mount.
|
|
186
|
+
const { configuredSocialProviders } = await import('@stacksjs/socials')
|
|
187
|
+
const mountSocial = selection?.explicit
|
|
188
|
+
? selection.bundles.has('social')
|
|
189
|
+
: configuredSocialProviders().length > 0
|
|
190
|
+
if (mountSocial) {
|
|
191
|
+
await route.register(frameworkPath('defaults/routes/socials.ts'))
|
|
192
|
+
}
|
|
170
193
|
}
|
|
171
194
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// are
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
: configuredSocialProviders().length > 0
|
|
184
|
-
if (mountSocial) {
|
|
185
|
-
await route.register(frameworkPath('defaults/routes/socials.ts'))
|
|
195
|
+
// Register the bundles, withholding any route whose framework action depends
|
|
196
|
+
// on a model this app excluded from its schema and whose table its database
|
|
197
|
+
// does not have. Those routes could only answer 500 (stacksjs/stacks#2943); now
|
|
198
|
+
// they are absent, and boot says which and why. When the table list cannot be
|
|
199
|
+
// read, nothing is withheld.
|
|
200
|
+
const { frameworkRouteAdmission } = await import('@stacksjs/database')
|
|
201
|
+
const withheld = await withRouteAdmission(await frameworkRouteAdmission(), registerDefaultBundles)
|
|
202
|
+
if (withheld.length > 0) {
|
|
203
|
+
const shown = withheld.slice(0, 5).map(r => `${r.method} ${r.path} -> ${r.handler} needs ${r.reason}`)
|
|
204
|
+
const more = withheld.length > shown.length ? `\n ... and ${withheld.length - shown.length} more` : ''
|
|
205
|
+
log.warn(`[Routes] Not registering ${withheld.length} framework route(s) whose models are not in this app's schema (database.models.includeFrameworkDefaults is false) and whose tables do not exist:\n ${shown.join('\n ')}${more}\nPublish a model with \`buddy publish model <Name>\` or set includeFrameworkDefaults to serve them.`)
|
|
186
206
|
}
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.264",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.264",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@iconify-json/f7": "^1.2.2",
|
|
47
47
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
48
|
-
"@stacksjs/mobile": "0.75.
|
|
49
|
-
"@stacksjs/sanitizer": "0.2.
|
|
50
|
-
"@stacksjs/stx": "0.2.
|
|
48
|
+
"@stacksjs/mobile": "0.75.264",
|
|
49
|
+
"@stacksjs/sanitizer": "0.2.663",
|
|
50
|
+
"@stacksjs/stx": "0.2.663",
|
|
51
51
|
"@xterm/addon-fit": "^0.11.0",
|
|
52
52
|
"@xterm/xterm": "^6.0.0",
|
|
53
53
|
"ts-qr-codes": "^0.1.8"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
import { authStore } from '../../../../views/dashboard/stores/auth'
|
|
@@ -59,11 +61,31 @@ function tokenStatus(token: AccessTokenRow): 'active' | 'expired' {
|
|
|
59
61
|
return new Date(token.expires_at).getTime() <= Date.now() ? 'expired' : 'active'
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const TOKEN_COLUMNS = [
|
|
65
|
+
{ key: 'name', label: 'Token', sortable: true, subtitleKey: 'createdLabel', class: 'font-medium text-neutral-900 dark:text-white' },
|
|
66
|
+
{ key: 'scopesLabel', label: 'Scopes', slotted: true },
|
|
67
|
+
{
|
|
68
|
+
key: 'status',
|
|
69
|
+
label: 'Status',
|
|
70
|
+
type: 'badge' as const,
|
|
71
|
+
sortable: true,
|
|
72
|
+
badgeClasses: {
|
|
73
|
+
active: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
74
|
+
expired: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
75
|
+
},
|
|
76
|
+
badgeLabels: { active: 'Active', expired: 'Expired' },
|
|
77
|
+
},
|
|
78
|
+
{ key: 'expires_at', label: 'Expires', align: 'right' as const, sortable: true, slotted: true, searchable: false },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
const tokenRows = derived(() => tokens().map(token => ({
|
|
82
|
+
...token,
|
|
83
|
+
createdLabel: `Created ${formatDate(token.created_at)}`,
|
|
84
|
+
// Searchable text for the scope pills the cell draws.
|
|
85
|
+
scopesLabel: token.scopes.join(' '),
|
|
86
|
+
status: tokenStatus(token),
|
|
87
|
+
expiresLabel: formatDate(token.expires_at),
|
|
88
|
+
})))
|
|
67
89
|
|
|
68
90
|
function formatDate(value: string | null): string {
|
|
69
91
|
if (!value)
|
|
@@ -270,38 +292,36 @@ onMount(() => {
|
|
|
270
292
|
</template>
|
|
271
293
|
</EmptyState>
|
|
272
294
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<
|
|
298
|
-
<Button variant="danger" size="sm"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
</tbody>
|
|
304
|
-
</table>
|
|
295
|
+
{{--
|
|
296
|
+
The page keeps its loading, sign-in and empty states; the table brings
|
|
297
|
+
sorting and paging. Scopes are slotted for their pills and expiry for
|
|
298
|
+
"Never", so it sorts on the date itself.
|
|
299
|
+
--}}
|
|
300
|
+
<div :if="!loading() && !authenticationRequired() && tokens().length > 0">
|
|
301
|
+
<Table :columns="TOKEN_COLUMNS" :data="tokenRows()" paginated :page-size="25">
|
|
302
|
+
<template #cell="{ row, column }">
|
|
303
|
+
<template :if="column.key === 'scopesLabel'">
|
|
304
|
+
<div class="flex flex-wrap gap-1">
|
|
305
|
+
<template :for="scope in row.scopes" :key="scope">
|
|
306
|
+
<span class="inline-flex px-2 py-0.5 font-medium text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded" x-text="scope"></span>
|
|
307
|
+
</template>
|
|
308
|
+
</div>
|
|
309
|
+
</template>
|
|
310
|
+
<template :else>
|
|
311
|
+
<span class="text-neutral-500 text-xs whitespace-nowrap dark:text-neutral-400" x-text="row.expiresLabel"></span>
|
|
312
|
+
</template>
|
|
313
|
+
</template>
|
|
314
|
+
<template #actions="{ row }">
|
|
315
|
+
<div class="text-right whitespace-nowrap">
|
|
316
|
+
<template :if="revoking() === row.id">
|
|
317
|
+
<Button variant="danger" size="sm" loading>Revoking...</Button>
|
|
318
|
+
</template>
|
|
319
|
+
<template :else>
|
|
320
|
+
<Button variant="danger" size="sm" @click="confirmingRevoke.set(row.id)">Revoke</Button>
|
|
321
|
+
</template>
|
|
322
|
+
</div>
|
|
323
|
+
</template>
|
|
324
|
+
</Table>
|
|
305
325
|
</div>
|
|
306
326
|
</Card>
|
|
307
327
|
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { BillingTransaction } from '../../../../functions/billing/dashboard'
|
|
3
5
|
|
|
4
6
|
const transactions = useReactiveProp('transactions', [] as BillingTransaction[])
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const COLUMNS = [
|
|
9
|
+
{ key: 'name', label: 'Transaction', sortable: true, emptyText: 'Unnamed transaction', subtitleKey: 'description', class: 'font-medium text-gray-900 max-w-lg dark:text-white' },
|
|
10
|
+
{
|
|
11
|
+
key: 'typeLabel',
|
|
12
|
+
label: 'Type',
|
|
13
|
+
type: 'badge' as const,
|
|
14
|
+
sortable: true,
|
|
15
|
+
},
|
|
16
|
+
{ key: 'amountLabel', label: 'Amount', class: 'whitespace-nowrap', searchable: false },
|
|
17
|
+
{ key: 'providerId', label: 'Provider reference', emptyText: 'Not reported', class: 'font-mono text-xs' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const rows = derived(() => transactions().map(transaction => ({
|
|
21
|
+
...transaction,
|
|
22
|
+
typeLabel: (transaction.type || 'unknown').replace(/\b\w/g, letter => letter.toUpperCase()),
|
|
23
|
+
amountLabel: transaction.amount === null ? 'Not reported' : `${new Intl.NumberFormat().format(transaction.amount)} minor units`,
|
|
24
|
+
})))
|
|
11
25
|
</script>
|
|
12
26
|
|
|
13
27
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
@@ -19,32 +33,12 @@ function amount(transaction: BillingTransaction): string {
|
|
|
19
33
|
<span class="h-5 w-5 text-gray-400 i-hugeicons-transaction-history"></span>
|
|
20
34
|
</header>
|
|
21
35
|
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<thead class="bg-gray-50 dark:bg-neutral-900/50">
|
|
31
|
-
<tr>
|
|
32
|
-
<th scope="col" class="px-5 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Transaction</th>
|
|
33
|
-
<th scope="col" class="px-5 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Type</th>
|
|
34
|
-
<th scope="col" class="px-5 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Amount</th>
|
|
35
|
-
<th scope="col" class="px-5 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Provider reference</th>
|
|
36
|
-
</tr>
|
|
37
|
-
</thead>
|
|
38
|
-
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
39
|
-
<template :for="transaction in transactions()" :key="transaction.id">
|
|
40
|
-
<tr>
|
|
41
|
-
<td class="px-5 py-4 max-w-lg"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ transaction.name || 'Unnamed transaction' }}</p><p :if="transaction.description" class="mt-1 text-gray-500 text-xs truncate dark:text-neutral-400">{{ transaction.description }}</p></td>
|
|
42
|
-
<td class="px-5 py-4"><span class="inline-flex px-2 py-1 capitalize font-medium text-gray-700 text-xs dark:text-neutral-200 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ transaction.type || 'unknown' }}</span></td>
|
|
43
|
-
<td class="px-5 py-4 text-gray-700 text-sm whitespace-nowrap dark:text-neutral-200">{{ amount(transaction) }}</td>
|
|
44
|
-
<td class="px-5 py-4 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ transaction.providerId || 'Not reported' }}</td>
|
|
45
|
-
</tr>
|
|
46
|
-
</template>
|
|
47
|
-
</tbody>
|
|
48
|
-
</table>
|
|
49
|
-
</div>
|
|
36
|
+
<Table
|
|
37
|
+
:columns="COLUMNS"
|
|
38
|
+
:data="rows()"
|
|
39
|
+
paginated
|
|
40
|
+
:page-size="25"
|
|
41
|
+
empty-title="No transactions recorded"
|
|
42
|
+
empty-description="Successful persisted billing transactions will appear here."
|
|
43
|
+
/>
|
|
50
44
|
</section>
|