RowActionsMenu
Three-dot dropdown menu for per-row actions.
Overview
The RowActionsMenu provides a compact dropdown menu for row-specific operations. It includes standard actions like view details, copy, export, and conditional delete.
Best for: Individual row operations in tables
Key Features:
- ✅ Three-dot dropdown menu
- ✅ Multiple action buttons
- ✅ Conditional visibility (delete only if permitted)
- ✅ Tooltip labels
- ✅ Responsive positioning
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
rowId | string | - | ✅ Yes | Row identifier |
row | any | - | - | Complete row data object |
disabled | boolean | false | - | Disable all actions |
Events
| Event | Payload | Description |
|---|---|---|
view | { rowId, row } | View row details |
copy-id | { rowId, row } | Copy row ID |
copy-message | { rowId, row } | Copy row message/content |
export-json | { rowId, row } | Export row as JSON |
delete | { rowId, row } | Delete row |
Basic Usage
<template>
<div class="table">
<div v-for="row in rows" :key="row.id" class="row">
<span>{{ row.name }}</span>
<RowActionsMenu
:row-id="String(row.id)"
:row="row"
@view="handleView"
@copy-id="handleCopyId"
@delete="handleDelete"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RowActionsMenu from '@/components/Table/RowActionsMenu.vue'
const rows = ref([
{ id: 1, name: 'Document 1', message: 'Invoice' },
{ id: 2, name: 'Document 2', message: 'Receipt' }
])
function handleView({ rowId, row }: { rowId: string; row: any }) {
console.log('View row:', rowId)
}
function handleCopyId({ rowId }: { rowId: string; row: any }) {
navigator.clipboard.writeText(rowId)
}
function handleDelete({ rowId, row }: { rowId: string; row: any }) {
if (confirm(`Delete ${row.name}?`)) {
rows.value = rows.value.filter(r => String(r.id) !== rowId)
}
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Standard Actions
View Details
Navigate to row detail page:
@view="({ rowId, row }) => router.push(`/documents/${rowId}`)"Copy ID
Copy row ID to clipboard:
@copy-id="({ rowId }) => {
navigator.clipboard.writeText(rowId)
showNotification('ID copied')
}"2
3
4
Copy Message
Copy row message/content:
@copy-message="({ row }) => {
navigator.clipboard.writeText(row.message)
}"2
3
Export as JSON
Download row as JSON file:
@export-json="({ row, rowId }) => {
const json = JSON.stringify(row, null, 2)
const blob = new Blob([json], { type: 'application/json' })
downloadFile(blob, `row-${rowId}.json`)
}"2
3
4
5
Delete Row
Delete row with confirmation:
@delete="async ({ rowId, row }) => {
const confirmed = await showConfirmDialog(
`Delete ${row.name}?`
)
if (confirmed) {
await api.deleteRow(rowId)
}
}"2
3
4
5
6
7
8
Advanced Examples
Conditional Delete
Only show delete for non-archived rows:
<RowActionsMenu
:row-id="String(row.id)"
:row="row"
:show-delete-option="row.status !== 'archived'"
/>2
3
4
5
Custom Actions
Extend menu with custom actions:
<RowActionsMenu
:row-id="String(row.id)"
:row="row"
:actions="customActions"
@action="handleCustomAction"
/>2
3
4
5
6
With Loading State
const deleting = ref(new Set<string>())
async function handleDelete({ rowId, row }) {
deleting.value.add(rowId)
try {
await api.deleteRow(rowId)
rows.value = rows.value.filter(r => String(r.id) !== rowId)
} finally {
deleting.value.delete(rowId)
}
}2
3
4
5
6
7
8
9
10
11
Real-World Example: Invoice Row Actions
This example shows per-row actions for invoice management including view details, export data, copy ID, and delete operations.
Implementation
<template>
<div class="invoice-list">
<div class="list-header">
<span class="header-col vendor-col">Vendor</span>
<span class="header-col amount-col">Amount</span>
<span class="header-col date-col">Invoice Date</span>
<span class="header-col status-col">Status</span>
<span class="header-col actions-col">Actions</span>
</div>
<div v-for="invoice in invoices" :key="invoice.id" class="invoice-item">
<span class="item-col vendor-col">
<strong>{{ invoice.vendor }}</strong>
<div class="doc-number">{{ invoice.documentNumber }}</div>
</span>
<span class="item-col amount-col">
{{ invoice.currency }} {{ invoice.amount.toFixed(2) }}
</span>
<span class="item-col date-col">
{{ new Date(invoice.invoiceDate).toLocaleDateString() }}
</span>
<span class="item-col status-col">
<span :class="`status-badge ${invoice.status}`">
{{ invoice.status.toUpperCase() }}
</span>
</span>
<span class="item-col actions-col">
<RowActionsMenu
:row-id="invoice.id"
:row="invoice"
@view="handleViewInvoice"
@copy-id="handleCopyInvoiceId"
@export-json="handleExportInvoice"
@delete="handleDeleteInvoice"
/>
</span>
</div>
</div>
<!-- Detail modal -->
<div v-if="selectedInvoice" class="invoice-modal">
<div class="modal-overlay" @click="selectedInvoice = null"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Invoice Details</h2>
<button @click="selectedInvoice = null" class="close-btn">✕</button>
</div>
<div class="modal-body">
<div class="detail-row">
<label>Document Number:</label>
<span>{{ selectedInvoice.documentNumber }}</span>
</div>
<div class="detail-row">
<label>Vendor:</label>
<span>{{ selectedInvoice.vendor }}</span>
</div>
<div class="detail-row">
<label>Amount:</label>
<span>{{ selectedInvoice.currency }} {{ selectedInvoice.amount.toFixed(2) }}</span>
</div>
<div class="detail-row">
<label>Invoice Date:</label>
<span>{{ new Date(selectedInvoice.invoiceDate).toLocaleDateString() }}</span>
</div>
<div class="detail-row">
<label>Status:</label>
<span :class="`status-badge ${selectedInvoice.status}`">
{{ selectedInvoice.status.toUpperCase() }}
</span>
</div>
<div class="detail-row">
<label>Extraction Confidence:</label>
<span>{{ (selectedInvoice.extractionConfidence * 100).toFixed(0) }}%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RowActionsMenu from '@/components/Table/RowActionsMenu.vue'
interface Invoice {
id: string
documentNumber: string
vendor: string
amount: number
currency: string
invoiceDate: string
status: 'pending' | 'approved' | 'rejected'
extractionConfidence: number
}
const invoices = ref<Invoice[]>([
{
id: 'INV-2024-001',
documentNumber: 'INV-001234',
vendor: 'Acme Corporation',
amount: 1250.00,
currency: 'USD',
invoiceDate: '2024-01-15',
status: 'pending',
extractionConfidence: 0.98
},
{
id: 'INV-2024-002',
documentNumber: 'INV-001235',
vendor: 'Global Supplies Inc',
amount: 3750.50,
currency: 'USD',
invoiceDate: '2024-01-16',
status: 'pending',
extractionConfidence: 0.95
},
{
id: 'INV-2024-003',
documentNumber: 'RE-2024-789',
vendor: 'Tech Solutions GmbH',
amount: 2100.00,
currency: 'EUR',
invoiceDate: '2024-01-18',
status: 'approved',
extractionConfidence: 0.97
},
{
id: 'INV-2024-004',
documentNumber: 'INV-UK-445',
vendor: 'British Office Ltd',
amount: 890.00,
currency: 'GBP',
invoiceDate: '2024-01-20',
status: 'pending',
extractionConfidence: 0.89
},
{
id: 'INV-2024-005',
documentNumber: 'F-2024-1122',
vendor: 'Consulting Partners SA',
amount: 8500.00,
currency: 'EUR',
invoiceDate: '2024-01-22',
status: 'rejected',
extractionConfidence: 0.93
},
{
id: 'INV-2024-006',
documentNumber: 'INV-2024-556',
vendor: 'Cloud Services Corp',
amount: 4200.00,
currency: 'USD',
invoiceDate: '2024-01-25',
status: 'approved',
extractionConfidence: 0.99
}
])
const selectedInvoice = ref<Invoice | null>(null)
function handleViewInvoice({ row }: { rowId: string; row: Invoice }) {
selectedInvoice.value = row
}
function handleCopyInvoiceId({ rowId }: { rowId: string; row: any }) {
navigator.clipboard.writeText(rowId)
console.log('Copied invoice ID:', rowId)
}
function handleExportInvoice({ row, rowId }: { rowId: string; row: Invoice }) {
const json = JSON.stringify(row, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `invoice-${rowId}.json`
a.click()
URL.revokeObjectURL(url)
}
function handleDeleteInvoice({ rowId, row }: { rowId: string; row: Invoice }) {
const confirmed = confirm(`Delete invoice ${row.documentNumber}?`)
if (confirmed) {
invoices.value = invoices.value.filter(inv => inv.id !== rowId)
console.log('Deleted invoice:', rowId)
}
}
</script>
<style scoped>
.invoice-list {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.list-header {
display: grid;
grid-template-columns: 2fr 140px 140px 100px 80px;
gap: 12px;
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
font-size: 0.9em;
align-items: center;
}
.invoice-item {
display: grid;
grid-template-columns: 2fr 140px 140px 100px 80px;
gap: 12px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
align-items: center;
transition: background 0.2s;
}
.invoice-item:hover {
background: #fafafa;
}
.item-col {
display: flex;
flex-direction: column;
justify-content: center;
}
.vendor-col {
min-width: 0;
}
.doc-number {
font-size: 0.85em;
color: #666;
font-weight: normal;
margin-top: 2px;
}
.amount-col {
text-align: right;
font-weight: 600;
}
.date-col {
text-align: center;
}
.status-col {
text-align: center;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75em;
font-weight: 600;
}
.status-badge.pending {
background: #fff3cd;
color: #856404;
}
.status-badge.approved {
background: #d4edda;
color: #155724;
}
.status-badge.rejected {
background: #f8d7da;
color: #721c24;
}
.actions-col {
display: flex;
justify-content: center;
}
/* Modal styles */
.invoice-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h2 {
margin: 0;
font-size: 1.25em;
}
.close-btn {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
color: #666;
padding: 0;
}
.modal-body {
padding: 24px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.detail-row label {
font-weight: 600;
color: #333;
}
.detail-row span {
color: #666;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
What This Example Shows
- View Details: Open invoice details in a modal
- Copy ID: Quick copy of invoice ID to clipboard
- Export JSON: Download invoice data as JSON file
- Delete Action: Delete invoice with confirmation
- Status Handling: Different visual states for pending, approved, rejected
- Realistic Data: Multiple vendors and currencies with proper formatting
Real-World Example: Receipt Row Actions
This example demonstrates per-row actions for receipt management including approval, rejection, and download operations.
Implementation
<template>
<div class="receipt-list">
<div class="list-header">
<span class="header-col merchant-col">Merchant</span>
<span class="header-col amount-col">Amount</span>
<span class="header-col date-col">Receipt Date</span>
<span class="header-col category-col">Category</span>
<span class="header-col status-col">Status</span>
<span class="header-col actions-col">Actions</span>
</div>
<div v-for="receipt in receipts" :key="receipt.id" class="receipt-item">
<span class="item-col merchant-col">
<strong>{{ receipt.merchant }}</strong>
<div class="receipt-number">#{{ receipt.receiptNumber }}</div>
</span>
<span class="item-col amount-col">
{{ receipt.currency }} {{ receipt.amount.toFixed(2) }}
</span>
<span class="item-col date-col">
{{ new Date(receipt.date).toLocaleDateString() }}
</span>
<span class="item-col category-col">
<span class="category-badge">{{ receipt.category }}</span>
</span>
<span class="item-col status-col">
<span :class="`status-badge ${receipt.status}`">
{{ formatStatus(receipt.status) }}
</span>
</span>
<span class="item-col actions-col">
<RowActionsMenu
:row-id="receipt.id"
:row="receipt"
@view="handleViewReceipt"
@copy-id="handleCopyReceiptId"
@copy-message="handleCopyReceiptData"
@export-json="handleExportReceipt"
@delete="handleDeleteReceipt"
/>
</span>
</div>
</div>
<!-- Detail modal -->
<div v-if="selectedReceipt" class="receipt-modal">
<div class="modal-overlay" @click="selectedReceipt = null"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Receipt Details</h2>
<button @click="selectedReceipt = null" class="close-btn">✕</button>
</div>
<div class="modal-body">
<div class="detail-row">
<label>Receipt Number:</label>
<span>#{{ selectedReceipt.receiptNumber }}</span>
</div>
<div class="detail-row">
<label>Merchant:</label>
<span>{{ selectedReceipt.merchant }}</span>
</div>
<div class="detail-row">
<label>Amount:</label>
<span>{{ selectedReceipt.currency }} {{ selectedReceipt.amount.toFixed(2) }}</span>
</div>
<div class="detail-row">
<label>Date:</label>
<span>{{ new Date(selectedReceipt.date).toLocaleDateString() }}</span>
</div>
<div class="detail-row">
<label>Category:</label>
<span class="category-badge">{{ selectedReceipt.category }}</span>
</div>
<div class="detail-row">
<label>Status:</label>
<span :class="`status-badge ${selectedReceipt.status}`">
{{ formatStatus(selectedReceipt.status) }}
</span>
</div>
<div class="detail-row">
<label>Submitted By:</label>
<span>{{ selectedReceipt.submittedBy }}</span>
</div>
<div class="detail-row">
<label>OCR Confidence:</label>
<span>{{ (selectedReceipt.extractionConfidence * 100).toFixed(0) }}%</span>
</div>
<div v-if="selectedReceipt.status === 'pending_review'" class="action-buttons">
<button class="btn approve" @click="approveReceipt">Approve</button>
<button class="btn reject" @click="rejectReceipt">Reject</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RowActionsMenu from '@/components/Table/RowActionsMenu.vue'
interface Receipt {
id: string
receiptNumber: string
merchant: string
amount: number
currency: string
date: string
category: string
status: 'pending_review' | 'approved' | 'reimbursed'
submittedBy: string
extractionConfidence: number
}
const receipts = ref<Receipt[]>([
{
id: 'RCP-2024-001',
receiptNumber: 'R-98765',
merchant: 'Office Depot',
amount: 85.50,
currency: 'USD',
date: '2024-01-15',
category: 'Office Supplies',
status: 'pending_review',
submittedBy: 'john.doe@company.com',
extractionConfidence: 0.92
},
{
id: 'RCP-2024-002',
receiptNumber: 'R-98766',
merchant: 'Starbucks',
amount: 12.50,
currency: 'USD',
date: '2024-01-16',
category: 'Meals',
status: 'pending_review',
submittedBy: 'jane.smith@company.com',
extractionConfidence: 0.88
},
{
id: 'RCP-2024-003',
receiptNumber: 'R-98767',
merchant: 'Shell Gas Station',
amount: 65.00,
currency: 'USD',
date: '2024-01-17',
category: 'Travel',
status: 'approved',
submittedBy: 'mike.johnson@company.com',
extractionConfidence: 0.95
},
{
id: 'RCP-2024-004',
receiptNumber: 'R-98768',
merchant: 'Hotel Marriott',
amount: 325.75,
currency: 'USD',
date: '2024-01-18',
category: 'Accommodation',
status: 'approved',
submittedBy: 'sarah.williams@company.com',
extractionConfidence: 0.96
},
{
id: 'RCP-2024-005',
receiptNumber: 'R-98769',
merchant: 'Uber',
amount: 34.20,
currency: 'USD',
date: '2024-01-19',
category: 'Travel',
status: 'reimbursed',
submittedBy: 'john.doe@company.com',
extractionConfidence: 0.91
},
{
id: 'RCP-2024-006',
receiptNumber: 'R-98770',
merchant: 'Restaurant Milano',
amount: 78.50,
currency: 'USD',
date: '2024-01-20',
category: 'Meals',
status: 'pending_review',
submittedBy: 'emily.brown@company.com',
extractionConfidence: 0.89
}
])
const selectedReceipt = ref<Receipt | null>(null)
function handleViewReceipt({ row }: { rowId: string; row: Receipt }) {
selectedReceipt.value = row
}
function handleCopyReceiptId({ rowId }: { rowId: string; row: any }) {
navigator.clipboard.writeText(rowId)
console.log('Copied receipt ID:', rowId)
}
function handleCopyReceiptData({ row }: { rowId: string; row: Receipt }) {
const data = `${row.merchant} - ${row.currency} ${row.amount.toFixed(2)}`
navigator.clipboard.writeText(data)
console.log('Copied receipt data')
}
function handleExportReceipt({ row, rowId }: { rowId: string; row: Receipt }) {
const json = JSON.stringify(row, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `receipt-${rowId}.json`
a.click()
URL.revokeObjectURL(url)
}
function handleDeleteReceipt({ rowId, row }: { rowId: string; row: Receipt }) {
const confirmed = confirm(`Delete receipt from ${row.merchant}?`)
if (confirmed) {
receipts.value = receipts.value.filter(r => r.id !== rowId)
console.log('Deleted receipt:', rowId)
}
}
function approveReceipt() {
if (selectedReceipt.value) {
selectedReceipt.value.status = 'approved'
const receipt = receipts.value.find(r => r.id === selectedReceipt.value?.id)
if (receipt) {
receipt.status = 'approved'
}
console.log('Approved receipt:', selectedReceipt.value.id)
}
}
function rejectReceipt() {
if (selectedReceipt.value) {
const confirmed = confirm('Reject this receipt?')
if (confirmed) {
receipts.value = receipts.value.filter(r => r.id !== selectedReceipt.value?.id)
selectedReceipt.value = null
console.log('Rejected receipt')
}
}
}
function formatStatus(status: string): string {
return status
.replace('_', ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>
<style scoped>
.receipt-list {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.list-header {
display: grid;
grid-template-columns: 2fr 120px 120px 120px 120px 80px;
gap: 12px;
padding: 16px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
font-size: 0.9em;
align-items: center;
}
.receipt-item {
display: grid;
grid-template-columns: 2fr 120px 120px 120px 120px 80px;
gap: 12px;
padding: 16px;
border-bottom: 1px solid #e0e0e0;
align-items: center;
transition: background 0.2s;
}
.receipt-item:hover {
background: #fafafa;
}
.item-col {
display: flex;
flex-direction: column;
justify-content: center;
}
.merchant-col {
min-width: 0;
}
.receipt-number {
font-size: 0.85em;
color: #666;
font-weight: normal;
margin-top: 2px;
}
.amount-col {
text-align: right;
font-weight: 600;
}
.date-col,
.category-col,
.status-col {
text-align: center;
}
.category-badge {
display: inline-block;
padding: 4px 8px;
background: #e8f4f8;
color: #2388AE;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75em;
font-weight: 600;
}
.status-badge.pending_review {
background: #fff3cd;
color: #856404;
}
.status-badge.approved {
background: #d4edda;
color: #155724;
}
.status-badge.reimbursed {
background: #34eea9;
color: white;
}
.actions-col {
display: flex;
justify-content: center;
}
/* Modal styles */
.receipt-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid #e0e0e0;
}
.modal-header h2 {
margin: 0;
font-size: 1.25em;
}
.close-btn {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
color: #666;
padding: 0;
}
.modal-body {
padding: 24px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.detail-row label {
font-weight: 600;
color: #333;
}
.detail-row span {
color: #666;
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
.btn {
flex: 1;
padding: 8px 16px;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn.approve {
background: #34eea9;
color: white;
}
.btn.approve:hover {
background: #2dd897;
}
.btn.reject {
background: #ff6b6b;
color: white;
}
.btn.reject:hover {
background: #ff5252;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
What This Example Shows
- View Details: Open receipt details in a modal with inline approval/rejection
- Copy Operations: Copy ID or formatted receipt data to clipboard
- Export JSON: Download receipt data as JSON file
- Inline Actions: Approve/reject from the detail modal
- Delete Action: Delete receipt with confirmation
- Status Tracking: Different statuses for review, approval, and reimbursement
- Multiple Categories: Organize receipts by category (meals, travel, accommodation, supplies)
- Real Employee Data: Track submissions by employee email addresses
Testing
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import RowActionsMenu from './RowActionsMenu.vue'
describe('RowActionsMenu', () => {
it('emits view event when view clicked', async () => {
const wrapper = mount(RowActionsMenu, {
props: {
rowId: '123',
row: { id: '123', name: 'Test' }
}
})
await wrapper.find('[aria-label*="View"]').trigger('click')
expect(wrapper.emitted('view')).toBeTruthy()
})
it('passes row data in event payload', async () => {
const row = { id: '123', name: 'Test', data: 'value' }
const wrapper = mount(RowActionsMenu, {
props: { rowId: '123', row }
})
await wrapper.find('[aria-label*="View"]').trigger('click')
const emitted = wrapper.emitted('view')[0][0] as any
expect(emitted.row).toEqual(row)
})
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Browser Support
Supported in all modern browsers:
- Chrome 88+
- Firefox 78+
- Safari 14+
- Edge 88+
Related Components
- BulkActionsMenu - Bulk operations
- DocBitsTable - Complete table with row actions
Source Code
- Component:
src/components/Table/RowActionsMenu.vue(152 lines)
Changelog
v1.0.0 (2025-12-18)
- Initial release
- Three-dot dropdown menu
- Standard actions (view, copy, export, delete)
- Conditional delete option
- Tooltip support