1602 lines
39 KiB
Vue
1602 lines
39 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="handleSort"
|
|
@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 === 'status-button'" #body="slotProps">
|
|
<Button
|
|
:label="slotProps.data[col.fieldName]"
|
|
:severity="getBadgeColor(slotProps.data[col.fieldName])"
|
|
size="small"
|
|
:variant="col.buttonVariant || 'filled'"
|
|
@click="handleStatusButtonClick(col, slotProps.data)"
|
|
:disabled="
|
|
loading ||
|
|
(col.disableCondition &&
|
|
col.disableCondition(slotProps.data[col.fieldName]))
|
|
"
|
|
class="status-button"
|
|
/>
|
|
</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
|
|
const handleSort = (event) => {
|
|
console.log("Sort event received:", {
|
|
sortField: event.sortField,
|
|
sortOrder: event.sortOrder,
|
|
type: typeof event.sortOrder,
|
|
event: event,
|
|
});
|
|
|
|
try {
|
|
// Handle removable sort - when sortOrder is null/undefined, clear sorting
|
|
if (event.sortOrder === null || event.sortOrder === undefined) {
|
|
console.log("Clearing sort due to removable sort");
|
|
filtersStore.updateTableSorting(props.tableName, null, null);
|
|
} else {
|
|
// Update the stored sorting state immediately
|
|
filtersStore.updateTableSorting(props.tableName, event.sortField, event.sortOrder);
|
|
}
|
|
|
|
// Reset to first page when sorting changes
|
|
currentPageState.value = { page: 0, first: 0 };
|
|
selectedPageJump.value = ""; // Reset page jump dropdown
|
|
|
|
if (props.lazy) {
|
|
paginationStore.clearTableCache(props.tableName);
|
|
paginationStore.resetToFirstPage(props.tableName);
|
|
|
|
// Trigger lazy load with sort information (or no sort if cleared)
|
|
triggerLazyLoad({
|
|
sortField: event.sortField || null,
|
|
sortOrder: event.sortOrder || null,
|
|
page: 0,
|
|
first: 0,
|
|
});
|
|
} else {
|
|
// For non-lazy tables, trigger data refresh
|
|
triggerDataRefresh();
|
|
}
|
|
|
|
emit("sort-change", event);
|
|
} catch (error) {
|
|
console.error("Error in handleSort:", error);
|
|
console.error("Event that caused error:", event);
|
|
console.error("Stack trace:", error.stack);
|
|
}
|
|
};
|
|
|
|
// 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);
|
|
|
|
// Get current sorting state from store
|
|
const primarySortField = filtersStore.getPrimarySortField(props.tableName);
|
|
const primarySortOrder = filtersStore.getPrimarySortOrder(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
|
|
: primarySortField || paginationParams.sortField,
|
|
sortOrder:
|
|
event.sortOrder !== undefined
|
|
? event.sortOrder
|
|
: primarySortOrder || paginationParams.sortOrder,
|
|
filters: { ...filters, ...(event.filters || {}) },
|
|
};
|
|
|
|
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 primarySortField = filtersStore.getPrimarySortField(props.tableName);
|
|
const primarySortOrder = filtersStore.getPrimarySortOrder(props.tableName);
|
|
const refreshEvent = {
|
|
filters: filters,
|
|
sortField: primarySortField,
|
|
sortOrder: primarySortOrder,
|
|
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);
|
|
}
|
|
};
|
|
|
|
// Handle status button clicks
|
|
const handleStatusButtonClick = (column, rowData) => {
|
|
try {
|
|
if (column.onStatusClick && typeof column.onStatusClick === "function") {
|
|
column.onStatusClick(rowData[column.fieldName], rowData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error executing status button click:", error);
|
|
}
|
|
};
|
|
|
|
const getBadgeColor = (status) => {
|
|
switch (status?.toLowerCase()) {
|
|
case "completed":
|
|
case "open":
|
|
case "active":
|
|
return "success";
|
|
case "in progress":
|
|
case "pending":
|
|
return "warn";
|
|
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: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.dt-filter-header {
|
|
padding: 0.5rem 0.75rem 0.25rem;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.dt-filter-title {
|
|
margin: 0;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #374151;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
.dt-filter-title i {
|
|
color: #6366f1;
|
|
}
|
|
|
|
.dt-filter-content {
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.dt-filter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.dt-filter-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.dt-filter-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.dt-filter-input {
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
padding: 0.375rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
transition: border-color 0.2s ease;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.dt-filter-input:focus {
|
|
border-color: #6366f1;
|
|
outline: none;
|
|
}
|
|
|
|
.dt-filter-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.dt-btn-primary,
|
|
.dt-btn-secondary {
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
font-size: 0.75rem;
|
|
padding: 0.375rem 0.75rem;
|
|
}
|
|
|
|
.dt-filter-status {
|
|
margin-top: 0.5rem;
|
|
padding: 0.375rem 0.5rem;
|
|
background: #f9fafb;
|
|
border-radius: 4px;
|
|
font-size: 0.7rem;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
/* Pagination Panel Styles */
|
|
.dt-pagination-panel {
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
margin-bottom: 0.5rem;
|
|
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.5rem 0.75rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dt-pagination-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
color: #6b7280;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.dt-pagination-info i {
|
|
color: #6366f1;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.dt-pagination-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
.dt-pagination-label {
|
|
font-size: 0.75rem;
|
|
color: #374151;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dt-pagination-select {
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
padding: 0.25rem 0.5rem;
|
|
font-size: 0.75rem;
|
|
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: #fef7ed;
|
|
border: 1px solid #fed7aa;
|
|
border-radius: 6px;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 1px 2px rgba(251, 146, 60, 0.1);
|
|
animation: slideInBulk 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes slideInBulk {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-5px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.dt-bulk-actions-content {
|
|
padding: 0.5rem 0.75rem;
|
|
}
|
|
|
|
.dt-bulk-actions-groups {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.dt-bulk-btn {
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
font-size: 0.75rem;
|
|
padding: 0.375rem 0.75rem;
|
|
}
|
|
|
|
.dt-bulk-actions-status {
|
|
padding: 0.25rem 0.5rem;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 4px;
|
|
font-size: 0.7rem;
|
|
color: #ea580c;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Global Actions Panel Styles */
|
|
.dt-global-actions-panel {
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 6px;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
border-left: 3px solid #6366f1;
|
|
}
|
|
|
|
.dt-global-actions-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem 0.75rem;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.dt-action-group {
|
|
display: flex;
|
|
gap: 0.375rem;
|
|
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: 4px;
|
|
font-weight: 500;
|
|
font-size: 0.75rem;
|
|
padding: 0.375rem 0.75rem;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.dt-action-btn:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px 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.375rem 0.75rem;
|
|
background: #f9fafb;
|
|
border-top: 1px solid #f3f4f6;
|
|
font-size: 0.7rem;
|
|
color: #6b7280;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
/* Animation utilities */
|
|
.dt-fade-in {
|
|
animation: fadeIn 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(5px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.dt-slide-in {
|
|
animation: slideIn 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-5px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
/* Status Button Styles */
|
|
.status-button {
|
|
font-weight: 500;
|
|
font-size: 0.8rem;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
min-width: 100px;
|
|
text-align: center;
|
|
}
|
|
|
|
.status-button:not(:disabled):hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.status-button:disabled {
|
|
cursor: default;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.status-button:disabled:hover {
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
</style>
|