<template>
<Header :name="$route.name" :title="title ?? $t($route.name)" v-if="!parent" :backButton="backButton" />
<div ref="parentElement" @keydown="handleKeyDown($event, true)">
<!-- Overlays for inline editing -->
<q-input v-if="overlays.overlayInput" class="input-box" outlined ref="overlayInput" v-model="editedItem"
type="text" @blur="closeOverlay" @keydown="handleKeyDown($event, false)" :style="overlayStyle" />
<JsonEditor v-if="overlays.overlayJson" ref="overlayJson" v-model="editedItem" @blur="closeOverlay"
@keydown="handleKeyDown($event, false)" :style="overlayStyle" />
<q-input v-if="overlays.overlayText" class="textarea" type="textarea" rows=8 ref="overlayText"
v-model="editedItem" @blur="closeOverlay" @keydown="handleKeyDown($event, false)" :style="overlayStyle"
:iconPicker="false" />
<autocomplete v-if="overlays.overlaySelect" ref="overlaySelect" class="input-box" v-model="editedItem"
:options="overlaySelectOptions.options" :option-label="overlaySelectOptions.optionLabel"
:option-value="overlaySelectOptions.optionValue" @blur="closeOverlay"
@update:model-value="editedItemChanged" emit-value map-options :style="overlayStyle" />
<icon-picker v-if="overlays.overlayIcon" ref="overlayIcon" v-model="editedItem" @blur="closeOverlay"
@update:model-value="editedItemChanged" :style="overlayStyle" />
<q-dialog v-model="showDetails">
<table-details v-if="showDetails && details" :details="details" :parent="current"
@close="showDetails = false" />
</q-dialog>
<div v-if="contextValuesLocal" class="row">
<Autocomplete v-for="cv of contextValuesLocal" :key="cv.name" v-model="cv.value" :label="cv.label"
:option-label="cv.optionLabel" :option-value="cv.optionValue" dense :options="cv.options"
@update:model-value="reload" :style="{ width: cv.width ?? '100px' }" map-options emit-value />
</div>
<!-- Toolbar before the table -->
<div class="header-container toolbar">
<div class="left-item">
<!-- <q-btn v-if="backButton" dense flat icon="arrow_back" @click="$router.go(-1)">
<q-tooltip>{{ $t("Back") }}</q-tooltip>
</q-btn> -->
<q-btn v-for="action of tableActions " dense flat :icon="action.icon"
:color="action.iconColor ?? 'primary'" no-caps @click="runTableAction(action)">
<div v-if="action.label" v-html="action.label"></div>
<q-tooltip v-if="action.tooltip">{{ $t(action.tooltip) }}</q-tooltip>
</q-btn>
<q-btn dense flat icon="filter_alt" color="primary" @click="showFilter = true">
<q-tooltip>{{ $t("Filter form") }}</q-tooltip>
</q-btn>
<q-btn v-if="filterSet" dense flat icon="filter_alt_off" color="primary" @click="clearFilter">
<q-tooltip>{{ $t("Clear filter") }}</q-tooltip></q-btn>
<q-btn dense flat icon="refresh" color="primary" @click="reload">
<q-tooltip>{{ $t("Reload data") }}</q-tooltip></q-btn>
<q-btn dense flat :icon="grid ? 'view_list' : 'view_module'" color="primary" @click="grid = !grid">
<q-tooltip>{{ $t("Toggle grid view") }}</q-tooltip></q-btn>
<q-btn v-if="standardEditing" class="text-bold" dense flat icon="add" color="primary" @click="addRow()">
<q-tooltip>{{ $t("Add new row") }}</q-tooltip></q-btn>
<q-btn v-if="$store.formChanged" dense flat icon="save" color="positive" @click="saveRows"><q-tooltip>{{
$t("Save changes") }}</q-tooltip></q-btn>
<q-btn v-if="$store.formChanged" dense flat icon="undo" color="negative"
@click="undoChanges"><q-tooltip>{{
$t("Undo changes") }}</q-tooltip></q-btn>
</div>
<div class="right-item">
<span v-if="nRows > 0">
{{ $t("Records") }} {{ from }}-{{ to }} {{ $t("of") }} {{ nRows }}
</span>
<span v-else>{{ $t("No data available") }}</span>
<q-btn v-if="from > 1" dense flat icon="first_page" color="primary" @click="$refs.table.scrollTo(0)">
<q-tooltip>{{ $t("First record") }}</q-tooltip>
</q-btn>
<q-btn v-if="to < nRows" dense flat icon="last_page" color="primary"
@click="$refs.table.scrollTo(nRows - 1)">
<q-tooltip>{{ $t("Last record") }}</q-tooltip>
</q-btn>
<q-btn v-if="nRows > 0" dense flat icon="download" color="primary" @click="exportTable">
<q-tooltip>{{ $t("Download table") }}</q-tooltip>
</q-btn>
</div>
</div>
<!-- The table -->
<q-table v-if="loaded" ref="table" class="my-sticky-header-table" @virtual-scroll="scroll"
:table-style="tableStyle" dense flat bordered :rows="filterSet ? rowsFiltered : rows" :columns="columns"
:visible-columns="visibleColumns" :row-key="key" virtual-scroll virtual-scroll-slice-size=1
v-model:pagination="pagination" :rows-per-page-options="[0]" :selection="selection"
v-model:selected="selectedRows" hide-bottom :selected-rows-label="selectedRowsLabel" :grid="grid">
<!-- Header -->
<template v-slot:header="props">
<!-- column names -->
<q-tr :props="props">
<q-th v-if="selection != 'none'" style="width:'15px'">
<q-checkbox dense v-if="selection == 'multiple'" v-model="props.selected" />
</q-th>
<q-th v-if="standardEditing || rowActions || isAdmin">
</q-th>
<q-th v-for=" col in props.cols " :key="col.name" :props="props">
<span class="text-bold" v-html="col.label" :style="{ display: 'inline-block' }"></span>
</q-th>
</q-tr>
<!-- filter row -->
<q-tr :props="props">
<q-td v-if="selection != 'none'" style="width:'15px'" />
<q-td v-if="standardEditing || rowActions || isAdmin" />
<q-td v-for=" col in props.cols " :key="col.name" :props="props" class="q-pa-none q-ma-none">
<q-checkbox v-if="col.type == 'boolean'" v-model="filter[col.name]" dense
toggle-indeterminate />
<q-input v-else type="search" placeholder="Search" placeholder-color="lightgray" clearable dense
v-model="filter[col.name]" class="q-my-none q-py-none">
<template v-slot:after>
<q-icon size="xs" name="search"></q-icon>
</template>
</q-input>
</q-td>
</q-tr>
</template>
<!-- grid rows -->
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3 grid-style-transition">
<!-- :style="props.selected ? 'transform: scale(0.95);' : ''"> -->
<q-card flat>
<!-- :class="props.selected ? 'bg-grey-2' : ''"> -->
<q-card-section v-if="selection != 'none' || standardEditing || rowActions">
<q-checkbox v-if="selection != 'none'" dense v-model="props.selected"
:label="props.row.name"></q-checkbox>
<table-row-toolbar :parent="this" :props="props" :columns="columns"
:rowActions="rowActions" />
</q-card-section>
<q-separator></q-separator>
<q-list dense>
<q-item v-for="col in props.cols" :key="col.name">
<span class="label">{{ col.label }}</span> <q-space />
<!-- <span v-html="col.value"></span> -->
<span @click="showOverlay(props.key + '-' + col.index, col, props)">
<q-checkbox dense v-if="col.type == 'boolean'" v-model="props.row[col.index]"
:disable="!standardEditing || col.disabled || noInlineEditing"
@update:model-value="changedRows[props.row[0]] = [...props.row]; $store.formChanged = true;" />
<span v-else :ref="props.key + '-' + col.index"
v-html="col.password ? '********' : col.value" class="q-pa-none q-ma-none"
:style="{ display: 'inline-block', overflow: 'hidden', maxWidth: col.width + '!important', maxHeight: col.height + '!important', verticalAlign: 'middle' }" />
</span>
</q-item>
</q-list>
</q-card>
</div>
</template>
<!-- normal rows -->
<template v-slot:body="props">
<q-tr :props="props" :class="{ 'background': (props.pageIndex % 2 == 0) }">
<q-td v-if="selection != 'none'">
<q-checkbox dense v-model="props.selected" />
</q-td>
<q-td v-if="standardEditing || rowActions || isAdmin">
<table-row-toolbar :parent="this" :props="props" :columns="columns" :rowActions="rowActions" />
<!-- <q-btn class="nompy" :size="btnSize" v-if="standardEditing" dense flat icon="edit"
color="primary" @click="editRow(props.row)">
<q-tooltip>{{ $t("Edit row") }}</q-tooltip>
</q-btn>
<q-btn class="nompy" :size="btnSize" v-if="standardEditing" dense flat icon="delete"
color="negative" @click="deleteRow(props.row)">
<q-tooltip>{{ $t("Delete row") }}</q-tooltip>
</q-btn>
<q-btn class="nompy" :size="btnSize" v-if="details" dense flat icon="expand_more"
color="primary" @click="openDetails(props.row)"><q-tooltip>{{ $t("Detail tables")
}}</q-tooltip></q-btn>
<q-btn class="nompy" :size="btnSize" v-for=" action of rowActions " dense flat
:icon="action.icon" :color="action.iconColor ?? 'primary'"
@click="runRowAction(action, props.row)">
<q-tooltip v-if="action.tooltip">{{ $t(action.tooltip) }}</q-tooltip>
</q-btn>
<q-btn class="nompy" :size="btnSize" v-if="isAdmin" dense flat icon="info" color="primary"
@click="showRowInfo(props.row)">
<q-tooltip>{{ $t("Row info") }}</q-tooltip></q-btn> -->
</q-td>
<q-td v-for=" col in props.cols " :key="col.name" :props="props"
:style="{ maxWidth: col.width + ' !important', verticalAlign: 'middle' }"
@click="showOverlay(props.key + '-' + col.index, col, props)" class="q-pa-none q-ma-none">
<!-- <div class="text-left q-gutter-md" :style="{ maxWidth: col.width }"> -->
<q-checkbox dense v-if="col.type == 'boolean'" v-model="props.row[col.index]"
:disable="!standardEditing || col.disabled || noInlineEditing"
@update:model-value="changedRows[props.row[0]] = [...props.row]; $store.formChanged = true;" />
<span v-else :ref="props.key + '-' + col.index" v-html="col.password ? '********' : col.value"
class="q-pa-none q-ma-none"
:style="{ display: 'inline-block', overflow: 'hidden', maxWidth: col.width + '!important', maxHeight: col.height + '!important', verticalAlign: 'middle' }" />
</q-td>
</q-tr>
</template>
</q-table>
<!-- The dialog for filtering the table -->
<q-dialog v-model="showFilter">
<q-card style="overflow: hidden">
<q-toolbar dark dense flat>
<q-toolbar-title>{{ $t("Filter") }}</q-toolbar-title>
<q-btn round margin="xs" size="xs" padding="xs" icon="close" @click="showFilter = false"></q-btn>
</q-toolbar>
<q-card-section :style="filterStyle">
<div class="row items-center" v-for="col in columns" :key="col.name">
<q-input v-model="filter[col.name]" :label="col.label" dense style="width:120px">
<template v-slot:label>
<label for="my-input" v-html="col.label"></label>
</template>
</q-input>
<span v-if="col.compare == 'interval'"> _ </span>
<q-input v-if="col.compare == 'interval'" label=" " v-model="filter2[col.name]" dense
style="width:120px" />
</div>
</q-card-section>
</q-card>
</q-dialog>
<table-row-editor v-if="inEdit" :parent=this @save="saveRow" @cancel="inEdit = false" :editStyle="editStyle" />
</div>
</template>
<script>
/**
* Generic table component
*
* @component
* @name Table
* @example
* <Table />
*/
import { TableEditMixin } from '../mixins/table-edit.js';
import { TableMixin } from '../mixins/table.js';
import { TableCustomMixin } from '../mixins/table-custom.js';
import Autocomplete from './autocomplete.vue';
import JsonEditor from "./json-editor.vue";
import IconPicker from "./icon-picker.vue";
import Header from './header.vue';
import TableRowEditor from './table-row-editor.vue';
import TableRowToolbar from './table-row-toolbar.vue';
import { defineAsyncComponent } from 'vue';
export default {
name: "Table",
mixins: [TableMixin, TableEditMixin, TableCustomMixin],
components: { Header, TableRowEditor, TableRowToolbar, Autocomplete, JsonEditor, IconPicker, TableDetails: defineAsyncComponent(() => import('./table-details.vue')) },
beforeRouteEnter(to, from, next) {
next(vm => { vm.init(to.name); });
},
beforeRouteUpdate(to, from, next) {
this.init(to.name);
next();
},
watch: {
filter: {
handler(val) {
this.filterSet = this.columns.find(col => this.filter[col.name]?.toString().length > 0 || this.filter2[col.name]?.toString().length > 0);
},
deep: true
}
},
data: () => ({
}),
computed: {
btnSize() {
return this.$q.screen.gt.sm ? 'sm' : 'md';
},
nRows() {
if (!this.rows) return 0;
return this.filterSet ? this.rowsFiltered.length : this.rows.length;
},
editColumns() {
let ec = [...this.columns];
this.swapIdAndValColumns(ec);
return ec.filter(col => this.showColInEdit(col));
},
editStyle() {
return {
maxHeight: (this.$q.screen.height - 200) + 'px', overflow: 'auto', minWidth: "600px"
};
},
filterStyle() {
return { maxHeight: (this.$q.screen.height - 200) + 'px', overflow: 'auto' };
},
tableStyle() {
return `height: ${this.$q.screen.height - (this.contextValuesLocal ? 150 : 110)}px; width: ${this.$q.screen.width - 5 - (this.$store.drawer ? this.$store.drawerWidth : 0)}px;`;
},
rowsFiltered() {
return this.rows.filter(row => {
for (let col of this.columns.filter(col => !(this.filter[col.name] === '') || !(this.filter2[col.name] === ''))) {
if (col.compare == 'interval') {
if (this.filter[col.name] != '' && row[col.index] < this.filter[col.name]) return false;
if (this.filter2[col.name] != '' && row[col.index] > this.filter2[col.name]) return false;
} else if (col.type == 'boolean') {
if (this.filter[col.name] == null) return true;
if (row[col.index] != this.filter[col.name]) return false;
} else {
if (!row[col.index]?.toString().toLowerCase().includes(this.filter[col.name].toLowerCase())) {
return false;
}
}
}
return true;
});
},
},
methods: {
/**
* Clear the filter
*/
clearFilter() {
for (let col of this.columns) {
this.filter[col.name] = ''; this.filter2[col.name] = '';
}
},
/**
* Scroll event
* @param {*} details
*/
scroll(details) {
this.from = details.from + 1;
this.to = details.to + 1;
this.closeAllOverlays();
},
/**
* Text displaying the number of selected rows
*/
selectedRowsLabel() {
this.selectedRows.length + ' ' + this.$t('selected');
},
/**
* Opens the details view for a given row.
*
* @param {Object} row - The row object containing the data for the selected row.
*/
openDetails(row) {
let parentName = "";
if (this.parentName) {
if (!this.frugal) {
parentName = row[this.parentName];
} else {
parentName = row[this.columns.find(x => x.name == this.parentName).index];
}
}
this.current = { key: this.parentKey, value: row[0], name: parentName };
this.showDetails = true;
},
/**
* Handles the keydown event.
*
* @param {Event} event - The keydown event object.
* @param {boolean} isParent - Indicates whether the event is triggered by a parent component.
*/
handleKeyDown(event, isParent) {
if (event.ctrlKey && event.key === 'f') {
this.showFilter = true;
event.preventDefault();
this.closeOverlay();
} else if (event.altKey && event.key === 'a') {
this.addRow();
event.preventDefault();
this.closeOverlay();
} else if (event.ctrlKey && event.key === 's') {
event.preventDefault();
event.stopPropagation();
this.saveRows();
this.closeOverlay();
} else if (event.ctrlKey && event.key === 'u') {
this.undoChanges();
event.preventDefault();
this.closeOverlay();
} else if (!isParent) {
this.editedItemChanged();
}
},
}
}
</script>
<style scoped>
.label {
color: gray;
}
.toolbar {
top: 76px;
width: 100%;
z-index: 100;
}
.my-sticky-header-table {
background-color: white;
}
.my-sticky-header-table thead tr:first-child th {
/* background-color: #1976D2; */
background-color: var(--q-primary);
color: white;
}
.my-sticky-header-table {
top: 0px !important;
}
.my-sticky-header-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0px;
}
:deep(.input-box .q-field__control),
:deep(.input-box .q-field__marginal) {
height: 32px;
padding: 0px 2px 2px 2px;
border-radius: 0px;
background-color: white;
}
:deep(.text-box .q-field__control),
:deep(.text-box .q-field__marginal) {
height: 100px;
padding: 0px 2px 2px 2px;
border-radius: 0px;
background-color: white;
}
.textarea {
outline: 1px solid #ccc;
background-color: white;
}
</style>