@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.
Files changed (22) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +3 -0
  2. package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
  3. package/ai/skills/stacks-native/CATALOG.md +4 -4
  4. package/ide/vscode/package.json +1 -1
  5. package/package.json +4 -4
  6. package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
  7. package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
  8. package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
  9. package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
  10. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
  11. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
  12. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
  13. package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
  14. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
  15. package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
  16. package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
  17. package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
  18. package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
  19. package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
  20. package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
  21. package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
  22. 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 type { ReleaseRecord, ReleaseSummary } from '../../../../app/Actions/Dashboard/Releases/release-records'
3
5
  import { dashboardApi } from '../../../../functions/dashboard-api'
@@ -41,27 +43,30 @@ function formatNumber(value: number): string {
41
43
  return new Intl.NumberFormat().format(value)
42
44
  }
43
45
 
44
- function formatDate(value: string): string {
45
- if (!value)
46
- return 'Not recorded'
47
- const parsed = new Date(value)
48
- if (Number.isNaN(parsed.getTime()))
49
- return value
50
- return new Intl.DateTimeFormat(undefined, {
51
- dateStyle: 'medium',
52
- timeStyle: 'short',
53
- }).format(parsed)
54
- }
46
+ const COLUMNS = [
47
+ { key: 'version', label: 'Version', sortable: true, subtitleKey: 'notes', class: 'font-semibold text-gray-900 dark:text-white' },
48
+ { key: 'type', label: 'Type', sortable: true, class: 'capitalize' },
49
+ {
50
+ key: 'statusKey',
51
+ label: 'Status',
52
+ type: 'badge' as const,
53
+ sortable: true,
54
+ badgeLabelKey: 'status',
55
+ badgeClasses: {
56
+ published: 'capitalize text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30',
57
+ scheduled: 'capitalize text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30',
58
+ draft: 'capitalize text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30',
59
+ },
60
+ },
61
+ { key: 'downloads', label: 'Downloads', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
62
+ { key: 'author', label: 'Author', sortable: true, emptyText: 'Not recorded' },
63
+ { key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
64
+ ]
55
65
 
56
- function statusClass(value: string): string {
57
- if (value === 'published')
58
- return 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30'
59
- if (value === 'scheduled')
60
- return 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/30'
61
- if (value === 'draft')
62
- return 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30'
63
- return 'text-gray-700 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700'
64
- }
66
+ const releaseRows = derived(() => rows().map(release => ({
67
+ ...release,
68
+ statusKey: release.status.toLowerCase(),
69
+ })))
65
70
 
66
71
  async function loadReleases(): Promise<void> {
67
72
  isLoading.set(true)
@@ -141,47 +146,16 @@ onMount(() => {
141
146
  </div>
142
147
  </div>
143
148
 
144
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
145
- <div :if="isLoading()" class="flex items-center justify-center p-8 text-gray-500 text-sm dark:text-neutral-400">
146
- <span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
147
- Loading releases
148
- </div>
149
- <div :else-if="rows().length === 0" class="p-8 text-center">
150
- <div class="mx-auto h-8 w-8 text-gray-400 i-hugeicons-right-to-left-list-number"></div>
151
- <p class="mt-3 font-medium text-gray-900 text-sm dark:text-white">No release records found</p>
152
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Create releases through your application workflow and they will appear here.</p>
153
- </div>
154
- <div :else class="overflow-x-auto">
155
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
156
- <thead class="bg-gray-50 dark:bg-neutral-900/50">
157
- <tr>
158
- <th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Version</th>
159
- <th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Type</th>
160
- <th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Status</th>
161
- <th class="px-4 py-3 font-semibold text-gray-600 text-right text-xs uppercase dark:text-neutral-400">Downloads</th>
162
- <th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Author</th>
163
- <th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Created</th>
164
- </tr>
165
- </thead>
166
- <tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
167
- <template :for="release in rows()" :key="release.id">
168
- <tr>
169
- <td class="px-4 py-3">
170
- <p class="font-semibold text-gray-900 text-sm dark:text-white">{{ release.version }}</p>
171
- <p :if="release.notes" class="mt-1 max-w-md text-gray-500 text-xs truncate dark:text-neutral-400">{{ release.notes }}</p>
172
- </td>
173
- <td class="px-4 py-3 capitalize text-gray-600 text-sm dark:text-neutral-300">{{ release.type }}</td>
174
- <td class="px-4 py-3">
175
- <span :class="'inline-flex px-2 py-1 font-medium text-xs capitalize rounded-full ' + statusClass(release.status)">{{ release.status }}</span>
176
- </td>
177
- <td class="px-4 py-3 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ formatNumber(release.downloads) }}</td>
178
- <td class="px-4 py-3 text-gray-600 text-sm dark:text-neutral-300">{{ release.author || 'Not recorded' }}</td>
179
- <td class="px-4 py-3 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDate(release.createdAt) }}</td>
180
- </tr>
181
- </template>
182
- </tbody>
183
- </table>
184
- </div>
149
+ <div class="bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
150
+ <Table
151
+ :columns="COLUMNS"
152
+ :data="releaseRows()"
153
+ :loading="isLoading()"
154
+ paginated
155
+ :page-size="25"
156
+ empty-title="No release records found"
157
+ empty-description="Create releases through your application workflow and they will appear here."
158
+ />
185
159
  </div>
186
160
  </section>
187
161
  </div>
@@ -1,4 +1,4 @@
1
- @import('../UI/Button', '../UI/ConfirmDialog')
1
+ @import('../UI/Button', '../UI/ConfirmDialog', '../UI/Table')
2
2
 
3
3
  <script client>
4
4
  import { dashboardApi } from '../../../../functions/dashboard-api'
@@ -53,8 +53,6 @@ const memberToRemove = state<TeamMember | null>(null)
53
53
  const invitationToRevoke = state<TeamInvitation | null>(null)
54
54
 
55
55
  const teamId = derived(() => Number(route.params.id))
56
- const hasMembers = derived(() => members().length > 0)
57
- const hasInvitations = derived(() => invitations().length > 0)
58
56
 
59
57
  function roleLabel(role: string): string {
60
58
  return role.charAt(0).toUpperCase() + role.slice(1)
@@ -65,24 +63,64 @@ function memberCountLabel(): string {
65
63
  return `${count} team ${count === 1 ? 'member' : 'members'}`
66
64
  }
67
65
 
68
- function statusClass(status: string): string {
69
- if (status === 'active' || status === 'sent')
70
- return 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400'
71
- if (status === 'suspended' || status === 'failed')
72
- return 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400'
73
- if (status === 'expired')
74
- return 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400'
75
- return 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300'
66
+ /**
67
+ * One palette for member status and invitation delivery, keyed by value, so
68
+ * a status the server adds later still reads, in the neutral pill.
69
+ */
70
+ const STATUS_CLASSES = {
71
+ active: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
72
+ sent: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
73
+ suspended: 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400',
74
+ failed: 'bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-400',
75
+ expired: 'bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400',
76
76
  }
77
77
 
78
- function formatDate(value: string | null): string {
79
- if (!value)
80
- return 'Not available'
81
- const date = new Date(value.replace(' ', 'T'))
82
- return Number.isNaN(date.getTime())
83
- ? value
84
- : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
85
- }
78
+ /**
79
+ * The member and role columns are slotted: the first for its avatar, the
80
+ * second because an owner's role is fixed and everyone else's is a select.
81
+ */
82
+ const MEMBER_COLUMNS = [
83
+ { key: 'name', label: 'Member', sortable: true, slotted: true },
84
+ { key: 'role', label: 'Role', sortable: true, slotted: true },
85
+ { key: 'status', label: 'Status', type: 'badge' as const, sortable: true, badgeLabelKey: 'statusLabel', badgeClasses: STATUS_CLASSES },
86
+ ]
87
+
88
+ /**
89
+ * An owner can be neither suspended nor removed. Anyone else is offered the
90
+ * opposite of their current state, and every status but `active` reads as
91
+ * one to activate, as the hand-rolled button did.
92
+ */
93
+ const MEMBER_ACTIONS = [
94
+ { action: 'suspend', label: 'Suspend', icon: 'i-hugeicons-pause', class: 'text-amber-500', when: { key: 'statusAction', equals: 'suspend' } },
95
+ { action: 'activate', label: 'Activate', icon: 'i-hugeicons-play', class: 'text-green-500', when: { key: 'statusAction', equals: 'activate' } },
96
+ { action: 'remove', label: 'Remove', icon: 'i-hugeicons-delete-02', class: 'text-red-500', when: { key: 'removable', equals: true } },
97
+ ]
98
+
99
+ const INVITATION_COLUMNS = [
100
+ { key: 'email', label: 'Recipient', sortable: true, class: 'text-neutral-900 dark:text-neutral-100' },
101
+ { key: 'roleLabel', label: 'Role', sortable: true },
102
+ { key: 'deliveryStatus', label: 'Delivery', type: 'badge' as const, sortable: true, badgeLabelKey: 'deliveryLabel', badgeClasses: STATUS_CLASSES },
103
+ { key: 'expiresAt', label: 'Expires', type: 'date' as const, sortable: true, emptyText: 'Not available' },
104
+ ]
105
+
106
+ const INVITATION_ACTIONS = [
107
+ { action: 'resend', label: 'Resend invitation to', icon: 'i-hugeicons-mail-send-01' },
108
+ { action: 'revoke', label: 'Revoke invitation to', icon: 'i-hugeicons-cancel-01', class: 'text-red-500' },
109
+ ]
110
+
111
+ const memberRows = derived(() => members().map(member => ({
112
+ ...member,
113
+ initial: member.name.charAt(0).toUpperCase(),
114
+ statusLabel: roleLabel(member.status),
115
+ statusAction: member.role === 'owner' ? 'none' : member.status === 'active' ? 'suspend' : 'activate',
116
+ removable: member.role !== 'owner',
117
+ })))
118
+
119
+ const invitationRows = derived(() => invitations().map(invitation => ({
120
+ ...invitation,
121
+ roleLabel: roleLabel(invitation.role),
122
+ deliveryLabel: roleLabel(invitation.deliveryStatus),
123
+ })))
86
124
 
87
125
  async function fetchPeople(): Promise<void> {
88
126
  isLoading.set(true)
@@ -233,6 +271,20 @@ async function revokeInvitation(): Promise<void> {
233
271
  }
234
272
  }
235
273
 
274
+ function handleMemberAction(payload: { action: string, row: TeamMember }): void {
275
+ if (payload.action === 'remove')
276
+ memberToRemove.set(payload.row)
277
+ else
278
+ void toggleMemberStatus(payload.row)
279
+ }
280
+
281
+ function handleInvitationAction(payload: { action: string, row: TeamInvitation }): void {
282
+ if (payload.action === 'resend')
283
+ void resendInvitation(payload.row)
284
+ else if (payload.action === 'revoke')
285
+ invitationToRevoke.set(payload.row)
286
+ }
287
+
236
288
  onMount(() => {
237
289
  fetchPeople()
238
290
  })
@@ -307,79 +359,48 @@ onMount(() => {
307
359
  </div>
308
360
  </div>
309
361
 
310
- <div :show="isLoading()" class="px-6 py-10 text-center text-neutral-500 text-sm dark:text-neutral-400">Loading members...</div>
311
- <div :show="!isLoading() && !hasMembers()" class="px-6 py-10 text-center text-neutral-500 text-sm dark:text-neutral-400">No members have joined this team yet.</div>
312
-
313
- <div :show="!isLoading() && hasMembers()" class="overflow-x-auto">
314
- <table class="min-w-full divide-neutral-200 divide-y dark:divide-neutral-800">
315
- <thead class="bg-neutral-50/80 dark:bg-neutral-950/50">
316
- <tr>
317
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Member</th>
318
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Role</th>
319
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Status</th>
320
- <th scope="col" class="px-6 py-3 font-medium text-neutral-500 text-right text-xs tracking-wide uppercase dark:text-neutral-400">Actions</th>
321
- </tr>
322
- </thead>
323
- <tbody class="divide-neutral-200 divide-y dark:divide-neutral-800">
324
- <template :for="member in members">
325
- <tr>
326
- <td class="px-6 py-4">
327
- <div class="flex gap-3 items-center">
328
- <div class="flex items-center justify-center shrink-0 h-9 w-9 font-semibold text-blue-700 text-sm dark:text-blue-300 bg-blue-50 dark:bg-blue-500/10 rounded-full">
329
- {{ member.name.charAt(0).toUpperCase() }}
330
- </div>
331
- <div>
332
- <p class="font-medium text-neutral-900 text-sm dark:text-white">{{ member.name }}</p>
333
- <p class="mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">{{ member.email }}</p>
334
- </div>
335
- </div>
336
- </td>
337
- <td class="px-6 py-4">
338
- <span :show="member.role === 'owner'" class="text-neutral-700 text-sm dark:text-neutral-300">Owner</span>
339
- <select
340
- :show="member.role !== 'owner'"
341
- :value="member.role"
342
- :aria-label="'Role for ' + member.name"
343
- :disabled="busyMemberId() === member.id"
344
- class="px-2.5 h-8 text-neutral-800 text-sm dark:text-neutral-100 bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700"
345
- @change="updateMemberRole(member, $event)"
346
- >
347
- <option value="admin">Admin</option>
348
- <option value="member">Member</option>
349
- <option value="viewer">Viewer</option>
350
- </select>
351
- </td>
352
- <td class="px-6 py-4">
353
- <span class="inline-flex px-2 py-1 font-medium text-xs rounded-full" :class="statusClass(member.status)">
354
- {{ roleLabel(member.status) }}
355
- </span>
356
- </td>
357
- <td class="px-6 py-4">
358
- <div :show="member.role !== 'owner'" class="flex gap-2 items-center justify-end">
359
- <Button
360
- variant="secondary"
361
- size="sm"
362
- :loading="busyMemberId() === member.id"
363
- :disabled="busyMemberId() === member.id"
364
- @click="toggleMemberStatus(member)"
365
- >
366
- {{ member.status === 'active' ? 'Suspend' : 'Activate' }}
367
- </Button>
368
- <Button
369
- variant="ghost"
370
- size="sm"
371
- :disabled="busyMemberId() === member.id"
372
- @click="memberToRemove.set(member)"
373
- >
374
- Remove
375
- </Button>
376
- </div>
377
- </td>
378
- </tr>
379
- </template>
380
- </tbody>
381
- </table>
382
- </div>
362
+ <Table
363
+ :columns="MEMBER_COLUMNS"
364
+ :data="memberRows()"
365
+ :loading="isLoading()"
366
+ :row-actions="MEMBER_ACTIONS"
367
+ action-label-key="name"
368
+ busy-key="id"
369
+ :busy-value="busyMemberId()"
370
+ paginated
371
+ :page-size="25"
372
+ empty-title="No members yet"
373
+ empty-description="No members have joined this team yet."
374
+ @row-action="handleMemberAction"
375
+ >
376
+ <template #cell="{ row, column }">
377
+ <template :if="column.key === 'name'">
378
+ <div class="flex gap-3 items-center">
379
+ <div class="flex items-center justify-center shrink-0 h-9 w-9 font-semibold text-blue-700 text-sm dark:text-blue-300 bg-blue-50 dark:bg-blue-500/10 rounded-full">{{ row.initial }}</div>
380
+ <div>
381
+ <p class="font-medium text-neutral-900 text-sm dark:text-white">{{ row.name }}</p>
382
+ <p class="mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">{{ row.email }}</p>
383
+ </div>
384
+ </div>
385
+ </template>
386
+ <template :if="column.key === 'role' && row.role === 'owner'">
387
+ <span class="text-neutral-700 text-sm dark:text-neutral-300">Owner</span>
388
+ </template>
389
+ <template :if="column.key === 'role' && row.role !== 'owner'">
390
+ <select
391
+ :value="row.role"
392
+ :aria-label="'Role for ' + row.name"
393
+ :disabled="busyMemberId() === row.id"
394
+ class="px-2.5 h-8 text-neutral-800 text-sm dark:text-neutral-100 bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700"
395
+ @change="updateMemberRole(row, $event)"
396
+ >
397
+ <option value="admin">Admin</option>
398
+ <option value="member">Member</option>
399
+ <option value="viewer">Viewer</option>
400
+ </select>
401
+ </template>
402
+ </template>
403
+ </Table>
383
404
  </section>
384
405
 
385
406
  <section class="overflow-hidden bg-white dark:bg-neutral-900 border border-neutral-200/70 rounded-xl dark:border-neutral-800">
@@ -388,57 +409,20 @@ onMount(() => {
388
409
  <p class="mt-0.5 text-neutral-500 text-sm dark:text-neutral-400">Track delivery, resend links, or revoke access before acceptance.</p>
389
410
  </div>
390
411
 
391
- <div :show="isLoading()" class="px-6 py-10 text-center text-neutral-500 text-sm dark:text-neutral-400">Loading invitations...</div>
392
- <div :show="!isLoading() && !hasInvitations()" class="px-6 py-10 text-center text-neutral-500 text-sm dark:text-neutral-400">No pending invitations.</div>
393
-
394
- <div :show="hasInvitations()" class="overflow-x-auto">
395
- <table class="min-w-full divide-neutral-200 divide-y dark:divide-neutral-800">
396
- <thead class="bg-neutral-50/80 dark:bg-neutral-950/50">
397
- <tr>
398
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Recipient</th>
399
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Role</th>
400
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Delivery</th>
401
- <th scope="col" class="px-6 py-3 font-medium text-left text-neutral-500 text-xs tracking-wide uppercase dark:text-neutral-400">Expires</th>
402
- <th scope="col" class="px-6 py-3 font-medium text-neutral-500 text-right text-xs tracking-wide uppercase dark:text-neutral-400">Actions</th>
403
- </tr>
404
- </thead>
405
- <tbody class="divide-neutral-200 divide-y dark:divide-neutral-800">
406
- <template :for="invitation in invitations">
407
- <tr>
408
- <td class="px-6 py-4 text-neutral-900 text-sm dark:text-neutral-100">{{ invitation.email }}</td>
409
- <td class="px-6 py-4 text-neutral-600 text-sm dark:text-neutral-300">{{ roleLabel(invitation.role) }}</td>
410
- <td class="px-6 py-4">
411
- <span class="inline-flex px-2 py-1 font-medium text-xs rounded-full" :class="statusClass(invitation.deliveryStatus)">
412
- {{ roleLabel(invitation.deliveryStatus) }}
413
- </span>
414
- </td>
415
- <td class="px-6 py-4 text-neutral-500 text-sm dark:text-neutral-400">{{ formatDate(invitation.expiresAt) }}</td>
416
- <td class="px-6 py-4">
417
- <div class="flex gap-2 items-center justify-end">
418
- <Button
419
- variant="secondary"
420
- size="sm"
421
- :loading="busyInvitationId() === invitation.id"
422
- :disabled="busyInvitationId() === invitation.id"
423
- @click="resendInvitation(invitation)"
424
- >
425
- Resend
426
- </Button>
427
- <Button
428
- variant="ghost"
429
- size="sm"
430
- :disabled="busyInvitationId() === invitation.id"
431
- @click="invitationToRevoke.set(invitation)"
432
- >
433
- Revoke
434
- </Button>
435
- </div>
436
- </td>
437
- </tr>
438
- </template>
439
- </tbody>
440
- </table>
441
- </div>
412
+ <Table
413
+ :columns="INVITATION_COLUMNS"
414
+ :data="invitationRows()"
415
+ :loading="isLoading()"
416
+ :row-actions="INVITATION_ACTIONS"
417
+ action-label-key="email"
418
+ busy-key="id"
419
+ :busy-value="busyInvitationId()"
420
+ paginated
421
+ :page-size="25"
422
+ empty-title="No pending invitations"
423
+ empty-description="Invitations you send appear here until they are accepted."
424
+ @row-action="handleInvitationAction"
425
+ />
442
426
  </section>
443
427
  </div>
444
428