Reworked Pop Table View

This commit is contained in:
2024-05-28 10:58:20 +02:00
parent 2d7a701fab
commit a66d6bf776
5 changed files with 246 additions and 415 deletions

View File

@@ -1,278 +0,0 @@
<?php
$pagination_baseurl = $this->getUrl($Mod, "Index");
$pagination_baseurl_params = ["filter" => $filter];
$pagination_entity_name = "Pops";
?>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet"
type="text/css"/>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?= self::getUrl("Dashboard") ?>"><?= MFAPPNAME_SLUG ?></a>
</li>
<li class="breadcrumb-item active">Pops</li>
</ol>
</div>
<h4 class="page-title">Pops</h4>
</div>
</div>
</div>
<!-- end page title -->
<div class="card">
<div class="card-body mb-3">
<div class="row">
<div class="col-12">
<div class="float-left">
<h4 class="header-title">Liste aller Pops</h4>
</div>
<div class="float-right">
<a class="btn btn-primary mb-2" href="<?= self::getUrl("Pop", "add", ['returnto' => "pop"]) ?>"><i
class="fas fa-plus"></i><span
class="d-none d-lg-inline"> Neuen Pop anlegen</span></a>
</div>
</div>
</div>
<table id="datatable" class="table table-striped table-hover table-sm font-13" style="width: 100%">
<thead>
<tr>
<th>Name</th>
<th>Netzgebiet</th>
<th>Zutritt</th>
<th>Vlan Public/Nat/IPv6</th>
<th>Koordinaten</th>
<th class="edit-width"></th>
</tr>
<tr id="filterrow">
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($pops as $pop):
$vlans = "";
if (!empty(trim($pop->vlan_public)))
$vlans .= ' <span class="order-date-pill active mb-0">Public: <span class="font-weight-500">' . $pop->vlan_public . '</span class="font-weight-500"></span>';
if (!empty(trim($pop->vlan_nat)))
$vlans .= ' <span class="order-date-pill active mb-0">Nat: <span class="font-weight-500">' . $pop->vlan_nat . '</span></span>';
if (!empty(trim($pop->vlan_ipv6)))
$vlans .= ' <span class="order-date-pill active mb-0">IPv6: <span class="font-weight-500">' . $pop->vlan_ipv6 . '</span></span>';
?>
<tr>
<td class="text-nowrap"><a
href="<?= self::getUrl("Pop", "Detail", ["id" => $pop->id]) ?>"><?= $pop->name ?></a>
</td>
<td><?= $pop->networks ?></td>
<td><?= $pop->location ?></td>
<td class="text-center"><?= trim($vlans) ?></td>
<td class="text-center"><a
title="Google-Maps: <?= rtrim($pop->gps_lat, '0') ?> , <?= $pop->gps_long ?>"
class="mapsLink"
href="http://maps.google.com/?q=<?= $pop->gps_lat ?> , <?= $pop->gps_long ?>"
target="_blank"><?= rtrim($pop->gps_lat, '0') ?>
, <?= rtrim($pop->gps_long, 0) ?></a></td>
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
<a href="<?= self::getUrl("Pop", "edit", ["id" => $pop->id, 'returnto' => "pop"]) ?>"><i
class="far fa-edit" title="Bearbeiten"></i></a>
<a href="<?= self::getUrl("Pop", "index") ?>" class="text-danger" title="Löschen"><i
class="fas fa-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- --><?php //include(realpath(dirname(__FILE__) . "/../") . "/tpl/pagination-summary.php"); ?>
<!-- --><?php //include(realpath(dirname(__FILE__) . "/../") . "/tpl/pagination.php"); ?>
<!---->
</div>
</div>
</div>
</div>
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
<script type="text/javascript">
function toggleBuilding(id) {
$('#building-detail-' + id).toggle();
if ($('#building-detail-' + id).is(":hidden")) {
$('#building-' + id).removeClass("table-info");
$('#building-' + id).removeClass("text-info");
} else {
$('#building-' + id).addClass("text-info");
$('#building-' + id).addClass("table-info");
}
}
function toggleTerminationControl(id, type) {
$("#term-" + type + "-" + id + "-text").toggle();
$("#term-" + type + "-" + id + "-input").toggle();
$("#term-" + type + "-" + id + "-edit").toggle();
}
function saveTerminationControl(id, type) {
if (!Number.isInteger(id) || id < 1) {
return false;
}
var value = $("#term-" + type + "-" + id + "-input input[type=text]").val();
$.post("<?=self::getUrl("Termination", "Api")?>",
{
'do': "setValue",
id: id,
type: type,
value: value
},
function (success) {
if (success.status == "OK") {
$("#term-" + type + "-" + id + "-text").text(value);
} else {
console.log("error saving (" + type + ", '" + value + "')");
}
toggleTerminationControl(id, type);
},
'json');
}
/*
* Globals for map display
*/
var buildingMap;
var buildings = [];
var markers = [];
var markerState = true;
var mapCenterPos = [<?=TT_PLACEHOLDER_GPS_LAT?>, <?=TT_PLACEHOLDER_GPS_LONG?>];
function refreshMap() {
// get buildings and render map
$('#map-link').hide();
$('#map-row').show();
getMapdata();
}
function renderMap() {
if (buildingMap) {
markers.forEach(function (m) {
buildingMap.removeLayer(m);
});
} else {
buildingMap = L.map('building-map').setView([<?=TT_PLACEHOLDER_GPS_LAT?>, <?=TT_PLACEHOLDER_GPS_LONG?>], 12);
}
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
minZoom: 4,
maxZoom: 22,
id: 'mapbox/streets-v11',
accessToken: '<?=TT_MAPBOX_TILE_API_TOKEN?>'
}).addTo(buildingMap);
addMarkers();
}
function addMarkers() {
if (!Array.isArray(buildings) | !buildings.length) {
return false;
}
// draw markers and calculate center position
var all_coords = [];
buildings.forEach(function (building) {
if (!building.gps_lat || !building.gps_long) {
return;
}
var gps = [building.gps_lat, building.gps_long];
all_coords.push(gps);
var marker = L.marker(gps).addTo(buildingMap);
markers[building.id] = marker;
});
// calculate center position
mapCenterPos = GetCenterFromDegrees(all_coords);
buildingMap.setView(mapCenterPos, 12);
return true;
}
function centerMap() {
buildingMap.setView(mapCenterPos, 12);
}
// gets buildings and calls renderMap()
function getMapdata() {
filter = getFilter();
$.post('<?=self::getUrl("Building", "Api")?>', {
'do': "getFilteredBuildings",
filter: filter
}, function (success) {
if (success.status == "OK") {
if (Array.isArray(success.result.buildings)) {
buildings = success.result.buildings;
renderMap();
}
}
},
'json'
);
}
function getFilter() {
var fields = ['network_id', 'networksection_id', 'status_id', 'code', 'street'];
var filter = {};
fields.forEach(function (field) {
if (!field) {
return;
}
let val = $('#filter_' + field).val();
if (val.length) {
filter[field] = val;
}
});
return filter;
}
// navigation
var building;
var hash = window.location.hash.substr(1);
var match = hash.match(/building=(\d+)/);
if (match && match[1]) {
building = match[1]
toggleBuilding(building);
//$('body').scrollTop($('#building-' + building).offset() - 50);
}
<?php if(is_array($filter) && count($filter)): ?>
//refreshMap();
<?php endif; ?>
var hidesearch = [5];
$(document).ready(function () {
});
</script>
<script type="text/javascript" src="<?= self::getResourcePath() ?>assets/js/datatables-std.js"></script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -19,11 +19,43 @@ class PopController extends mfBaseController
protected function indexAction()
{
$networks = array_map(function($network) {
return [
"text" => $network->name,
"value" => $network->name
];
}, NetworkModel::getAll());
$this->layout()->setTemplate("Pop/Index");
$pops = PopModel::getAlladv();
$pops = array_map(function($pop) {
return [
"id" => $pop->id,
"name" => $pop->name,
"networkArea" => $pop->network->name,
"location" => $pop->location,
"vlan" => [
"public" => $pop->vlan_public,
"nat" => $pop->vlan_nat,
"ipv6" => $pop->vlan_ipv6
],
"gps" => $pop->gps_lat . ", " . $pop->gps_long
];
}, PopModel::getAlladv());
$this->layout()->set("pops", $pops);
$JSGlobals = ["BASE_URL" => self::getUrl(""),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Pops",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Devices", "href" => self::getUrl("Pop")]
],
"NETWORKS" => $networks,
"POPS" => $pops,
];
$this->layout()->set("vueViewName", "Pop");
$this->layout()->set("JSGlobals", $JSGlobals);
$this->layout()->setTemplate("VueViews/Vue");
}
@@ -81,7 +113,7 @@ class PopController extends mfBaseController
$popnetwork = PopNetworkModel::getbyPopid($id);
$this->layout()->set("popnetwork", $popnetwork['network_id']);
$this->layout()->set("pop", $pop);
return $this->addAction();
$this->addAction();
}
protected function saveAction()
@@ -136,8 +168,8 @@ class PopController extends mfBaseController
if (!$new_id) {
$this->layout()->setFlash("Fehler beim Speichern", "error");
$this->layout()->set("network", $network);
return $this->addAction();
if (isset($network)) $this->layout()->set("network", $network);
$this->addAction();
}
if ($r->network_id) {
@@ -188,8 +220,6 @@ class PopController extends mfBaseController
$this->layout()->setFlash("Pop erfolgreich gespeichert.", "success");
$this->redirect($this->returUrl, $returnAction, $returnVariables, $returnAnker);
}
protected function apiAction()

View File

@@ -0,0 +1,64 @@
Vue.component('Pop', {
//language=Vue
template: `
<tt-card>
<tt-table :data="window['TT_CONFIG']['POPS']" :config="PopTableConfig" excel-export>
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="window.location = window['TT_CONFIG']['BASE_URL'] + '/Pop/add'">
<i class="fas fa-plus"></i>
Pop hinzufügen
</button>
</template>
<template v-slot:name="{ row }">
<a target="_blank" :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/Detail?id=' + row.id">{{row.name}}</a>
</template>
<template v-slot:vlan="{ row }">
<span v-if="row.vlan.public" class="order-date-pill text-nowrap active mb-1">Public: <span class="font-weight-bold">{{row.vlan.public}}</span></span>
<span v-if="row.vlan.nat" class="order-date-pill text-nowrap active mb-1">Nat: <span class="font-weight-bold">{{row.vlan.nat}}</span></span>
<span v-if="row.vlan.ipv6" class="order-date-pill text-nowrap active mb-0">IPv6: <span class="font-weight-bold">{{row.vlan.ipv6}}</span></span>
</template>
<template v-slot:gps="{ row }">
<a
v-if="row.gps"
:title="'Google Maps: ' + row.gps"
class="mapsLink"
:href="'http://maps.google.com/?q=' + row.gps"
v-text="row.gps"
target="_blank"></a>
</template>
<template v-slot:actions="{ row }">
<a :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/edit/?id=' + row.id"><i class="far fa-edit" title="Bearbeiten"></i></a>
<a :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/delete/?id=' + row.id" onclick="if(!confirm('Device wirklich löschen?')) return false;" class="text-danger" title="Löschen"><i class="fas fa-trash "></i></a>
</template>
</tt-table>
</tt-card>
`,
data() {
return {
window: window,
PopTableConfig: {
key: 'PopTable',
tableHeader: 'Pops',
defaultPageSize: 25,
headers: [
{text: 'Name', key: 'name', priority: 10},
{text: 'Netzgebiet', key: 'networkArea', class: 'text-center', filter: 'autocomplete',
filterOptions: window['TT_CONFIG']['NETWORKS'],
priority: 8},
{text: 'Zutritt', key: 'location', class: 'text-center', priority: 1},
{text: 'Standort', key: 'gps', class: 'text-center', priority: 2},
{text: 'Vlan Public/Nat/ipv6', key: 'vlan', class: 'text-center', priority: 7},
{text: 'Aktionen', key: 'actions', class: 'text-center', sortable: false, filter: false, priority: 9},
],
},
}
}
});

View File

@@ -79,7 +79,7 @@
}
.tt-table.table-sm > tbody > tr > td * {
font-size: 13px !important;
font-size: 13px;
}
.tt-table.table-sm > tbody > tr {
@@ -139,4 +139,16 @@ input[type=number]::-webkit-outer-spin-button {
margin-top: 8px;
margin-bottom: 8px;
}
.fa-circle-xmark, .fa-ban, .fa-trash, .fa-edit, .fa-square-check, .fa-arrows-up-down-left-right, .fa-chevron-right {
font-size: 20px !important
}
.tt-table.table-sm > tbody > tr > td * {
font-size: 16px !important;
}
}
td {
vertical-align: middle !important;
}

View File

@@ -109,135 +109,137 @@ Vue.component('tt-table-pagination', {
Vue.component('tt-table', {
template: `
<div class="tt-table-container" ref="tableContainer">
<!-- Top Buttons -->
<div class="tt-table-top-buttons-container">
<slot name="top-buttons"></slot>
</div>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<div class="tt-table-top-pagination-container">
<!-- if excelExport is true, show the export button fontawesome icon excel -->
<div style="display:flex;align-items: center;">
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
</div>
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
v-if="pagination"></tt-table-pagination>
<div class="tt-table-container" ref="tableContainer">
<!-- Top Buttons -->
<div class="tt-table-top-buttons-container">
<slot name="top-buttons"></slot>
</div>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<div class="tt-table-top-pagination-container">
<!-- if excelExport is true, show the export button fontawesome icon excel -->
<div style="display:flex;align-items: center;">
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
</div>
</nav>
<!-- Table -->
<table
ref="table"
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
<thead style="border-width: 2px">
<tr>
<th scope="col" v-for="column in columns"
:ref="'table_header_'+column.key"
v-if="!hiddenColumns.includes(column.key)"
:style="'vertical-align: top; text-align: center;' +
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
v-if="pagination"></tt-table-pagination>
</div>
</nav>
<!-- Table -->
<table
ref="table"
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
<thead style="border-width: 2px">
<tr>
<th scope="col" v-for="column in columns"
:ref="'table_header_'+column.key"
v-if="!hiddenColumns.includes(column.key)"
:style="'vertical-align: top; text-align: center;' +
(column.filter === 'dateRange' ? 'min-width: 260px;' : '') +
(originalColumnWidths[column.key] ? 'width: ' + originalColumnWidths[column.key] + 'px;' : '')"
>
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
@click="column.sortable ? setOrder(column.key) : undefined">
{{ column.text }}
<i
v-if="column.sortable"
:class="getSortIconClass(column.key)"></i>
</div>
<tt-input v-if="column.filter === 'search'" sm v-model="filters[column.key]"></tt-input>
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions"
v-model="filters[column.key]"></tt-icon-select>
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr"
v-model="filters[column.key]"></tt-number-range>
<tt-select v-else-if="column.filter === 'select'"
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"></tt-select>
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"> </tt-autocomplete>
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
</th>
</tr>
</thead>
<tbody>
<tr v-if="pagination?.filtered_available === 0" style="height: 150px">
<td :colspan="Object.keys(columns).length" :rowspan="5" class="text-center">Keine Ergebnisse!</td>
</tr>
<tr v-else-if="(pagination === null && ssr === true) || rows === null"
style="height: 150px">
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
</tr>
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
@click="$emit('row-click', row)"
>
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
<td :class="{ 'text-center': column.filter === 'iconSelect',
[columns[key].class]: true, 'text-nowrap': !originalColumnWidths[column.key] }">
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
<i v-if="key === Object.keys(columns)[0] &&
>
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
@click="column.sortable ? setOrder(column.key) : undefined">
{{ column.text }}
<i
v-if="column.sortable"
:class="getSortIconClass(column.key)"></i>
</div>
<tt-input v-if="column.filter === 'search'" sm v-model="filters[column.key]"></tt-input>
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions"
v-model="filters[column.key]"></tt-icon-select>
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr"
v-model="filters[column.key]"></tt-number-range>
<tt-select v-else-if="column.filter === 'select'"
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"></tt-select>
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
v-model="filters[column.key]"></tt-autocomplete>
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
</th>
</tr>
</thead>
<tbody>
<tr v-if="pagination?.filtered_available === 0" style="height: 150px">
<td :colspan="Object.keys(columns).length" :rowspan="5" class="text-center">Keine Ergebnisse!</td>
</tr>
<tr v-else-if="(pagination === null && ssr === true) || rows === null"
style="height: 150px">
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
</tr>
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
@click="$emit('row-click', row)"
>
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
<td :class="{ 'text-center': column.filter === 'iconSelect',
[columns[key].class]: true,
//'text-nowrap': !originalColumnWidths[column.key]
}">
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
<i v-if="key === Object.keys(columns)[0] &&
($scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row)) || hiddenColumns.length > 0)"
@click.stop="toggleExpand(index)"
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
@click.stop="toggleExpand(index)"
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
<span
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
<i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
<i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
</slot>
</td>
</template>
</tr>
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
<td :colspan="Object.keys(columns).length">
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
<template v-if="hiddenColumns.includes(key)">
<strong>{{ column.text }}:</strong>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
</slot>
</td>
</template>
</tr>
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
<td :colspan="Object.keys(columns).length">
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
<template v-if="hiddenColumns.includes(key)">
<strong>{{ column.text }}:</strong>
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
<span
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
<i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
<i v-else-if="column.filter === 'iconSelect'"
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
<span v-else
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
</span>
</slot>
</template>
</li>
</ul>
<slot name="expandedRow" :row="row"></slot>
</td>
</tr>
</template>
</tbody>
</table>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
v-if="pagination"></tt-table-pagination>
</nav>
</div>
</slot>
</template>
</li>
</ul>
<slot name="expandedRow" :row="row"></slot>
</td>
</tr>
</template>
</tbody>
</table>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
v-if="pagination"></tt-table-pagination>
</nav>
</div>
`, props: {
fetchUrl: String,
data: Array,
@@ -482,12 +484,9 @@ Vue.component('tt-table', {
this.hiddenColumns = []
await this.$nextTick()
// Initialize original column widths only if empty
if (Object.keys(this.originalColumnWidths).length === 0) {
for (let i = 0; i < Object.keys(this.columns).length; i++) {
const column = Object.keys(this.columns)[i]
this.originalColumnWidths[column] = this.$refs[`table_header_${column}`][0].offsetWidth
}
for (let i = 0; i < Object.keys(this.columns).length; i++) {
const column = Object.keys(this.columns)[i]
this.originalColumnWidths[column] = this.$refs[`table_header_${column}`][0].offsetWidth
}
// Create an array of columns sorted by priority
@@ -602,6 +601,7 @@ Vue.component('tt-table', {
return pagesArray.length === 0 ? [1] : pagesArray;
}, computedRows() {
if (!this.rawRows || this.ssr === true) return null;
// console.time('Filtering and pagination');
function handleRangeFilter(filter, value) {
@@ -647,11 +647,14 @@ Vue.component('tt-table', {
if (header.filter === 'search') {
const isNegated = filterValue.startsWith('!');
const targetValue = (row[header.key] ? row[header.key].toString().toLowerCase() : '');
const substrings = (isNegated ? filterValue.slice(1) : filterValue).split(' ').map(s => s.toLowerCase());
const targetValue = !row[header.key] ? '' :
typeof row[header.key] === 'object' ? Object.values(row[header.key]).join(' ').toLowerCase() : row[header.key].toString().toLowerCase();
let substringMatch = true;
for (var k = 0, klen = substrings.length; k < klen; ++k) {
if (!targetValue.includes(substrings[k])) {
substringMatch = false;
break;
@@ -672,7 +675,7 @@ Vue.component('tt-table', {
}
} else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
if (filterValue === '') continue;
if (filterValue !== row[header.key].toString()) {
if (filterValue !== row[header.key]?.toString()) {
match = false;
break;
}
@@ -708,10 +711,10 @@ Vue.component('tt-table', {
let valueA = isDateColumn ?
new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) * 1000 : parseInt(a[this.order.key])).getTime() :
a[this.order.key];
b[this.order.key] || ''
let valueB = isDateColumn ?
new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) * 1000 : parseInt(b[this.order.key])).getTime() :
b[this.order.key];
b[this.order.key] || ''
if (valueA === valueB) return 0;