@stacksjs/defaults 0.75.263 → 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-browse/SKILL.md +1 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +40 -4
- package/ai/skills/stacks-native/CATALOG.md +2 -2
- 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
|
@@ -121,6 +121,7 @@ On a failed step, execution stops and the report retains its number and error,
|
|
|
121
121
|
the completed steps, final page state and requested screenshot.
|
|
122
122
|
Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
|
|
123
123
|
Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
|
|
124
|
+
For asynchronous results, use `{"action":"assert","selector":"button","text":"Sound on","timeoutMs":2000}`. This observes the actual visible result until it matches or the bounded budget expires. Ordinary assertions remain immediate. Only assertions accept `timeoutMs`, an integer from 0 to 30000; protocol and evaluation failures are not retried.
|
|
124
125
|
|
|
125
126
|
### Crawl (whole-site browser audit)
|
|
126
127
|
```bash
|
|
@@ -600,9 +600,12 @@ interface ScenarioStep {
|
|
|
600
600
|
key?: string
|
|
601
601
|
ms?: number
|
|
602
602
|
settle?: number
|
|
603
|
+
timeoutMs?: number
|
|
603
604
|
}
|
|
604
605
|
|
|
605
|
-
|
|
606
|
+
class ScenarioAssertionError extends Error {}
|
|
607
|
+
|
|
608
|
+
export function parseScenarioStep(value: string): ScenarioStep {
|
|
606
609
|
let parsed: unknown
|
|
607
610
|
try {
|
|
608
611
|
parsed = JSON.parse(value)
|
|
@@ -633,11 +636,41 @@ function parseScenarioStep(value: string): ScenarioStep {
|
|
|
633
636
|
throw new TypeError('Scenario action "press" requires a key.')
|
|
634
637
|
if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
|
|
635
638
|
throw new TypeError('Scenario action "wait" requires ms between 0 and 30000.')
|
|
639
|
+
if (step.timeoutMs !== undefined && (action !== 'assert' || !Number.isSafeInteger(step.timeoutMs) || Number(step.timeoutMs) < 0 || Number(step.timeoutMs) > 30_000))
|
|
640
|
+
throw new TypeError('Scenario timeoutMs is supported only by assert actions and must be an integer between 0 and 30000.')
|
|
636
641
|
|
|
637
642
|
return step as unknown as ScenarioStep
|
|
638
643
|
}
|
|
639
644
|
|
|
640
|
-
async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<string, unknown>> {
|
|
645
|
+
export async function runScenarioStep(cdp: Pick<Cdp, 'send'>, step: ScenarioStep): Promise<Record<string, unknown>> {
|
|
646
|
+
if (step.action === 'assert' && step.timeoutMs) {
|
|
647
|
+
const deadline = performance.now() + step.timeoutMs
|
|
648
|
+
let last: ScenarioAssertionError | undefined
|
|
649
|
+
const expired = () => new Error(`${last?.message || `Scenario assert failed (${step.selector || ''})`}; timed out after ${step.timeoutMs}ms`)
|
|
650
|
+
while (true) {
|
|
651
|
+
const budget = deadline - performance.now()
|
|
652
|
+
if (budget <= 0) throw expired()
|
|
653
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
654
|
+
try {
|
|
655
|
+
const expiry = Promise.withResolvers<never>()
|
|
656
|
+
timeout = setTimeout(() => expiry.reject(expired()), budget)
|
|
657
|
+
const answer = await Promise.race([
|
|
658
|
+
runScenarioStep(cdp, { ...step, timeoutMs: 0 }),
|
|
659
|
+
expiry.promise,
|
|
660
|
+
])
|
|
661
|
+
if (performance.now() > deadline) throw expired()
|
|
662
|
+
return answer
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
if (!(error instanceof ScenarioAssertionError)) throw error
|
|
666
|
+
last = error
|
|
667
|
+
const remaining = deadline - performance.now()
|
|
668
|
+
if (remaining <= 0) throw error
|
|
669
|
+
await Bun.sleep(Math.min(50, remaining))
|
|
670
|
+
}
|
|
671
|
+
finally { if (timeout !== undefined) clearTimeout(timeout) }
|
|
672
|
+
}
|
|
673
|
+
}
|
|
641
674
|
if (step.action === 'wait') {
|
|
642
675
|
await Bun.sleep(Number(step.ms))
|
|
643
676
|
return { action: step.action, ms: Number(step.ms), ok: true }
|
|
@@ -740,8 +773,11 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
|
|
|
740
773
|
returnByValue: true,
|
|
741
774
|
})
|
|
742
775
|
const value = result.result?.value as Record<string, unknown> | undefined
|
|
743
|
-
if (!value?.ok)
|
|
776
|
+
if (!value?.ok) {
|
|
777
|
+
if (step.action === 'assert' && value)
|
|
778
|
+
throw new ScenarioAssertionError(`Scenario assert failed: ${String(value.error || 'unknown error')} (${step.selector || ''})`)
|
|
744
779
|
throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
|
|
780
|
+
}
|
|
745
781
|
if (step.action === 'click') {
|
|
746
782
|
const point = value.clickPoint as { x: number, y: number }
|
|
747
783
|
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
|
|
@@ -1196,7 +1232,7 @@ async function main() {
|
|
|
1196
1232
|
}
|
|
1197
1233
|
}
|
|
1198
1234
|
|
|
1199
|
-
main().catch((e) => {
|
|
1235
|
+
if (import.meta.main) main().catch((e) => {
|
|
1200
1236
|
console.error('browse error:', e?.message || e)
|
|
1201
1237
|
process.exit(1)
|
|
1202
1238
|
})
|
|
@@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
21
21
|
| `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
|
|
22
22
|
| `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
|
|
23
23
|
| `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
|
|
24
|
-
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) |
|
|
24
|
+
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 162 |
|
|
25
25
|
| `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 3 |
|
|
26
26
|
| `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 14 |
|
|
27
27
|
| `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
|
|
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
|
|
|
215
215
|
|
|
216
216
|
Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
|
|
217
217
|
|
|
218
|
-
Evidence directory: [
|
|
218
|
+
Evidence directory: [162 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
219
219
|
|
|
220
220
|
### @stacksjs/build
|
|
221
221
|
|
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>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { PaymentRecord } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
3
5
|
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
@@ -6,61 +8,95 @@ const records = useReactiveProp('records', [] as PaymentRecord[])
|
|
|
6
8
|
const loading = useReactiveProp('loading', false)
|
|
7
9
|
const emit = defineEmits()
|
|
8
10
|
|
|
11
|
+
/** `partiallyRefunded` and `bank_transfer` as words, each capitalized. */
|
|
9
12
|
function label(value: string): string {
|
|
10
|
-
return value.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function money(value: number, currency: string): string {
|
|
14
|
-
return formatCurrency(value, currency)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function date(value: string): string {
|
|
18
|
-
const parsed = new Date(value.includes('T') ? value : value.replace(' ', 'T'))
|
|
19
|
-
return !value || Number.isNaN(parsed.getTime())
|
|
20
|
-
? 'Unknown'
|
|
21
|
-
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(parsed)
|
|
13
|
+
return value.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ').replace(/\b\w/g, letter => letter.toUpperCase())
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The status pill's colour, which is a tone rather than the status itself so
|
|
18
|
+
* that every status the ledger has not named still reads as pending, as the
|
|
19
|
+
* hand-rolled pill did.
|
|
20
|
+
*/
|
|
21
|
+
function statusTone(status: string): string {
|
|
25
22
|
if (status === 'completed' || status === 'succeeded')
|
|
26
|
-
return '
|
|
23
|
+
return 'settled'
|
|
27
24
|
if (status === 'failed')
|
|
28
|
-
return '
|
|
25
|
+
return 'failed'
|
|
29
26
|
if (status === 'refunded' || status === 'partiallyRefunded')
|
|
30
|
-
return '
|
|
31
|
-
return '
|
|
27
|
+
return 'refunded'
|
|
28
|
+
return 'pending'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Only the amount is slotted, for its refund line, whose colour says whether
|
|
33
|
+
* the refund was recorded. The rest are values with a second line, which is
|
|
34
|
+
* `subtitleKey`.
|
|
35
|
+
*/
|
|
36
|
+
const COLUMNS = [
|
|
37
|
+
{ key: 'paymentLabel', label: 'Payment', sortable: true, subtitleKey: 'referenceLabel', class: 'font-medium text-gray-900 dark:text-white' },
|
|
38
|
+
{ key: 'billingLabel', label: 'Billing', sortable: true, subtitleKey: 'orderLabel' },
|
|
39
|
+
{ key: 'amount', label: 'Amount', sortable: true, slotted: true },
|
|
40
|
+
// Capitalized in `label()` rather than with a class, which would reach the
|
|
41
|
+
// processor line beneath it too.
|
|
42
|
+
{ key: 'methodLabel', label: 'Method', sortable: true, subtitleKey: 'processorLabel' },
|
|
43
|
+
{
|
|
44
|
+
key: 'statusTone',
|
|
45
|
+
label: 'Status',
|
|
46
|
+
type: 'badge' as const,
|
|
47
|
+
sortable: true,
|
|
48
|
+
badgeLabelKey: 'statusLabel',
|
|
49
|
+
badgeClasses: {
|
|
50
|
+
settled: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40',
|
|
51
|
+
failed: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
52
|
+
refunded: 'text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-950/40',
|
|
53
|
+
pending: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{ key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const ROW_ACTIONS = [
|
|
60
|
+
{ action: 'view', label: 'View payment', icon: 'i-hugeicons-view' },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const rows = derived(() => records().map(record => ({
|
|
64
|
+
...record,
|
|
65
|
+
paymentLabel: `#${record.id}`,
|
|
66
|
+
referenceLabel: record.referenceNumber || record.transactionId || 'No reference',
|
|
67
|
+
billingLabel: record.billingEmail || 'No billing email',
|
|
68
|
+
orderLabel: record.orderId ? `Order #${record.orderId}` : 'No linked order',
|
|
69
|
+
amountLabel: formatCurrency(record.amount, record.currency),
|
|
70
|
+
refundLabel: record.refundAmount > 0 ? `Refunded ${formatCurrency(record.refundAmount, record.currency)}` : '',
|
|
71
|
+
refundMissing: record.refundAmount <= 0 && record.status === 'refunded' && !record.refundRecorded,
|
|
72
|
+
methodLabel: label(record.method),
|
|
73
|
+
processorLabel: record.cardBrand || record.cardLastFour
|
|
74
|
+
? [record.cardBrand, record.cardLastFour].filter(Boolean).join(' · ')
|
|
75
|
+
: record.paymentProvider || 'No processor details',
|
|
76
|
+
statusTone: statusTone(record.status),
|
|
77
|
+
statusLabel: label(record.status),
|
|
78
|
+
})))
|
|
79
|
+
|
|
80
|
+
function handleRowAction(payload: { action: string, row: PaymentRecord }): void {
|
|
81
|
+
emit(payload.action, payload.row)
|
|
32
82
|
}
|
|
33
83
|
</script>
|
|
34
84
|
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<tr>
|
|
54
|
-
<td class="px-4 py-4"><p class="font-medium text-gray-900 text-sm dark:text-white">#{{ record.id }}</p><p class="mt-1 max-w-44 text-gray-400 text-xs truncate dark:text-neutral-500">{{ record.referenceNumber || record.transactionId || 'No reference' }}</p></td>
|
|
55
|
-
<td class="px-4 py-4"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.billingEmail || 'No billing email' }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.orderId ? 'Order #' + record.orderId : 'No linked order' }}</p></td>
|
|
56
|
-
<td class="px-4 py-4"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ money(record.amount, record.currency) }}</p><p :if="record.refundAmount > 0" class="mt-1 text-violet-600 text-xs dark:text-violet-300">Refunded {{ money(record.refundAmount, record.currency) }}</p><p :else-if="record.status === 'refunded' && !record.refundRecorded" class="mt-1 text-amber-600 text-xs dark:text-amber-300">Refund amount not recorded</p></td>
|
|
57
|
-
<td class="px-4 py-4"><p class="capitalize text-gray-700 text-sm dark:text-neutral-200">{{ label(record.method) }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.cardBrand }}<span :if="record.cardLastFour"> · {{ record.cardLastFour }}</span><span :if="!record.cardBrand && !record.cardLastFour">{{ record.paymentProvider || 'No processor details' }}</span></p></td>
|
|
58
|
-
<td class="px-4 py-4"><span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(record.status)">{{ label(record.status) }}</span></td>
|
|
59
|
-
<td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ date(record.createdAt) }}</td>
|
|
60
|
-
<td class="px-4 py-4 text-right"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'View payment ' + record.id" @click="emit('view', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-view"></span></Button></td>
|
|
61
|
-
</tr>
|
|
62
|
-
</template>
|
|
63
|
-
</tbody>
|
|
64
|
-
</table>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
85
|
+
<Table
|
|
86
|
+
:columns="COLUMNS"
|
|
87
|
+
:data="rows()"
|
|
88
|
+
:loading="loading()"
|
|
89
|
+
:row-actions="ROW_ACTIONS"
|
|
90
|
+
action-label-key="id"
|
|
91
|
+
paginated
|
|
92
|
+
:page-size="25"
|
|
93
|
+
empty-title="No payments found"
|
|
94
|
+
empty-description="Adjust the filters to inspect another part of the native ledger."
|
|
95
|
+
@row-action="handleRowAction"
|
|
96
|
+
>
|
|
97
|
+
<template #cell="{ row }">
|
|
98
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.amountLabel"></p>
|
|
99
|
+
<p :if="row.refundLabel" class="mt-1 text-violet-600 text-xs dark:text-violet-300" x-text="row.refundLabel"></p>
|
|
100
|
+
<p :if="row.refundMissing" class="mt-1 text-amber-600 text-xs dark:text-amber-300">Refund amount not recorded</p>
|
|
101
|
+
</template>
|
|
102
|
+
</Table>
|