@stacksjs/defaults 0.75.263 → 0.75.266
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 +3 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
- package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
- 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/Infrastructure/DnsDashboard.stx +46 -28
- 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/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
- package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
|
@@ -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>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { TaxRateRecord } from '../../../../app/Actions/Dashboard/Commerce/tax-rate-records'
|
|
3
5
|
|
|
@@ -5,72 +7,67 @@ const records = useReactiveProp('records', [] as TaxRateRecord[])
|
|
|
5
7
|
const loading = useReactiveProp('loading', false)
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The name cell is slotted: the default, code and exemptible badges sit
|
|
12
|
+
* beside the name because each changes what a customer is charged, and the
|
|
13
|
+
* code is what application code matches on, so renaming the rate must not
|
|
14
|
+
* change the bill. Everything else is a column option.
|
|
15
|
+
*/
|
|
16
|
+
const COLUMNS = [
|
|
17
|
+
{ key: 'name', label: 'Tax rate', sortable: true, slotted: true },
|
|
18
|
+
{ key: 'rateLabel', label: 'Rate', sortable: true, searchable: false, class: 'font-medium text-gray-900 whitespace-nowrap dark:text-white' },
|
|
19
|
+
{ key: 'country', label: 'Location', sortable: true, subtitleKey: 'region' },
|
|
20
|
+
{
|
|
21
|
+
key: 'status',
|
|
22
|
+
label: 'Status',
|
|
23
|
+
type: 'badge' as const,
|
|
24
|
+
sortable: true,
|
|
25
|
+
badgeClasses: { active: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' },
|
|
26
|
+
badgeLabels: { active: 'Active', inactive: 'Inactive' },
|
|
27
|
+
},
|
|
28
|
+
{ key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const ROW_ACTIONS = [
|
|
32
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
33
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const rows = derived(() => records().map(record => ({
|
|
37
|
+
...record,
|
|
38
|
+
rateLabel: `${record.rate}%`,
|
|
39
|
+
// Anything but active reads as inactive, as the hand-rolled pill did.
|
|
40
|
+
status: record.status === 'active' ? 'active' : 'inactive',
|
|
41
|
+
})))
|
|
42
|
+
|
|
43
|
+
function handleRowAction(payload: { action: string, row: TaxRateRecord }): void {
|
|
44
|
+
emit(payload.action, payload.row)
|
|
13
45
|
}
|
|
14
46
|
</script>
|
|
15
47
|
|
|
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
|
-
<div class="flex flex-wrap gap-2 items-center">
|
|
43
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name }}</p>
|
|
44
|
-
<span :if="record.isDefault" class="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-full">Default</span>
|
|
45
|
-
{{--
|
|
46
|
-
Both badges change what a customer is charged, so they sit
|
|
47
|
-
next to the name rather than in a column someone has to
|
|
48
|
-
scroll to. The code is what application code matches on;
|
|
49
|
-
renaming the rate must not change the bill.
|
|
50
|
-
--}}
|
|
51
|
-
<span :if="record.code" class="px-2 py-0.5 font-mono text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full">{{ record.code }}</span>
|
|
52
|
-
<span :if="record.exemptible" class="px-2 py-0.5 font-medium text-amber-700 text-xs dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40 rounded-full" x-tooltip="Not charged to a customer who qualifies for an exemption" title="Not charged to a customer who qualifies for an exemption">Exemptible</span>
|
|
53
|
-
</div>
|
|
54
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.type }}</p>
|
|
55
|
-
</td>
|
|
56
|
-
<td class="px-4 py-4 font-medium text-gray-900 text-sm whitespace-nowrap dark:text-white">{{ record.rate }}%</td>
|
|
57
|
-
<td class="px-4 py-4">
|
|
58
|
-
<p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.country }}</p>
|
|
59
|
-
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.region }}</p>
|
|
60
|
-
</td>
|
|
61
|
-
<td class="px-4 py-4">
|
|
62
|
-
<span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + (record.status === 'active' ? 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' : 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700')">{{ record.status === 'active' ? 'Active' : 'Inactive' }}</span>
|
|
63
|
-
</td>
|
|
64
|
-
<td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
|
|
65
|
-
<td class="px-4 py-4">
|
|
66
|
-
<div class="flex gap-1 justify-end">
|
|
67
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span></Button>
|
|
68
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button>
|
|
69
|
-
</div>
|
|
70
|
-
</td>
|
|
71
|
-
</tr>
|
|
72
|
-
</template>
|
|
73
|
-
</tbody>
|
|
74
|
-
</table>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
48
|
+
{{--
|
|
49
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
50
|
+
and the filter selects. Paging is what was missing, and Table brings it.
|
|
51
|
+
--}}
|
|
52
|
+
<Table
|
|
53
|
+
:columns="COLUMNS"
|
|
54
|
+
:data="rows()"
|
|
55
|
+
:loading="loading()"
|
|
56
|
+
:row-actions="ROW_ACTIONS"
|
|
57
|
+
action-label-key="name"
|
|
58
|
+
paginated
|
|
59
|
+
:page-size="25"
|
|
60
|
+
empty-title="No tax rates found"
|
|
61
|
+
empty-description="Adjust the filters or add a TaxRate record."
|
|
62
|
+
@row-action="handleRowAction"
|
|
63
|
+
>
|
|
64
|
+
<template #cell="{ row }">
|
|
65
|
+
<div class="flex flex-wrap gap-2 items-center">
|
|
66
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.name"></p>
|
|
67
|
+
<span :if="row.isDefault" class="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-full">Default</span>
|
|
68
|
+
<span :if="row.code" class="px-2 py-0.5 font-mono text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full" x-text="row.code"></span>
|
|
69
|
+
<span :if="row.exemptible" class="px-2 py-0.5 font-medium text-amber-700 text-xs dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40 rounded-full" x-tooltip="Not charged to a customer who qualifies for an exemption" title="Not charged to a customer who qualifies for an exemption">Exemptible</span>
|
|
70
|
+
</div>
|
|
71
|
+
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400" x-text="row.type"></p>
|
|
72
|
+
</template>
|
|
73
|
+
</Table>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { ProductVariantRecord } from '../../../../app/Actions/Dashboard/Commerce/product-variant-records'
|
|
3
5
|
|
|
@@ -5,30 +7,72 @@ const records = useReactiveProp('records', [] as ProductVariantRecord[])
|
|
|
5
7
|
const loading = useReactiveProp('loading', false)
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Two slotted columns, dispatched on `column.key`: the variant with its type
|
|
12
|
+
* and description beneath it, and the option pills, of which the first four
|
|
13
|
+
* show and the rest are counted.
|
|
14
|
+
*/
|
|
15
|
+
const COLUMNS = [
|
|
16
|
+
{ key: 'variant', label: 'Variant', sortable: true, slotted: true },
|
|
17
|
+
{ key: 'productName', label: 'Product', sortable: true, emptyText: 'Unassigned' },
|
|
18
|
+
{ key: 'optionsLabel', label: 'Options', slotted: true, class: 'max-w-sm' },
|
|
19
|
+
{
|
|
20
|
+
key: 'status',
|
|
21
|
+
label: 'Status',
|
|
22
|
+
type: 'badge' as const,
|
|
23
|
+
sortable: true,
|
|
24
|
+
badgeClasses: {
|
|
25
|
+
active: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40',
|
|
26
|
+
draft: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const ROW_ACTIONS = [
|
|
33
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
34
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const rows = derived(() => records().map(record => ({
|
|
38
|
+
...record,
|
|
39
|
+
// Searchable text for the options, and the parts the cell draws.
|
|
40
|
+
optionsLabel: record.options.join(', '),
|
|
41
|
+
shownOptions: record.options.slice(0, 4),
|
|
42
|
+
moreOptions: record.options.length > 4 ? `+${record.options.length - 4}` : '',
|
|
43
|
+
})))
|
|
44
|
+
|
|
45
|
+
function handleRowAction(payload: { action: string, row: ProductVariantRecord }): void {
|
|
46
|
+
emit(payload.action, payload.row)
|
|
11
47
|
}
|
|
12
48
|
</script>
|
|
13
49
|
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
<Table
|
|
51
|
+
:columns="COLUMNS"
|
|
52
|
+
:data="rows()"
|
|
53
|
+
:loading="loading()"
|
|
54
|
+
:row-actions="ROW_ACTIONS"
|
|
55
|
+
action-label-key="variant"
|
|
56
|
+
paginated
|
|
57
|
+
:page-size="25"
|
|
58
|
+
empty-title="No product variants found"
|
|
59
|
+
empty-description="Adjust the filters or add a ProductVariant record."
|
|
60
|
+
@row-action="handleRowAction"
|
|
61
|
+
>
|
|
62
|
+
<template #cell="{ row, column }">
|
|
63
|
+
<template :if="column.key === 'variant'">
|
|
64
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.variant"></p>
|
|
65
|
+
<p class="mt-1 text-gray-500 text-xs dark:text-neutral-400" x-text="row.type"></p>
|
|
66
|
+
<p class="mt-2 max-w-sm text-gray-500 text-xs dark:text-neutral-400" x-text="row.description"></p>
|
|
67
|
+
</template>
|
|
68
|
+
<template :else>
|
|
69
|
+
<div class="flex flex-wrap gap-1">
|
|
70
|
+
<template :for="option in row.shownOptions" :key="option">
|
|
71
|
+
<span class="px-2 py-0.5 text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-full" x-text="option"></span>
|
|
30
72
|
</template>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</
|
|
73
|
+
<span :if="row.moreOptions" class="px-2 py-0.5 text-gray-600 text-xs dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700 rounded-full" x-text="row.moreOptions"></span>
|
|
74
|
+
<span :if="row.shownOptions.length === 0" class="text-gray-400 text-xs">No options</span>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
</template>
|
|
78
|
+
</Table>
|