1519 lines
37 KiB
Vue
1519 lines
37 KiB
Vue
<template lang="html">
|
|
<!-- Filter Controls Panel -->
|
|
<div
|
|
v-if="hasFilters"
|
|
:key="`filter-controls-${tableName}`"
|
|
class="dt-filter-panel"
|
|
>
|
|
<div class="dt-filter-header">
|
|
<h6 class="dt-filter-title">
|
|
<i class="pi pi-filter"></i>
|
|
Filters
|
|
</h6>
|
|
</div>
|
|
<div class="dt-filter-content">
|
|
<div class="dt-filter-grid">
|
|
<div v-for="col in filterableColumns" :key="col.fieldName" class="dt-filter-field">
|
|
<label :for="`filter-${col.fieldName}`" class="dt-filter-label">
|
|
{{ col.label }}
|
|
</label>
|
|
<InputText
|
|
:id="`filter-${col.fieldName}`"
|
|
v-model="pendingFilters[col.fieldName]"
|
|
:placeholder="`Filter by ${col.label.toLowerCase()}...`"
|
|
class="dt-filter-input"
|
|
@keyup.enter="applyFilters"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="dt-filter-actions">
|
|
<Button
|
|
label="Apply"
|
|
icon="pi pi-check"
|
|
@click="applyFilters"
|
|
:disabled="!hasFilterChanges"
|
|
class="dt-btn-primary"
|
|
size="small"
|
|
/>
|
|
<Button
|
|
label="Clear"
|
|
icon="pi pi-times"
|
|
@click="clearFilters"
|
|
severity="secondary"
|
|
outlined
|
|
size="small"
|
|
:disabled="!hasActiveFilters"
|
|
class="dt-btn-secondary"
|
|
/>
|
|
</div>
|
|
<div v-if="hasActiveFilters" class="dt-filter-status">
|
|
<i class="pi pi-info-circle"></i>
|
|
<span>Active filters: {{ getActiveFiltersText() }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Page Jump Controls -->
|
|
<div
|
|
v-if="totalPages > 1"
|
|
:key="`page-controls-${totalPages}-${getPageInfo().total}`"
|
|
class="dt-pagination-panel"
|
|
>
|
|
<div class="dt-pagination-content">
|
|
<div class="dt-pagination-info">
|
|
<i class="pi pi-info-circle"></i>
|
|
<span class="dt-pagination-text">
|
|
Showing {{ getPageInfo().start }} - {{ getPageInfo().end }} of
|
|
{{ getPageInfo().total }} records
|
|
</span>
|
|
</div>
|
|
<div class="dt-pagination-controls">
|
|
<label for="page-jump" class="dt-pagination-label">Jump to:</label>
|
|
<select
|
|
id="page-jump"
|
|
v-model="selectedPageJump"
|
|
@change="jumpToPage"
|
|
class="dt-pagination-select"
|
|
>
|
|
<option value="">Page...</option>
|
|
<option v-for="page in totalPages" :key="page" :value="page">
|
|
Page {{ page }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Actions Section (when rows are selected) -->
|
|
<div
|
|
v-if="hasBulkActions && hasSelectedRows"
|
|
class="dt-bulk-actions-panel"
|
|
>
|
|
<div class="dt-bulk-actions-content">
|
|
<div class="dt-bulk-actions-groups">
|
|
<!-- Left positioned bulk actions -->
|
|
<div v-if="bulkActionsGrouped.left.length > 0" class="dt-action-group dt-action-group-left">
|
|
<Button
|
|
v-for="action in bulkActionsGrouped.left"
|
|
:key="action.label"
|
|
:label="`${action.label} (${selectedRows.length})`"
|
|
:severity="action.style || 'warning'"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleBulkAction(action, selectedRows)"
|
|
:disabled="loading"
|
|
class="dt-bulk-btn"
|
|
/>
|
|
</div>
|
|
<!-- Center positioned bulk actions -->
|
|
<div v-if="bulkActionsGrouped.center.length > 0" class="dt-action-group dt-action-group-center">
|
|
<Button
|
|
v-for="action in bulkActionsGrouped.center"
|
|
:key="action.label"
|
|
:label="`${action.label} (${selectedRows.length})`"
|
|
:severity="action.style || 'warning'"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleBulkAction(action, selectedRows)"
|
|
:disabled="loading"
|
|
class="dt-bulk-btn"
|
|
/>
|
|
</div>
|
|
<!-- Right positioned bulk actions -->
|
|
<div v-if="bulkActionsGrouped.right.length > 0" class="dt-action-group dt-action-group-right">
|
|
<Button
|
|
v-for="action in bulkActionsGrouped.right"
|
|
:key="action.label"
|
|
:label="`${action.label} (${selectedRows.length})`"
|
|
:severity="action.style || 'warning'"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleBulkAction(action, selectedRows)"
|
|
:disabled="loading"
|
|
class="dt-bulk-btn"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="dt-bulk-actions-status">
|
|
<i class="pi pi-check-circle"></i>
|
|
<span>{{ selectedRows.length }} row{{ selectedRows.length !== 1 ? "s" : "" }} selected</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Global Actions Section -->
|
|
<div v-if="hasTopActions" class="dt-global-actions-panel">
|
|
<div class="dt-global-actions-content">
|
|
<!-- Left positioned actions -->
|
|
<div v-if="topActionsGrouped.left.length > 0" class="dt-action-group dt-action-group-left">
|
|
<Button
|
|
v-for="action in topActionsGrouped.left"
|
|
:key="action.label"
|
|
:label="action.label"
|
|
:severity="getActionSeverity(action)"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleTopAction(action)"
|
|
:disabled="getActionDisabled(action)"
|
|
:class="getActionClasses(action)"
|
|
/>
|
|
</div>
|
|
<!-- Center positioned actions -->
|
|
<div v-if="topActionsGrouped.center.length > 0" class="dt-action-group dt-action-group-center">
|
|
<Button
|
|
v-for="action in topActionsGrouped.center"
|
|
:key="action.label"
|
|
:label="action.label"
|
|
:severity="getActionSeverity(action)"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleTopAction(action)"
|
|
:disabled="getActionDisabled(action)"
|
|
:class="getActionClasses(action)"
|
|
/>
|
|
</div>
|
|
<!-- Right positioned actions -->
|
|
<div v-if="topActionsGrouped.right.length > 0" class="dt-action-group dt-action-group-right">
|
|
<Button
|
|
v-for="action in topActionsGrouped.right"
|
|
:key="action.label"
|
|
:label="action.label"
|
|
:severity="getActionSeverity(action)"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleTopAction(action)"
|
|
:disabled="getActionDisabled(action)"
|
|
:class="getActionClasses(action)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-if="singleSelectionActions.length > 0" class="dt-global-actions-status">
|
|
<i class="pi pi-info-circle"></i>
|
|
<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="hasExactlyOneRowSelected">Single-selection actions enabled</span>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
ref="dataTableRef"
|
|
:value="data"
|
|
:rowsPerPageOptions="[5, 10, 20, 50]"
|
|
:paginator="true"
|
|
:rows="currentRows"
|
|
:first="currentFirst"
|
|
:lazy="lazy"
|
|
:totalRecords="lazy ? totalRecords : getFilteredDataLength"
|
|
@page="handlePage"
|
|
@sort="triggerLazyLoad"
|
|
@filter="handleFilter"
|
|
sortMode="single"
|
|
removableSort
|
|
filterDisplay="none"
|
|
v-model:filters="filterRef"
|
|
scrollable
|
|
scrollHeight="70vh"
|
|
v-model:selection="selectedRows"
|
|
selectionMode="multiple"
|
|
metaKeySelection="true"
|
|
dataKey="id"
|
|
:loading="loading"
|
|
:loadingIcon="loadingIcon"
|
|
>
|
|
<template #empty>
|
|
<div class="text-center py-6">
|
|
<i class="pi pi-info-circle text-4xl text-gray-400 mb-2"></i>
|
|
<p class="text-gray-500">{{ emptyMessage || "No data available" }}</p>
|
|
</div>
|
|
</template>
|
|
<template #loading>
|
|
<div class="text-center py-6">
|
|
<i class="pi pi-spin pi-spinner text-4xl text-blue-500 mb-2"></i>
|
|
<p class="text-gray-600">{{ loadingMessage || "Loading data. Please wait..." }}</p>
|
|
</div>
|
|
</template>
|
|
<Column
|
|
v-for="col in columns"
|
|
:key="col.fieldName"
|
|
:field="col.fieldName"
|
|
:header="col.label"
|
|
:sortable="col.sortable"
|
|
>
|
|
<template v-if="col.filterable === true" #filter="{ filterModel, filterCallback }">
|
|
<InputText
|
|
v-model="filterModel.value"
|
|
type="text"
|
|
@input="handleFilterInput(col.fieldName, filterModel.value, filterCallback)"
|
|
:placeholder="`Search ${col.label}...`"
|
|
:disabled="loading"
|
|
/>
|
|
</template>
|
|
<template v-if="col.type === 'status'" #body="slotProps">
|
|
<Tag
|
|
:value="slotProps.data[col.fieldName]"
|
|
:severity="getBadgeColor(slotProps.data[col.fieldName])"
|
|
/>
|
|
</template>
|
|
<template v-if="col.type === 'date'" #body="slotProps">
|
|
<span>{{ formatDate(slotProps.data[col.fieldName]) }}</span>
|
|
</template>
|
|
<template v-if="col.type === 'button'" #body="slotProps">
|
|
<Button
|
|
:label="slotProps.data[col.fieldName]"
|
|
size="small"
|
|
severity="info"
|
|
@click="$emit('rowClick', slotProps)"
|
|
/>
|
|
</template>
|
|
</Column>
|
|
|
|
<!-- Actions Column -->
|
|
<Column
|
|
v-if="rowActions.length > 0"
|
|
header="Actions"
|
|
:exportable="false"
|
|
class="dt-actions-column"
|
|
>
|
|
<template #body="slotProps">
|
|
<div class="dt-row-actions">
|
|
<!-- Primary row actions -->
|
|
<div v-if="rowActionsGrouped.primary.length > 0" class="dt-row-actions-primary">
|
|
<Button
|
|
v-for="action in rowActionsGrouped.primary"
|
|
:key="action.label"
|
|
:label="action.label"
|
|
:severity="action.style || 'secondary'"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleRowAction(action, slotProps.data)"
|
|
:disabled="loading"
|
|
:class="['dt-row-btn', action.layout?.variant && `dt-row-btn-${action.layout.variant}`]"
|
|
outlined
|
|
/>
|
|
</div>
|
|
<!-- Secondary row actions -->
|
|
<div v-if="rowActionsGrouped.secondary.length > 0" class="dt-row-actions-secondary">
|
|
<Button
|
|
v-for="action in rowActionsGrouped.secondary"
|
|
:key="action.label"
|
|
:label="action.label"
|
|
:severity="action.style || 'secondary'"
|
|
:icon="action.icon"
|
|
:size="action.size || 'small'"
|
|
@click="handleRowAction(action, slotProps.data)"
|
|
:disabled="loading"
|
|
:class="['dt-row-btn', 'dt-row-btn-secondary', action.layout?.variant && `dt-row-btn-${action.layout.variant}`]"
|
|
text
|
|
/>
|
|
</div>
|
|
<!-- Dropdown menu for overflow actions -->
|
|
<div v-if="rowActionsGrouped.dropdown.length > 0" class="dt-row-actions-dropdown">
|
|
<Button
|
|
icon="pi pi-ellipsis-v"
|
|
:size="'small'"
|
|
severity="secondary"
|
|
text
|
|
@click="toggleRowDropdown($event, slotProps.data)"
|
|
class="dt-row-btn dt-row-btn-dropdown"
|
|
aria-haspopup="true"
|
|
/>
|
|
<!-- Dropdown menu would go here - could be implemented with PrimeVue Menu component -->
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
</template>
|
|
<script setup>
|
|
import { defineProps, computed, onMounted, watch, ref } from "vue";
|
|
import DataTable from "primevue/datatable";
|
|
import Column from "primevue/column";
|
|
import Tag from "primevue/tag";
|
|
import Button from "primevue/button";
|
|
import InputText from "primevue/inputtext";
|
|
import { FilterMatchMode } from "@primevue/core";
|
|
import { useFiltersStore } from "../../stores/filters";
|
|
import { useLoadingStore } from "../../stores/loading";
|
|
import { usePaginationStore } from "../../stores/pagination";
|
|
|
|
const filtersStore = useFiltersStore();
|
|
const loadingStore = useLoadingStore();
|
|
const paginationStore = usePaginationStore();
|
|
|
|
const props = defineProps({
|
|
columns: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
data: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
filters: {
|
|
type: Object,
|
|
default: () => ({
|
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
|
}),
|
|
},
|
|
tableName: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
loadingMessage: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
emptyMessage: {
|
|
type: String,
|
|
default: "",
|
|
},
|
|
loadingIcon: {
|
|
type: String,
|
|
default: "pi pi-spinner pi-spin",
|
|
},
|
|
// Auto-connect to global loading store
|
|
useGlobalLoading: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
// Server-side pagination support
|
|
lazy: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
totalRecords: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
// Total filtered records for non-lazy tables (when server-side filtering is used)
|
|
totalFilteredRecords: {
|
|
type: Number,
|
|
default: null,
|
|
},
|
|
// Custom pagination event handler
|
|
onLazyLoad: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
// Table actions for rows
|
|
tableActions: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
"rowClick",
|
|
"lazy-load",
|
|
"page-change",
|
|
"sort-change",
|
|
"filter-change",
|
|
"data-refresh",
|
|
]);
|
|
|
|
// Computed loading state that considers both prop and global store
|
|
const loading = computed(() => {
|
|
if (props.useGlobalLoading) {
|
|
return (
|
|
props.loading ||
|
|
loadingStore.getComponentLoading("dataTable") ||
|
|
loadingStore.getComponentLoading(props.tableName) ||
|
|
loadingStore.isAnyLoading
|
|
);
|
|
}
|
|
return props.loading;
|
|
});
|
|
|
|
// Get current rows per page from pagination store or default
|
|
const currentRows = computed(() => {
|
|
if (props.lazy) {
|
|
return paginationStore.getTablePagination(props.tableName).rows;
|
|
}
|
|
return 10; // Default for non-lazy tables
|
|
});
|
|
|
|
// Get current first index for pagination synchronization
|
|
const currentFirst = computed(() => {
|
|
if (props.lazy) {
|
|
return paginationStore.getTablePagination(props.tableName).first;
|
|
}
|
|
return currentPageState.value.first;
|
|
});
|
|
|
|
// Initialize filters and pagination in store when component mounts
|
|
onMounted(() => {
|
|
filtersStore.initializeTableFilters(props.tableName, props.columns);
|
|
filtersStore.initializeTableSorting(props.tableName);
|
|
if (props.lazy) {
|
|
paginationStore.initializeTablePagination(props.tableName, {
|
|
rows: 10,
|
|
totalRecords: props.totalRecords,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get filters from store, with fallback to props.filters
|
|
const filterRef = computed({
|
|
get() {
|
|
const storeFilters = filtersStore.getTableFilters(props.tableName);
|
|
// Merge store filters with any additional filters from props
|
|
return { ...props.filters, ...storeFilters };
|
|
},
|
|
set(newFilters) {
|
|
// Update store when filters change
|
|
Object.keys(newFilters).forEach((key) => {
|
|
if (key !== "global" && newFilters[key]) {
|
|
const filter = newFilters[key];
|
|
filtersStore.updateTableFilter(
|
|
props.tableName,
|
|
key,
|
|
filter.value,
|
|
filter.matchMode,
|
|
);
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
// Watch for changes in total records to update page controls reactivity
|
|
watch(
|
|
() => props.totalRecords,
|
|
(newTotal) => {
|
|
if (props.lazy && newTotal !== undefined) {
|
|
// Force reactivity update for page controls
|
|
selectedPageJump.value = "";
|
|
}
|
|
},
|
|
);
|
|
|
|
// Watch for data changes to update page controls for non-lazy tables
|
|
watch(
|
|
() => props.data,
|
|
(newData) => {
|
|
if (!props.lazy && newData) {
|
|
// Force reactivity update for page controls
|
|
selectedPageJump.value = "";
|
|
}
|
|
},
|
|
);
|
|
|
|
// Watch for filter changes to sync match mode changes
|
|
watch(
|
|
filterRef,
|
|
(newFilters) => {
|
|
Object.keys(newFilters).forEach((key) => {
|
|
if (key !== "global" && newFilters[key]) {
|
|
const filter = newFilters[key];
|
|
const storeFilter = filtersStore.getTableFilters(props.tableName)[key];
|
|
|
|
// Only update if the match mode has actually changed
|
|
if (storeFilter && storeFilter.matchMode !== filter.matchMode) {
|
|
filtersStore.updateTableFilter(
|
|
props.tableName,
|
|
key,
|
|
filter.value,
|
|
filter.matchMode,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
const selectedRows = ref();
|
|
const pendingFilters = ref({});
|
|
const selectedPageJump = ref("");
|
|
const dataTableRef = ref();
|
|
const currentPageState = ref({ page: 0, first: 0 }); // Track current page for non-lazy tables
|
|
|
|
// Computed properties for filtering
|
|
const filterableColumns = computed(() => {
|
|
return props.columns.filter((col) => col.filterable);
|
|
});
|
|
|
|
const hasFilters = computed(() => {
|
|
return filterableColumns.value.length > 0;
|
|
});
|
|
|
|
const hasActiveFilters = computed(() => {
|
|
return filtersStore.getActiveFiltersCount(props.tableName) > 0;
|
|
});
|
|
|
|
const hasFilterChanges = computed(() => {
|
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
|
return Object.keys(pendingFilters.value).some((key) => {
|
|
const pending = pendingFilters.value[key] || "";
|
|
const current = currentFilters[key]?.value || "";
|
|
return pending.trim() !== current.trim();
|
|
});
|
|
});
|
|
|
|
const totalPages = computed(() => {
|
|
if (props.lazy) {
|
|
return paginationStore.getTotalPages(props.tableName);
|
|
}
|
|
// For non-lazy tables, calculate based on current filtered data
|
|
const filteredDataLength = getFilteredDataLength.value;
|
|
return Math.ceil(filteredDataLength / currentRows.value) || 1;
|
|
});
|
|
|
|
// Computed properties for table actions
|
|
const hasActions = computed(() => {
|
|
return props.tableActions && props.tableActions.length > 0;
|
|
});
|
|
|
|
const globalActions = computed(() => {
|
|
return props.tableActions.filter(
|
|
(action) =>
|
|
!action.requiresSelection && !action.requiresMultipleSelection && !action.rowAction,
|
|
);
|
|
});
|
|
|
|
const singleSelectionActions = computed(() => {
|
|
return props.tableActions.filter(
|
|
(action) =>
|
|
action.requiresSelection && !action.requiresMultipleSelection && !action.rowAction,
|
|
);
|
|
});
|
|
|
|
const rowActions = computed(() => {
|
|
return props.tableActions.filter((action) => action.rowAction === true);
|
|
});
|
|
|
|
const bulkActions = computed(() => {
|
|
return props.tableActions.filter((action) => action.requiresMultipleSelection === true);
|
|
});
|
|
|
|
// Layout-based action grouping
|
|
const topActionsGrouped = computed(() => {
|
|
const actions = [...globalActions.value, ...singleSelectionActions.value];
|
|
const groups = {
|
|
left: actions.filter(action => action.layout?.position === 'left' || !action.layout?.position),
|
|
center: actions.filter(action => action.layout?.position === 'center'),
|
|
right: actions.filter(action => action.layout?.position === 'right'),
|
|
};
|
|
return groups;
|
|
});
|
|
|
|
const bulkActionsGrouped = computed(() => {
|
|
const groups = {
|
|
left: bulkActions.value.filter(action => action.layout?.position === 'left' || !action.layout?.position),
|
|
center: bulkActions.value.filter(action => action.layout?.position === 'center'),
|
|
right: bulkActions.value.filter(action => action.layout?.position === 'right'),
|
|
};
|
|
return groups;
|
|
});
|
|
|
|
const rowActionsGrouped = computed(() => {
|
|
const groups = {
|
|
primary: rowActions.value.filter(action => action.layout?.priority === 'primary' || !action.layout?.priority),
|
|
secondary: rowActions.value.filter(action => action.layout?.priority === 'secondary'),
|
|
dropdown: rowActions.value.filter(action => action.layout?.priority === 'dropdown'),
|
|
};
|
|
return groups;
|
|
});
|
|
|
|
const hasBulkActions = computed(() => {
|
|
return bulkActions.value.length > 0;
|
|
});
|
|
|
|
const hasSingleSelectionActions = computed(() => {
|
|
return singleSelectionActions.value.length > 0;
|
|
});
|
|
|
|
const hasTopActions = computed(() => {
|
|
return globalActions.value.length > 0 || singleSelectionActions.value.length > 0;
|
|
});
|
|
|
|
const hasSelectedRows = computed(() => {
|
|
return selectedRows.value && selectedRows.value.length > 0;
|
|
});
|
|
|
|
const hasExactlyOneRowSelected = computed(() => {
|
|
return selectedRows.value && selectedRows.value.length === 1;
|
|
});
|
|
|
|
// Initialize pending filters from store
|
|
onMounted(() => {
|
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
|
filterableColumns.value.forEach((col) => {
|
|
pendingFilters.value[col.fieldName] = currentFilters[col.fieldName]?.value || "";
|
|
});
|
|
});
|
|
|
|
// Computed property to get filtered data length for non-lazy tables
|
|
const getFilteredDataLength = computed(() => {
|
|
if (props.lazy) {
|
|
return props.totalRecords;
|
|
}
|
|
|
|
// For non-lazy tables, use totalFilteredRecords if provided (server-side filtering),
|
|
// otherwise fall back to data length (client-side filtering)
|
|
return props.totalFilteredRecords !== null ? props.totalFilteredRecords : props.data.length;
|
|
});
|
|
|
|
// Filter management methods
|
|
const applyFilters = () => {
|
|
// Update store with pending filter values
|
|
Object.keys(pendingFilters.value).forEach((fieldName) => {
|
|
const value = pendingFilters.value[fieldName]?.trim();
|
|
filtersStore.updateTableFilter(
|
|
props.tableName,
|
|
fieldName,
|
|
value || null,
|
|
FilterMatchMode.CONTAINS,
|
|
);
|
|
});
|
|
|
|
// Reset to first page when filters change (for both lazy and non-lazy)
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
selectedPageJump.value = ""; // Reset page jump dropdown
|
|
|
|
// For both lazy and non-lazy tables, trigger reload with new filters
|
|
if (props.lazy) {
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
triggerLazyLoad();
|
|
} else {
|
|
// For non-lazy tables, also trigger a reload to get fresh data from server
|
|
triggerDataRefresh();
|
|
}
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
// Clear pending filters
|
|
filterableColumns.value.forEach((col) => {
|
|
pendingFilters.value[col.fieldName] = "";
|
|
});
|
|
|
|
// Clear store filters (but keep sorting)
|
|
filtersStore.clearTableFilters(props.tableName);
|
|
|
|
// Reset to first page when filters are cleared (for both lazy and non-lazy)
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
selectedPageJump.value = ""; // Reset page jump dropdown
|
|
|
|
// For both lazy and non-lazy tables, trigger reload with cleared filters
|
|
if (props.lazy) {
|
|
paginationStore.clearTableCache(props.tableName);
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
triggerLazyLoad();
|
|
} else {
|
|
// For non-lazy tables, also trigger a reload to get fresh data from server
|
|
triggerDataRefresh();
|
|
}
|
|
};
|
|
|
|
const getActiveFiltersText = () => {
|
|
const currentFilters = filtersStore.getTableFilters(props.tableName);
|
|
const activeFilters = [];
|
|
Object.keys(currentFilters).forEach((key) => {
|
|
if (currentFilters[key]?.value) {
|
|
const column = filterableColumns.value.find((col) => col.fieldName === key);
|
|
if (column) {
|
|
activeFilters.push(`${column.label}: "${currentFilters[key].value}"`);
|
|
}
|
|
}
|
|
});
|
|
return activeFilters.join(", ");
|
|
};
|
|
|
|
// Page navigation methods
|
|
const jumpToPage = () => {
|
|
if (selectedPageJump.value) {
|
|
const pageNumber = parseInt(selectedPageJump.value) - 1; // Convert to 0-based
|
|
|
|
if (props.lazy) {
|
|
// Update pagination store - the currentFirst computed property will automatically sync
|
|
paginationStore.setPage(props.tableName, pageNumber);
|
|
triggerLazyLoad();
|
|
} else {
|
|
// For non-lazy tables, update our internal state
|
|
currentPageState.value = {
|
|
page: pageNumber,
|
|
first: pageNumber * currentRows.value,
|
|
};
|
|
}
|
|
}
|
|
selectedPageJump.value = ""; // Reset selection
|
|
};
|
|
|
|
const getPageInfo = () => {
|
|
if (props.lazy) {
|
|
return paginationStore.getPageInfo(props.tableName);
|
|
}
|
|
|
|
// For non-lazy tables, calculate based on current page state and filtered data
|
|
const filteredTotal = getFilteredDataLength.value;
|
|
const rows = currentRows.value;
|
|
const currentFirst = currentPageState.value.first;
|
|
const start = filteredTotal > 0 ? currentFirst + 1 : 0;
|
|
const end = Math.min(filteredTotal, currentFirst + rows);
|
|
|
|
return {
|
|
start,
|
|
end,
|
|
total: filteredTotal,
|
|
};
|
|
};
|
|
|
|
// Handle pagination events
|
|
const handlePage = (event) => {
|
|
console.log("Page event:", event);
|
|
|
|
// Update current page state for both lazy and non-lazy tables
|
|
currentPageState.value = {
|
|
page: event.page,
|
|
first: event.first,
|
|
};
|
|
|
|
if (props.lazy) {
|
|
paginationStore.updateTablePagination(props.tableName, {
|
|
page: event.page,
|
|
first: event.first,
|
|
rows: event.rows,
|
|
});
|
|
triggerLazyLoad();
|
|
}
|
|
emit("page-change", event);
|
|
};
|
|
|
|
// Handle sorting events
|
|
// Handle filter events
|
|
const handleFilter = (event) => {
|
|
console.log("Filter event:", event);
|
|
|
|
// Reset to first page when filters change (for both lazy and non-lazy)
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
selectedPageJump.value = ""; // Reset page jump dropdown
|
|
|
|
if (props.lazy) {
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
triggerLazyLoad();
|
|
}
|
|
emit("filter-change", event);
|
|
};
|
|
|
|
// Trigger lazy load event
|
|
const triggerLazyLoad = (event = {}) => {
|
|
if (props.lazy) {
|
|
const paginationParams = paginationStore.getPaginationParams(props.tableName);
|
|
const filters = filtersStore.getTableFilters(props.tableName);
|
|
|
|
// Use sort information from the event if provided (from DataTable sort event)
|
|
// Otherwise fall back to stored sorting state
|
|
const storedSorting = filtersStore.getTableSorting(props.tableName);
|
|
|
|
const lazyEvent = {
|
|
first: event.first !== undefined ? event.first : paginationParams.offset,
|
|
rows: event.rows !== undefined ? event.rows : paginationParams.limit,
|
|
page: event.page !== undefined ? event.page : paginationParams.page,
|
|
sortField:
|
|
event.sortField !== undefined
|
|
? event.sortField
|
|
: storedSorting.field || paginationParams.sortField,
|
|
sortOrder:
|
|
event.sortOrder !== undefined
|
|
? event.sortOrder
|
|
: storedSorting.order || paginationParams.sortOrder,
|
|
filters: { ...filters, ...(event.filters || {}) },
|
|
};
|
|
|
|
// If this is a sort event, update the stored sorting state
|
|
if (event.sortField !== undefined || event.sortOrder !== undefined) {
|
|
filtersStore.updateTableSorting(
|
|
props.tableName,
|
|
lazyEvent.sortField,
|
|
lazyEvent.sortOrder,
|
|
);
|
|
|
|
// Reset to first page when sorting changes
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
selectedPageJump.value = ""; // Reset page jump dropdown
|
|
paginationStore.clearTableCache(props.tableName);
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
lazyEvent.page = 0;
|
|
lazyEvent.first = 0;
|
|
}
|
|
|
|
console.log("Triggering lazy load with:", lazyEvent);
|
|
emit("lazy-load", lazyEvent);
|
|
}
|
|
};
|
|
|
|
// Trigger data refresh for non-lazy tables when filters change
|
|
const triggerDataRefresh = () => {
|
|
const filters = filtersStore.getTableFilters(props.tableName);
|
|
const sorting = filtersStore.getTableSorting(props.tableName);
|
|
const refreshEvent = {
|
|
filters: filters,
|
|
sortField: sorting.field,
|
|
sortOrder: sorting.order,
|
|
page: currentPageState.value.page,
|
|
first: currentPageState.value.first,
|
|
rows: currentRows.value,
|
|
};
|
|
|
|
console.log("Triggering data refresh with:", refreshEvent);
|
|
emit("data-refresh", refreshEvent);
|
|
};
|
|
|
|
const handleFilterInput = (fieldName, value, filterCallback) => {
|
|
// Update the filter store
|
|
filtersStore.updateTableFilter(props.tableName, fieldName, value, FilterMatchMode.CONTAINS);
|
|
|
|
// Call the PrimeVue callback to update the filter
|
|
if (filterCallback) {
|
|
filterCallback();
|
|
}
|
|
|
|
// Reset to first page when filters change (for both lazy and non-lazy)
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
|
|
// For lazy tables, reset pagination and trigger lazy load
|
|
if (props.lazy) {
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
triggerLazyLoad();
|
|
} else {
|
|
// For non-lazy tables, also trigger a data refresh when individual filters change
|
|
triggerDataRefresh();
|
|
}
|
|
};
|
|
|
|
// Action handler methods
|
|
const handleActionClick = (action, rowData = null) => {
|
|
try {
|
|
if (typeof action.action === "function") {
|
|
if (rowData) {
|
|
// Row-specific action - pass row data
|
|
action.action(rowData);
|
|
} else {
|
|
// Global action - no row data needed
|
|
action.action();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error executing action:", error);
|
|
}
|
|
};
|
|
|
|
const handleGlobalAction = (action) => {
|
|
handleActionClick(action);
|
|
};
|
|
|
|
const handleRowAction = (action, rowData) => {
|
|
handleActionClick(action, rowData);
|
|
};
|
|
|
|
const handleSingleSelectionAction = (action) => {
|
|
if (hasExactlyOneRowSelected.value) {
|
|
const selectedRow = selectedRows.value[0];
|
|
handleActionClick(action, selectedRow);
|
|
}
|
|
};
|
|
|
|
// Unified handler for top-level actions (global and single selection)
|
|
const handleTopAction = (action) => {
|
|
if (action.requiresSelection) {
|
|
handleSingleSelectionAction(action);
|
|
} else {
|
|
handleGlobalAction(action);
|
|
}
|
|
};
|
|
|
|
// Helper methods for action styling and behavior
|
|
const getActionSeverity = (action) => {
|
|
if (action.requiresSelection) {
|
|
return action.style || 'info';
|
|
}
|
|
return action.style || 'primary';
|
|
};
|
|
|
|
const getActionDisabled = (action) => {
|
|
if (loading.value) return true;
|
|
if (action.requiresSelection) {
|
|
return !hasExactlyOneRowSelected.value;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const getActionClasses = (action) => {
|
|
const classes = ['dt-action-btn'];
|
|
if (action.requiresSelection) {
|
|
classes.push('dt-action-btn-selection');
|
|
if (!hasExactlyOneRowSelected.value) {
|
|
classes.push('dt-action-btn-disabled');
|
|
}
|
|
} else {
|
|
classes.push('dt-action-btn-global');
|
|
}
|
|
if (action.layout?.variant) {
|
|
classes.push(`dt-action-btn-${action.layout.variant}`);
|
|
}
|
|
return classes;
|
|
};
|
|
|
|
const toggleRowDropdown = (event, rowData) => {
|
|
// Placeholder for dropdown menu functionality
|
|
// Could be implemented with PrimeVue OverlayPanel or Menu component
|
|
console.log('Toggle dropdown for row:', rowData);
|
|
};
|
|
|
|
const handleBulkAction = (action, selectedRows) => {
|
|
try {
|
|
if (typeof action.action === "function") {
|
|
// Bulk action - pass array of selected row data
|
|
action.action(selectedRows);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error executing bulk action:", error);
|
|
}
|
|
};
|
|
|
|
const getBadgeColor = (status) => {
|
|
switch (status?.toLowerCase()) {
|
|
case "completed":
|
|
case "open":
|
|
case "active":
|
|
return "success";
|
|
case "in progress":
|
|
case "pending":
|
|
return "warning";
|
|
case "not started":
|
|
case "closed":
|
|
case "cancelled":
|
|
return "danger";
|
|
default:
|
|
return "info";
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateValue) => {
|
|
if (!dateValue) return "";
|
|
|
|
try {
|
|
// Handle different date formats
|
|
let date;
|
|
if (typeof dateValue === "string") {
|
|
date = new Date(dateValue);
|
|
} else if (dateValue instanceof Date) {
|
|
date = dateValue;
|
|
} else {
|
|
return "";
|
|
}
|
|
|
|
// Check if date is valid
|
|
if (isNaN(date.getTime())) {
|
|
return dateValue; // Return original value if can't parse
|
|
}
|
|
|
|
// Format as MM/DD/YYYY
|
|
return date.toLocaleDateString("en-US");
|
|
} catch (error) {
|
|
console.error("Error formatting date:", error);
|
|
return dateValue;
|
|
}
|
|
};
|
|
console.log("DEBUG: - DataTable props.columns", props.columns);
|
|
console.log("DEBUG: - DataTable props.data", props.data);
|
|
|
|
// Expose control methods for parent components
|
|
defineExpose({
|
|
// Loading controls
|
|
startLoading: (message) => loadingStore.setComponentLoading(props.tableName, true, message),
|
|
stopLoading: () => loadingStore.setComponentLoading(props.tableName, false),
|
|
isLoading: () => loading.value,
|
|
|
|
// Pagination controls (for lazy tables)
|
|
goToPage: (page) => {
|
|
if (props.lazy) {
|
|
paginationStore.setPage(props.tableName, page);
|
|
triggerLazyLoad();
|
|
}
|
|
},
|
|
nextPage: () => {
|
|
if (props.lazy) {
|
|
paginationStore.nextPage(props.tableName);
|
|
triggerLazyLoad();
|
|
}
|
|
},
|
|
previousPage: () => {
|
|
if (props.lazy) {
|
|
paginationStore.previousPage(props.tableName);
|
|
triggerLazyLoad();
|
|
}
|
|
},
|
|
refresh: () => {
|
|
if (props.lazy) {
|
|
triggerLazyLoad();
|
|
} else {
|
|
triggerDataRefresh();
|
|
}
|
|
},
|
|
|
|
// Get current state
|
|
getCurrentPage: () => paginationStore.getTablePagination(props.tableName).page,
|
|
getTotalPages: () => paginationStore.getTotalPages(props.tableName),
|
|
getFilters: () => filtersStore.getTableFilters(props.tableName),
|
|
getPaginationInfo: () => paginationStore.getPageInfo(props.tableName),
|
|
});
|
|
</script>
|
|
<style lang="css">
|
|
/* Modern DataTable Styling */
|
|
|
|
/* Filter Panel Styles */
|
|
.dt-filter-panel {
|
|
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.dt-filter-panel:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.dt-filter-header {
|
|
padding: 1rem 1.25rem 0.5rem;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.dt-filter-title {
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
color: #374151;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dt-filter-title i {
|
|
color: #6366f1;
|
|
}
|
|
|
|
.dt-filter-content {
|
|
padding: 1.25rem;
|
|
}
|
|
|
|
.dt-filter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.dt-filter-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
.dt-filter-label {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.dt-filter-input {
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 8px;
|
|
padding: 0.625rem;
|
|
font-size: 0.875rem;
|
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
.dt-filter-input:focus {
|
|
border-color: #6366f1;
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
}
|
|
|
|
.dt-filter-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.dt-btn-primary, .dt-btn-secondary {
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dt-btn-primary:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.dt-filter-status {
|
|
margin-top: 0.75rem;
|
|
padding: 0.75rem;
|
|
background: #f3f4f6;
|
|
border-radius: 8px;
|
|
font-size: 0.8125rem;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* Pagination Panel Styles */
|
|
.dt-pagination-panel {
|
|
background: linear-gradient(135deg, #ffffff 0%, #f9fafb 100%);
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.dt-pagination-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.875rem 1.25rem;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.dt-pagination-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
color: #6b7280;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.dt-pagination-info i {
|
|
color: #6366f1;
|
|
}
|
|
|
|
.dt-pagination-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dt-pagination-label {
|
|
font-size: 0.875rem;
|
|
color: #374151;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dt-pagination-select {
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 6px;
|
|
padding: 0.375rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
background: white;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.dt-pagination-select:focus {
|
|
border-color: #6366f1;
|
|
outline: none;
|
|
}
|
|
|
|
/* Bulk Actions Panel Styles */
|
|
.dt-bulk-actions-panel {
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fcd34d 100%);
|
|
border: 1px solid #f59e0b;
|
|
border-radius: 10px;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 2px 4px rgba(245, 158, 11, 0.1);
|
|
animation: slideInBulk 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideInBulk {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px) scale(0.98);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
.dt-bulk-actions-content {
|
|
padding: 1rem 1.25rem;
|
|
}
|
|
|
|
.dt-bulk-actions-groups {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.dt-bulk-btn {
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dt-bulk-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.dt-bulk-actions-status {
|
|
padding: 0.5rem 0.75rem;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 6px;
|
|
font-size: 0.8125rem;
|
|
color: #92400e;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Global Actions Panel Styles */
|
|
.dt-global-actions-panel {
|
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 10px;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
border-left: 4px solid #6366f1;
|
|
}
|
|
|
|
.dt-global-actions-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.25rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.dt-action-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.dt-action-group-left {
|
|
flex: 1;
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.dt-action-group-center {
|
|
justify-content: center;
|
|
}
|
|
|
|
.dt-action-group-right {
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.dt-action-btn {
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.dt-action-btn:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.dt-action-btn-global {
|
|
/* Global action styling */
|
|
position: relative;
|
|
}
|
|
|
|
.dt-action-btn-selection {
|
|
/* Selection action styling */
|
|
position: relative;
|
|
}
|
|
|
|
.dt-action-btn-disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.dt-action-btn-outlined {
|
|
background: transparent;
|
|
border: 1.5px solid currentColor;
|
|
}
|
|
|
|
.dt-action-btn-filled {
|
|
border: none;
|
|
color: white;
|
|
}
|
|
|
|
.dt-global-actions-status {
|
|
padding: 0.625rem 1.25rem;
|
|
background: #f3f4f6;
|
|
border-top: 1px solid #e5e7eb;
|
|
font-size: 0.8125rem;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* Row Actions Styles */
|
|
.dt-actions-column {
|
|
min-width: 160px;
|
|
}
|
|
|
|
.dt-row-actions {
|
|
display: flex;
|
|
gap: 0.375rem;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.dt-row-actions-primary {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.dt-row-actions-secondary {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.dt-row-actions-dropdown {
|
|
display: flex;
|
|
}
|
|
|
|
.dt-row-btn {
|
|
border-radius: 6px;
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dt-row-btn:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.dt-row-btn-secondary {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.dt-row-btn-secondary:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.dt-row-btn-dropdown {
|
|
padding: 0.25rem;
|
|
min-width: auto;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
}
|
|
|
|
.dt-row-btn-icon-only {
|
|
min-width: auto;
|
|
width: 2.25rem;
|
|
padding: 0.375rem;
|
|
}
|
|
|
|
.dt-row-btn-compact {
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.dt-filter-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.dt-pagination-content {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
text-align: center;
|
|
}
|
|
|
|
.dt-global-actions-content {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.dt-action-group {
|
|
justify-content: center;
|
|
}
|
|
|
|
.dt-bulk-actions-groups {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
}
|
|
|
|
/* Dark mode support (optional) */
|
|
@media (prefers-color-scheme: dark) {
|
|
.dt-filter-panel,
|
|
.dt-pagination-panel,
|
|
.dt-global-actions-panel {
|
|
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
|
border-color: #374151;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.dt-filter-title,
|
|
.dt-filter-label,
|
|
.dt-pagination-label {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.dt-filter-input,
|
|
.dt-pagination-select {
|
|
background: #374151;
|
|
border-color: #4b5563;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.dt-filter-status,
|
|
.dt-global-actions-status {
|
|
background: #374151;
|
|
color: #d1d5db;
|
|
}
|
|
}
|
|
|
|
/* Animation utilities */
|
|
.dt-fade-in {
|
|
animation: fadeIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.dt-slide-in {
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
</style>
|