Skip to content

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

PropTypeDefaultRequiredDescription
rowIdstring-✅ YesRow identifier
rowany--Complete row data object
disabledbooleanfalse-Disable all actions

Events

EventPayloadDescription
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

vue
<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>

Standard Actions

View Details

Navigate to row detail page:

typescript
@view="({ rowId, row }) => router.push(`/documents/${rowId}`)"

Copy ID

Copy row ID to clipboard:

typescript
@copy-id="({ rowId }) => {
  navigator.clipboard.writeText(rowId)
  showNotification('ID copied')
}"

Copy Message

Copy row message/content:

typescript
@copy-message="({ row }) => {
  navigator.clipboard.writeText(row.message)
}"

Export as JSON

Download row as JSON file:

typescript
@export-json="({ row, rowId }) => {
  const json = JSON.stringify(row, null, 2)
  const blob = new Blob([json], { type: 'application/json' })
  downloadFile(blob, `row-${rowId}.json`)
}"

Delete Row

Delete row with confirmation:

typescript
@delete="async ({ rowId, row }) => {
  const confirmed = await showConfirmDialog(
    `Delete ${row.name}?`
  )
  if (confirmed) {
    await api.deleteRow(rowId)
  }
}"

Advanced Examples

Conditional Delete

Only show delete for non-archived rows:

vue
<RowActionsMenu
  :row-id="String(row.id)"
  :row="row"
  :show-delete-option="row.status !== 'archived'"
/>

Custom Actions

Extend menu with custom actions:

vue
<RowActionsMenu
  :row-id="String(row.id)"
  :row="row"
  :actions="customActions"
  @action="handleCustomAction"
/>

With Loading State

typescript
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)
  }
}

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

vue
<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>

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

vue
<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>

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

typescript
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)
  })
})

Browser Support

Supported in all modern browsers:

  • Chrome 88+
  • Firefox 78+
  • Safari 14+
  • Edge 88+

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

DocBits Component Library