fix data table

This commit is contained in:
Casey Wittrock 2025-11-11 12:40:47 -06:00
parent 8fa55bea70
commit 10cda824e8
3 changed files with 151 additions and 103 deletions

View File

@ -768,102 +768,107 @@ const customFilters = {
```vue ```vue
<script setup> <script setup>
import { ref } from 'vue' import { ref } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
import { useModalStore } from '@/stores/modal' import { useModalStore } from "@/stores/modal";
const router = useRouter() const router = useRouter();
const modalStore = useModalStore() const modalStore = useModalStore();
const columns = [ const columns = [
{ fieldName: 'name', label: 'Client Name', sortable: true, filterable: true }, { fieldName: "name", label: "Client Name", sortable: true, filterable: true },
{ fieldName: 'email', label: 'Email', filterable: true }, { fieldName: "email", label: "Email", filterable: true },
{ fieldName: 'status', label: 'Status', type: 'status', sortable: true } { fieldName: "status", label: "Status", type: "status", sortable: true },
] ];
const data = ref([ const data = ref([
{ id: 1, name: 'Acme Corp', email: 'contact@acme.com', status: 'completed' }, { id: 1, name: "Acme Corp", email: "contact@acme.com", status: "completed" },
{ id: 2, name: 'Tech Solutions', email: 'info@tech.com', status: 'in progress' } {
]) id: 2,
name: "Tech Solutions",
email: "info@tech.com",
status: "in progress",
},
]);
const tableActions = [ const tableActions = [
// Left-positioned action with filled style // Left-positioned action with filled style
{ {
label: 'Quick Export', label: "Quick Export",
icon: 'pi pi-download', icon: "pi pi-download",
action: () => exportAllClients(), action: () => exportAllClients(),
severity: 'success', severity: "success",
layout: { position: 'left', variant: 'outlined' } layout: { position: "left", variant: "outlined" },
}, },
// Center-positioned bulk action // Center-positioned bulk action
{ {
label: 'Archive Selected', label: "Archive Selected",
icon: 'pi pi-archive', icon: "pi pi-archive",
action: (selectedRows) => archiveClients(selectedRows), action: (selectedRows) => archiveClients(selectedRows),
severity: 'warning', severity: "warning",
requiresMultipleSelection: true, requiresMultipleSelection: true,
layout: { position: 'center', variant: 'outlined' } layout: { position: "center", variant: "outlined" },
}, },
// Right-positioned main action // Right-positioned main action
{ {
label: 'Create Client', label: "Create Client",
icon: 'pi pi-plus', icon: "pi pi-plus",
action: () => modalStore.openModal('createClient'), action: () => modalStore.openModal("createClient"),
severity: 'info', severity: "info",
layout: { position: 'right', variant: 'filled' } layout: { position: "right", variant: "filled" },
}, },
// Single selection action // Single selection action
{ {
label: 'Edit Details', label: "Edit Details",
icon: 'pi pi-pencil', icon: "pi pi-pencil",
action: (rowData) => router.push(`/clients/${rowData.id}/edit`), action: (rowData) => router.push(`/clients/${rowData.id}/edit`),
severity: 'secondary', severity: "secondary",
requiresSelection: true, requiresSelection: true,
layout: { position: 'left', variant: 'text' } layout: { position: "left", variant: "text" },
}, },
// Primary row action - most important // Primary row action - most important
{ {
label: 'View', label: "View",
icon: 'pi pi-eye', icon: "pi pi-eye",
action: (rowData) => router.push(`/clients/${rowData.id}`), action: (rowData) => router.push(`/clients/${rowData.id}`),
severity: 'info', severity: "info",
rowAction: true, rowAction: true,
layout: { priority: 'primary', variant: 'outlined' } layout: { priority: "primary", variant: "outlined" },
}, },
// Secondary row action - less important // Secondary row action - less important
{ {
label: 'Contact', label: "Contact",
icon: 'pi pi-phone', icon: "pi pi-phone",
action: (rowData) => initiateContact(rowData), action: (rowData) => initiateContact(rowData),
severity: 'success', severity: "success",
rowAction: true, rowAction: true,
layout: { priority: 'secondary', variant: 'text' } layout: { priority: "secondary", variant: "text" },
}, },
// Dropdown row action - additional options // Dropdown row action - additional options
{ {
label: 'More', label: "More",
icon: 'pi pi-ellipsis-v', icon: "pi pi-ellipsis-v",
action: (rowData) => showMoreOptions(rowData), action: (rowData) => showMoreOptions(rowData),
rowAction: true, rowAction: true,
layout: { priority: 'dropdown', variant: 'icon-only' } layout: { priority: "dropdown", variant: "icon-only" },
} },
] ];
const exportAllClients = () => { const exportAllClients = () => {
// Export logic // Export logic
} };
const archiveClients = (clients) => { const archiveClients = (clients) => {
// Archive logic // Archive logic
} };
const initiateContact = (client) => { const initiateContact = (client) => {
// Contact logic // Contact logic
} };
const showMoreOptions = (client) => { const showMoreOptions = (client) => {
// More options logic // More options logic
} };
</script> </script>
<template> <template>

View File

@ -1,10 +1,6 @@
<template lang="html"> <template lang="html">
<!-- Filter Controls Panel --> <!-- Filter Controls Panel -->
<div <div v-if="hasFilters" :key="`filter-controls-${tableName}`" class="dt-filter-panel">
v-if="hasFilters"
:key="`filter-controls-${tableName}`"
class="dt-filter-panel"
>
<div class="dt-filter-header"> <div class="dt-filter-header">
<h6 class="dt-filter-title"> <h6 class="dt-filter-title">
<i class="pi pi-filter"></i> <i class="pi pi-filter"></i>
@ -85,14 +81,14 @@
</div> </div>
<!-- Bulk Actions Section (when rows are selected) --> <!-- Bulk Actions Section (when rows are selected) -->
<div <div v-if="hasBulkActions && hasSelectedRows" class="dt-bulk-actions-panel">
v-if="hasBulkActions && hasSelectedRows"
class="dt-bulk-actions-panel"
>
<div class="dt-bulk-actions-content"> <div class="dt-bulk-actions-content">
<div class="dt-bulk-actions-groups"> <div class="dt-bulk-actions-groups">
<!-- Left positioned bulk actions --> <!-- Left positioned bulk actions -->
<div v-if="bulkActionsGrouped.left.length > 0" class="dt-action-group dt-action-group-left"> <div
v-if="bulkActionsGrouped.left.length > 0"
class="dt-action-group dt-action-group-left"
>
<Button <Button
v-for="action in bulkActionsGrouped.left" v-for="action in bulkActionsGrouped.left"
:key="action.label" :key="action.label"
@ -106,7 +102,10 @@
/> />
</div> </div>
<!-- Center positioned bulk actions --> <!-- Center positioned bulk actions -->
<div v-if="bulkActionsGrouped.center.length > 0" class="dt-action-group dt-action-group-center"> <div
v-if="bulkActionsGrouped.center.length > 0"
class="dt-action-group dt-action-group-center"
>
<Button <Button
v-for="action in bulkActionsGrouped.center" v-for="action in bulkActionsGrouped.center"
:key="action.label" :key="action.label"
@ -120,7 +119,10 @@
/> />
</div> </div>
<!-- Right positioned bulk actions --> <!-- Right positioned bulk actions -->
<div v-if="bulkActionsGrouped.right.length > 0" class="dt-action-group dt-action-group-right"> <div
v-if="bulkActionsGrouped.right.length > 0"
class="dt-action-group dt-action-group-right"
>
<Button <Button
v-for="action in bulkActionsGrouped.right" v-for="action in bulkActionsGrouped.right"
:key="action.label" :key="action.label"
@ -136,7 +138,12 @@
</div> </div>
<div class="dt-bulk-actions-status"> <div class="dt-bulk-actions-status">
<i class="pi pi-check-circle"></i> <i class="pi pi-check-circle"></i>
<span>{{ selectedRows.length }} row{{ selectedRows.length !== 1 ? "s" : "" }} selected</span> <span
>{{ selectedRows.length }} row{{
selectedRows.length !== 1 ? "s" : ""
}}
selected</span
>
</div> </div>
</div> </div>
</div> </div>
@ -145,7 +152,10 @@
<div v-if="hasTopActions" class="dt-global-actions-panel"> <div v-if="hasTopActions" class="dt-global-actions-panel">
<div class="dt-global-actions-content"> <div class="dt-global-actions-content">
<!-- Left positioned actions --> <!-- Left positioned actions -->
<div v-if="topActionsGrouped.left.length > 0" class="dt-action-group dt-action-group-left"> <div
v-if="topActionsGrouped.left.length > 0"
class="dt-action-group dt-action-group-left"
>
<Button <Button
v-for="action in topActionsGrouped.left" v-for="action in topActionsGrouped.left"
:key="action.label" :key="action.label"
@ -159,7 +169,10 @@
/> />
</div> </div>
<!-- Center positioned actions --> <!-- Center positioned actions -->
<div v-if="topActionsGrouped.center.length > 0" class="dt-action-group dt-action-group-center"> <div
v-if="topActionsGrouped.center.length > 0"
class="dt-action-group dt-action-group-center"
>
<Button <Button
v-for="action in topActionsGrouped.center" v-for="action in topActionsGrouped.center"
:key="action.label" :key="action.label"
@ -173,7 +186,10 @@
/> />
</div> </div>
<!-- Right positioned actions --> <!-- Right positioned actions -->
<div v-if="topActionsGrouped.right.length > 0" class="dt-action-group dt-action-group-right"> <div
v-if="topActionsGrouped.right.length > 0"
class="dt-action-group dt-action-group-right"
>
<Button <Button
v-for="action in topActionsGrouped.right" v-for="action in topActionsGrouped.right"
:key="action.label" :key="action.label"
@ -190,7 +206,9 @@
<div v-if="singleSelectionActions.length > 0" class="dt-global-actions-status"> <div v-if="singleSelectionActions.length > 0" class="dt-global-actions-status">
<i class="pi pi-info-circle"></i> <i class="pi pi-info-circle"></i>
<span v-if="!hasSelectedRows">Select a row to enable single-selection actions</span> <span v-if="!hasSelectedRows">Select a row to enable single-selection actions</span>
<span v-else-if="selectedRows.length > 1">Select only one row to enable single-selection actions</span> <span v-else-if="selectedRows.length > 1"
>Select only one row to enable single-selection actions</span
>
<span v-else-if="hasExactlyOneRowSelected">Single-selection actions enabled</span> <span v-else-if="hasExactlyOneRowSelected">Single-selection actions enabled</span>
</div> </div>
</div> </div>
@ -277,7 +295,10 @@
<template #body="slotProps"> <template #body="slotProps">
<div class="dt-row-actions"> <div class="dt-row-actions">
<!-- Primary row actions --> <!-- Primary row actions -->
<div v-if="rowActionsGrouped.primary.length > 0" class="dt-row-actions-primary"> <div
v-if="rowActionsGrouped.primary.length > 0"
class="dt-row-actions-primary"
>
<Button <Button
v-for="action in rowActionsGrouped.primary" v-for="action in rowActionsGrouped.primary"
:key="action.label" :key="action.label"
@ -287,12 +308,18 @@
:size="action.size || 'small'" :size="action.size || 'small'"
@click="handleRowAction(action, slotProps.data)" @click="handleRowAction(action, slotProps.data)"
:disabled="loading" :disabled="loading"
:class="['dt-row-btn', action.layout?.variant && `dt-row-btn-${action.layout.variant}`]" :class="[
'dt-row-btn',
action.layout?.variant && `dt-row-btn-${action.layout.variant}`,
]"
outlined outlined
/> />
</div> </div>
<!-- Secondary row actions --> <!-- Secondary row actions -->
<div v-if="rowActionsGrouped.secondary.length > 0" class="dt-row-actions-secondary"> <div
v-if="rowActionsGrouped.secondary.length > 0"
class="dt-row-actions-secondary"
>
<Button <Button
v-for="action in rowActionsGrouped.secondary" v-for="action in rowActionsGrouped.secondary"
:key="action.label" :key="action.label"
@ -302,12 +329,19 @@
:size="action.size || 'small'" :size="action.size || 'small'"
@click="handleRowAction(action, slotProps.data)" @click="handleRowAction(action, slotProps.data)"
:disabled="loading" :disabled="loading"
:class="['dt-row-btn', 'dt-row-btn-secondary', action.layout?.variant && `dt-row-btn-${action.layout.variant}`]" :class="[
'dt-row-btn',
'dt-row-btn-secondary',
action.layout?.variant && `dt-row-btn-${action.layout.variant}`,
]"
text text
/> />
</div> </div>
<!-- Dropdown menu for overflow actions --> <!-- Dropdown menu for overflow actions -->
<div v-if="rowActionsGrouped.dropdown.length > 0" class="dt-row-actions-dropdown"> <div
v-if="rowActionsGrouped.dropdown.length > 0"
class="dt-row-actions-dropdown"
>
<Button <Button
icon="pi pi-ellipsis-v" icon="pi pi-ellipsis-v"
:size="'small'" :size="'small'"
@ -593,27 +627,33 @@ const bulkActions = computed(() => {
const topActionsGrouped = computed(() => { const topActionsGrouped = computed(() => {
const actions = [...globalActions.value, ...singleSelectionActions.value]; const actions = [...globalActions.value, ...singleSelectionActions.value];
const groups = { const groups = {
left: actions.filter(action => action.layout?.position === 'left' || !action.layout?.position), left: actions.filter(
center: actions.filter(action => action.layout?.position === 'center'), (action) => action.layout?.position === "left" || !action.layout?.position,
right: actions.filter(action => action.layout?.position === 'right'), ),
center: actions.filter((action) => action.layout?.position === "center"),
right: actions.filter((action) => action.layout?.position === "right"),
}; };
return groups; return groups;
}); });
const bulkActionsGrouped = computed(() => { const bulkActionsGrouped = computed(() => {
const groups = { const groups = {
left: bulkActions.value.filter(action => action.layout?.position === 'left' || !action.layout?.position), left: bulkActions.value.filter(
center: bulkActions.value.filter(action => action.layout?.position === 'center'), (action) => action.layout?.position === "left" || !action.layout?.position,
right: bulkActions.value.filter(action => action.layout?.position === 'right'), ),
center: bulkActions.value.filter((action) => action.layout?.position === "center"),
right: bulkActions.value.filter((action) => action.layout?.position === "right"),
}; };
return groups; return groups;
}); });
const rowActionsGrouped = computed(() => { const rowActionsGrouped = computed(() => {
const groups = { const groups = {
primary: rowActions.value.filter(action => action.layout?.priority === 'primary' || !action.layout?.priority), primary: rowActions.value.filter(
secondary: rowActions.value.filter(action => action.layout?.priority === 'secondary'), (action) => action.layout?.priority === "primary" || !action.layout?.priority,
dropdown: rowActions.value.filter(action => action.layout?.priority === 'dropdown'), ),
secondary: rowActions.value.filter((action) => action.layout?.priority === "secondary"),
dropdown: rowActions.value.filter((action) => action.layout?.priority === "dropdown"),
}; };
return groups; return groups;
}); });
@ -928,9 +968,9 @@ const handleTopAction = (action) => {
// Helper methods for action styling and behavior // Helper methods for action styling and behavior
const getActionSeverity = (action) => { const getActionSeverity = (action) => {
if (action.requiresSelection) { if (action.requiresSelection) {
return action.style || 'info'; return action.style || "info";
} }
return action.style || 'primary'; return action.style || "primary";
}; };
const getActionDisabled = (action) => { const getActionDisabled = (action) => {
@ -942,14 +982,14 @@ const getActionDisabled = (action) => {
}; };
const getActionClasses = (action) => { const getActionClasses = (action) => {
const classes = ['dt-action-btn']; const classes = ["dt-action-btn"];
if (action.requiresSelection) { if (action.requiresSelection) {
classes.push('dt-action-btn-selection'); classes.push("dt-action-btn-selection");
if (!hasExactlyOneRowSelected.value) { if (!hasExactlyOneRowSelected.value) {
classes.push('dt-action-btn-disabled'); classes.push("dt-action-btn-disabled");
} }
} else { } else {
classes.push('dt-action-btn-global'); classes.push("dt-action-btn-global");
} }
if (action.layout?.variant) { if (action.layout?.variant) {
classes.push(`dt-action-btn-${action.layout.variant}`); classes.push(`dt-action-btn-${action.layout.variant}`);
@ -960,7 +1000,7 @@ const getActionClasses = (action) => {
const toggleRowDropdown = (event, rowData) => { const toggleRowDropdown = (event, rowData) => {
// Placeholder for dropdown menu functionality // Placeholder for dropdown menu functionality
// Could be implemented with PrimeVue OverlayPanel or Menu component // Could be implemented with PrimeVue OverlayPanel or Menu component
console.log('Toggle dropdown for row:', rowData); console.log("Toggle dropdown for row:", rowData);
}; };
const handleBulkAction = (action, selectedRows) => { const handleBulkAction = (action, selectedRows) => {
@ -1126,7 +1166,9 @@ defineExpose({
border-radius: 8px; border-radius: 8px;
padding: 0.625rem; padding: 0.625rem;
font-size: 0.875rem; font-size: 0.875rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease; transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
} }
.dt-filter-input:focus { .dt-filter-input:focus {
@ -1141,7 +1183,8 @@ defineExpose({
flex-wrap: wrap; flex-wrap: wrap;
} }
.dt-btn-primary, .dt-btn-secondary { .dt-btn-primary,
.dt-btn-secondary {
border-radius: 8px; border-radius: 8px;
font-weight: 500; font-weight: 500;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -1433,22 +1476,22 @@ defineExpose({
.dt-filter-grid { .dt-filter-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.dt-pagination-content { .dt-pagination-content {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
text-align: center; text-align: center;
} }
.dt-global-actions-content { .dt-global-actions-content {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.dt-action-group { .dt-action-group {
justify-content: center; justify-content: center;
} }
.dt-bulk-actions-groups { .dt-bulk-actions-groups {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@ -1464,20 +1507,20 @@ defineExpose({
border-color: #374151; border-color: #374151;
color: #f9fafb; color: #f9fafb;
} }
.dt-filter-title, .dt-filter-title,
.dt-filter-label, .dt-filter-label,
.dt-pagination-label { .dt-pagination-label {
color: #f9fafb; color: #f9fafb;
} }
.dt-filter-input, .dt-filter-input,
.dt-pagination-select { .dt-pagination-select {
background: #374151; background: #374151;
border-color: #4b5563; border-color: #4b5563;
color: #f9fafb; color: #f9fafb;
} }
.dt-filter-status, .dt-filter-status,
.dt-global-actions-status { .dt-global-actions-status {
background: #374151; background: #374151;

View File

@ -158,8 +158,8 @@ const tableActions = [
icon: "pi pi-plus", icon: "pi pi-plus",
layout: { layout: {
position: "left", position: "left",
variant: "filled" variant: "filled",
} },
// Global action - always available // Global action - always available
}, },
{ {
@ -173,8 +173,8 @@ const tableActions = [
requiresSelection: true, // Single selection action - appears above table, enabled when exactly one row selected requiresSelection: true, // Single selection action - appears above table, enabled when exactly one row selected
layout: { layout: {
position: "center", position: "center",
variant: "outlined" variant: "outlined",
} },
}, },
{ {
label: "Export Selected", label: "Export Selected",
@ -188,8 +188,8 @@ const tableActions = [
requiresMultipleSelection: true, // Bulk action - operates on selected rows requiresMultipleSelection: true, // Bulk action - operates on selected rows
layout: { layout: {
position: "right", position: "right",
variant: "filled" variant: "filled",
} },
}, },
{ {
label: "Edit", label: "Edit",
@ -203,8 +203,8 @@ const tableActions = [
rowAction: true, // Row action - appears in each row's actions column rowAction: true, // Row action - appears in each row's actions column
layout: { layout: {
priority: "primary", priority: "primary",
variant: "outlined" variant: "outlined",
} },
}, },
{ {
label: "Quick View", label: "Quick View",
@ -218,8 +218,8 @@ const tableActions = [
rowAction: true, // Row action - appears in each row's actions column rowAction: true, // Row action - appears in each row's actions column
layout: { layout: {
priority: "secondary", priority: "secondary",
variant: "compact" variant: "compact",
} },
}, },
]; ];
// Handle lazy loading events from DataTable // Handle lazy loading events from DataTable