Reworked Device Table and added table features
This commit is contained in:
@@ -1,167 +0,0 @@
|
|||||||
<?php
|
|
||||||
$pagination_baseurl = $this->getUrl($Mod, "Index");
|
|
||||||
$pagination_baseurl_params = ["filter" => $filter];
|
|
||||||
$pagination_entity_name = "Device";
|
|
||||||
?>
|
|
||||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
|
|
||||||
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet"
|
|
||||||
type="text/css"/>
|
|
||||||
<style>
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<!-- 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">Devices</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
<h4 class="page-title">Devices</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 Devices</h4>
|
|
||||||
</div>
|
|
||||||
<div class="float-right ">
|
|
||||||
<a class="btn btn-primary mb-2" href="<?= self::getUrl("Device", "add") ?>"><i
|
|
||||||
class="fas fa-plus"></i><span
|
|
||||||
class="d-none d-lg-inline"> Neues Device anlegen</span></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table id="datatable" class="table table-striped table-hover table-sm font-13">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="all">Device Name</th>
|
|
||||||
<th class="text-center ">Geräte Typ</th>
|
|
||||||
<th class="text-center ">Hersteller</th>
|
|
||||||
<th class="text-center ">Pop/Adresse</th>
|
|
||||||
<th class="text-center all">IP-Adresse</th>
|
|
||||||
<th class="text-center">Mac-Adresse</th>
|
|
||||||
<th class="text-center">Seriennummer</th>
|
|
||||||
<th class="text-center">Preis</th>
|
|
||||||
<th class="text-center text-nowrap ml-2 mr-2">max. Leistung</th>
|
|
||||||
<th class="text-center">Backup</th>
|
|
||||||
<th class="edit-width"></th>
|
|
||||||
</tr>
|
|
||||||
<tr id="filterrow">
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th class="pr-1"></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php
|
|
||||||
|
|
||||||
foreach ($devices as $device):
|
|
||||||
if ($device->price != "0.00") {
|
|
||||||
$price = $device->price;
|
|
||||||
} else {
|
|
||||||
$price = $device->devicetype->price;
|
|
||||||
}
|
|
||||||
if ($device->power != "0.0") {
|
|
||||||
$power = $device->power;
|
|
||||||
} else {
|
|
||||||
$power = $device->devicetype->power;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($device->last_config_backup) {
|
|
||||||
if (time() - $device->last_config_backup <= 172800) {
|
|
||||||
$backup = '<i class="fa-regular fa-circle-check"><span style="display: none">OK</span></i>';
|
|
||||||
} else {
|
|
||||||
$backup = '<i class="fa-regular fa-circle-xmark" title="Letztes Configbackup älter als 48 Stunden"><span style="display: none">AGED</span></i>';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$backup = '<i class="fa-regular fa-ban" title="Kein Configbackup"><span style="display: none">N/A</span></i>';
|
|
||||||
}
|
|
||||||
if ($device->autobackup==1) {
|
|
||||||
$backup = '<i class="fa-regular fa-circle-a mr-1"><span style="display: none">Auto</span></i>'.$backup;
|
|
||||||
}
|
|
||||||
if ($device->pop->id) {
|
|
||||||
$destination = '<a href="' . self::getUrl("Pop", "Detail", ["id" => $device->pop->id]) . '">' . $device->pop->name . '</a>';
|
|
||||||
} else if ($device->addr_street) {
|
|
||||||
$address = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
|
|
||||||
$destination = '<a title="Google-Maps: ' . $address . '" class="mapsLink" href="http://maps.google.com/?q=' . $address . '" target="_blank">' . $address . '</a>';
|
|
||||||
} else if ($device->gps_lat) {
|
|
||||||
$address = $device->gps_lat . " , " . $device->gps_long;
|
|
||||||
$destination = '<a title="Google-Maps: ' . $address . '" class="mapsLink" href="http://maps.google.com/?q=' . $address . '" target="_blank">' . $address . '</a>';
|
|
||||||
} else {
|
|
||||||
$destination = '';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td class="text-nowrap">
|
|
||||||
<a class=" text-nowrap"
|
|
||||||
href="<?= self::getUrl("Device", "Detail", ["id" => $device->id]) ?>"><?= $device->name ?></a>
|
|
||||||
</td>
|
|
||||||
<td class="text-center"><?= $device->devicetype->name ?></td>
|
|
||||||
<td class="text-center"><?= $device->devicetype->devicemanufactor->name ?></td>
|
|
||||||
<td class="text-center text-nowrap"><?= $destination ?></td>
|
|
||||||
|
|
||||||
|
|
||||||
<td class="text-center"><?= $device->ip ?></td>
|
|
||||||
<td class="text-center"><?= $device->mac ?></td>
|
|
||||||
<td class="text-center"><?= $device->serial ?></td>
|
|
||||||
<td class="text-right text-nowrap"><?= $price ?> €</td>
|
|
||||||
<td class="text-right text-nowrap"><?= $power ?> Watt</td>
|
|
||||||
<td class="text-center"><?= $backup ?></td>
|
|
||||||
|
|
||||||
|
|
||||||
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em; width: 80px">
|
|
||||||
<a href="<?= self::getUrl("Device", "edit", ["id" => $device->id]) ?>"><i
|
|
||||||
class="far fa-edit" title="Bearbeiten"></i></a>
|
|
||||||
<a href="<?= self::getUrl("Device", "delete", ["id" => $device->id]) ?>"
|
|
||||||
onclick="if(!confirm('Device wirklich löschen?')) return false;" class="text-danger"
|
|
||||||
title="Löschen"><i class="fas fa-trash "></i></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var hidesearch = [7, 8, 10];
|
|
||||||
var columndefs = {type: 'ip-address', targets: 4};
|
|
||||||
|
|
||||||
var columnfilter = [9];
|
|
||||||
var columnoptions = '<option value=""></option><option value="OK">OK</option><option value="AGED">AGED</option><option value="N/A">N/A</option><option value="Auto">AUTO</option>';
|
|
||||||
$(document).ready(function () {
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript"
|
|
||||||
src="<?= self::getResourcePath() ?>assets/js/datatables-std.js?<?= date('U') ?>"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
|
|
||||||
@@ -17,11 +17,36 @@ class DeviceController extends mfBaseController
|
|||||||
|
|
||||||
protected function indexAction()
|
protected function indexAction()
|
||||||
{
|
{
|
||||||
|
$deviceManufacturers = array_map(function($manufacturer) {
|
||||||
|
return [
|
||||||
|
"text" => $manufacturer->name,
|
||||||
|
"value" => $manufacturer->name,
|
||||||
|
];
|
||||||
|
}, DevicemanufactorModel::getAll());
|
||||||
|
|
||||||
$this->layout()->setTemplate("Device/Index");
|
$deviceTypes = array_map(function($deviceType) {
|
||||||
$devices = DeviceModel::getAll();
|
return [
|
||||||
$this->layout()->set("devices", $devices);
|
"text" => $deviceType->name,
|
||||||
|
"value" => $deviceType->name,
|
||||||
|
];
|
||||||
|
}, DevicetypeModel::getAll());
|
||||||
|
|
||||||
|
$JSGlobals = ["BASE_URL" => self::getUrl("Device"),
|
||||||
|
"DASHBOARD_URL" => self::getUrl("Dashboard"),
|
||||||
|
"MFAPPNAME" => MFAPPNAME_SLUG,
|
||||||
|
"PAGE_TITLE" => "Devices",
|
||||||
|
"PATH" => [
|
||||||
|
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||||
|
["text" => "Devices", "href" => self::getUrl("Device")]
|
||||||
|
],
|
||||||
|
"DEVICE_MANUFACTURERS" => $deviceManufacturers,
|
||||||
|
"DEVICE_TYPES" => $deviceTypes,
|
||||||
|
"DEVICE_API_URL" => self::getUrl("Device/api"),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->layout()->set("vueViewName", "Device");
|
||||||
|
$this->layout()->set("JSGlobals", $JSGlobals);
|
||||||
|
$this->layout()->setTemplate("VueViews/Vue");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +108,7 @@ class DeviceController extends mfBaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->layout()->set("device", $device);
|
$this->layout()->set("device", $device);
|
||||||
return $this->addAction();
|
$this->addAction();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function saveAction()
|
protected function saveAction()
|
||||||
@@ -253,57 +278,25 @@ class DeviceController extends mfBaseController
|
|||||||
|
|
||||||
switch ($do) {
|
switch ($do) {
|
||||||
case "getDevices":
|
case "getDevices":
|
||||||
$devices = DeviceModel::getAll();
|
header('Content-Type: application/json');
|
||||||
|
die(json_encode($this->getDevices()));
|
||||||
foreach ($devices as $device) {
|
|
||||||
$locationText = "";
|
|
||||||
$locationUrl = "";
|
|
||||||
|
|
||||||
if (trim($device->pop->name)) {
|
|
||||||
$locationText = $device->pop->name;
|
|
||||||
$locationUrl = self::getUrl("Pop", "Detail", ["id" => $device->pop->id]);
|
|
||||||
} else if (trim($device->addr_street)) {
|
|
||||||
$locationText = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
|
|
||||||
$locationUrl = "http://maps.google.com/?q=" . $locationText;
|
|
||||||
} else if (trim($device->gps_lat)) {
|
|
||||||
$locationText = $device->gps_lat . " , " . $device->gps_long;
|
|
||||||
$locationUrl = "http://maps.google.com/?q=" . $locationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data[] = [
|
|
||||||
"name" => $device->name,
|
|
||||||
"devicetype" => $device->devicetype->name,
|
|
||||||
"devicemanufactor" => $device->devicetype->devicemanufactor->name,
|
|
||||||
"locationText" => $locationText,
|
|
||||||
"locationUrl" => $locationUrl,
|
|
||||||
"ip" => $device->ip,
|
|
||||||
"mac" => $device->mac,
|
|
||||||
"serial" => $device->serial,
|
|
||||||
"price" => $device->price != "0.0" ? $device->price : $device->devicetype->price,
|
|
||||||
"power" => $device->power != "0.0" ? $device->power : $device->devicetype->power,
|
|
||||||
"backup" => $device->last_config_backup ? (time() - $device->last_config_backup <= 172800 ? 'ok' : 'aged') : 'na',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
die(json_encode($data));
|
|
||||||
break;
|
|
||||||
case "getconfig":
|
case "getconfig":
|
||||||
$return = $this->getConfig($id, $format, $filename);
|
$this->getConfig($id, $format, $filename);
|
||||||
break;
|
break;
|
||||||
case "createconfig":
|
case "createconfig":
|
||||||
$return = $this->createConfig($ip);
|
$this->createConfig($ip);
|
||||||
break;
|
break;
|
||||||
case "getoltinfo":
|
case "getoltinfo":
|
||||||
$return = $this->getoltInfo($ip, $portid, $adv);
|
$this->getoltInfo($ip, $portid, $adv);
|
||||||
break;
|
break;
|
||||||
case "getontinfo":
|
case "getontinfo":
|
||||||
$return = $this->getontInfo($ip, $portid, $ont);
|
$this->getontInfo($ip, $portid, $ont);
|
||||||
break;
|
break;
|
||||||
case "getontinfomac":
|
case "getontinfomac":
|
||||||
$return = $this->getontInfoMac($ip, $portid, $ont);
|
$this->getontInfoMac($ip, $portid, $ont);
|
||||||
break;
|
break;
|
||||||
case "changeoltsplitter":
|
case "changeoltsplitter":
|
||||||
$return = $this->changeoltSplitter($id, $portid, $ports);
|
$this->changeoltSplitter($id, $portid, $ports);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$return = false;
|
$return = false;
|
||||||
@@ -352,7 +345,7 @@ class DeviceController extends mfBaseController
|
|||||||
$returnUrl = "Device";
|
$returnUrl = "Device";
|
||||||
$returnAction = "Detail";
|
$returnAction = "Detail";
|
||||||
$returnVariables['id'] = $id;
|
$returnVariables['id'] = $id;
|
||||||
return $this->redirect($returnUrl, $returnAction, $returnVariables, $returnAnker);
|
$this->redirect($returnUrl, $returnAction, $returnVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function changeoltSplitter($id, $portid, $ports)
|
private function changeoltSplitter($id, $portid, $ports)
|
||||||
@@ -390,4 +383,54 @@ class DeviceController extends mfBaseController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getDevices()
|
||||||
|
{
|
||||||
|
$devices = DeviceModel::getAll();
|
||||||
|
|
||||||
|
foreach ($devices as $device) {
|
||||||
|
$locationText = "";
|
||||||
|
$locationUrl = "";
|
||||||
|
|
||||||
|
if (trim($device->pop->name)) {
|
||||||
|
$locationText = $device->pop->name;
|
||||||
|
$locationUrl = self::getUrl("Pop", "Detail", ["id" => $device->pop->id]);
|
||||||
|
} else if (trim($device->addr_street)) {
|
||||||
|
$locationText = $device->addr_street . " " . $device->addr_number . ", " . $device->addr_zip . " " . $device->addr_city;
|
||||||
|
$locationUrl = "http://maps.google.com/?q=" . $locationText;
|
||||||
|
} else if (trim($device->gps_lat)) {
|
||||||
|
$locationText = $device->gps_lat . " , " . $device->gps_long;
|
||||||
|
$locationUrl = "http://maps.google.com/?q=" . $locationText;
|
||||||
|
}
|
||||||
|
|
||||||
|
$backup = 'na';
|
||||||
|
if ($device->last_config_backup) {
|
||||||
|
if (time() - $device->last_config_backup <= 172800) {
|
||||||
|
$backup = 'ok';
|
||||||
|
if ($device->autobackup==1) {
|
||||||
|
$backup = 'auto';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$backup = 'aged';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = [
|
||||||
|
"id" => $device->id,
|
||||||
|
"name" => $device->name,
|
||||||
|
"devicetype" => $device->devicetype->name,
|
||||||
|
"devicemanufactor" => $device->devicetype->devicemanufactor->name,
|
||||||
|
"locationText" => $locationText,
|
||||||
|
"locationUrl" => $locationUrl,
|
||||||
|
"ip" => $device->ip,
|
||||||
|
"mac" => $device->mac,
|
||||||
|
"serial" => $device->serial,
|
||||||
|
"price" => $device->price != "0.00" ? $device->price : $device->devicetype->price,
|
||||||
|
"power" => $device->power != "0.00" ? intval($device->power) : intval($device->devicetype->power),
|
||||||
|
"backup" => $backup,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
72
public/js/pages/Device/Device.js
Normal file
72
public/js/pages/Device/Device.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
Vue.component('Device', {
|
||||||
|
//language=Vue
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
|
||||||
|
|
||||||
|
<tt-table
|
||||||
|
:fetch-url="window['TT_CONFIG']['DEVICE_API_URL'] + '?do=getDevices'"
|
||||||
|
:config="DeviceTableConfig"
|
||||||
|
small ref="table" excel-export
|
||||||
|
>
|
||||||
|
|
||||||
|
<template v-slot:top-buttons>
|
||||||
|
<button type="button" class="btn btn-primary" @click="window.location = window['TT_CONFIG']['BASE_URL'] + '/add'">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
Device hinzufügen
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:name="{ row }">
|
||||||
|
<a target="_blank" :href="window['TT_CONFIG']['BASE_URL'] +'/Detail?id=' + row.id">{{row.name}}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:locationtext="{ row }">
|
||||||
|
<a target="_blank" :href="row['locationUrl']">{{row.locationText}}</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:actions="{ row }">
|
||||||
|
<a :href="window['TT_CONFIG']['BASE_URL'] +'/edit/?id=' + row.id"><i class="far fa-edit" title="Bearbeiten"></i></a>
|
||||||
|
<a :href="window['TT_CONFIG']['BASE_URL'] +'/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>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
window: window,
|
||||||
|
DeviceTableConfig: {
|
||||||
|
key: 'DeviceTable',
|
||||||
|
tableHeader: 'Device-Liste',
|
||||||
|
defaultPageSize: 25,
|
||||||
|
headers: [
|
||||||
|
{ text: 'Device Name', key: 'name', sortable: true, class: 'text-nowrap', priority: 10 },
|
||||||
|
{ text: 'Hersteller', key: 'devicemanufactor', filter: 'select',class: 'text-nowrap text-center', filterOptions: window.TT_CONFIG["DEVICE_MANUFACTURERS"] },
|
||||||
|
{ text: 'Geräte Typ', key: 'devicetype', filter: 'autocomplete', class: 'text-nowrap text-center', filterOptions: window.TT_CONFIG["DEVICE_TYPES"] , priority: 7},
|
||||||
|
{ text: 'Pop/Adresse', key: 'locationText', filter: 'search' , class: 'text-nowrap text-center'},
|
||||||
|
{ text: 'IP-Adresse', key: 'ip', filter: 'search',class: 'text-center', priority: 8},
|
||||||
|
{ text: 'Mac-Adresse', key: 'mac', filter: 'search',class: 'text-center' },
|
||||||
|
{ text: 'Seriennummer', key: 'serial', filter: 'search',class: 'text-center' },
|
||||||
|
{ text: 'Preis', key: 'price', filter: 'numberRange',class: 'text-center', suffix: ' €' },
|
||||||
|
{ text: 'max. Leistung', key: 'power', filter: 'numberRange',class: 'text-center', suffix: ' W' },
|
||||||
|
{
|
||||||
|
text: 'Backup',
|
||||||
|
key: 'backup',
|
||||||
|
filter: 'iconSelect',
|
||||||
|
filterOptions: [
|
||||||
|
{ value: 'ok', text: 'Configbackup aktuell', icon: 'fa-regular fa-circle-check text-success' },
|
||||||
|
{ value: 'auto', text: 'Configbackup aktuell', icon: 'fa-regular fa-circle-a text-success' },
|
||||||
|
{ value: 'aged', text: 'Letztes Configbackup älter als 48 Stunden', icon: 'fa-regular fa-circle-xmark' },
|
||||||
|
{ value: 'na', text: 'N/A', icon: 'fa-regular fa-ban text-warning' }
|
||||||
|
],
|
||||||
|
priority: 6,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{text: 'Aktionen', key: 'actions', class: 'text-center', sortable: false, filter: false, priority: 9},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -20,7 +20,7 @@ Vue.component('Domain', {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" v-model="checkDomainInput" placeholder="Neue Domain überprüfen">
|
<input type="text" class="form-control" v-model="checkDomainInput" style="height: 100%" placeholder="Neue Domain überprüfen">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-primary" @click="checkDomainAvailability">
|
<button class="btn btn-primary" @click="checkDomainAvailability">
|
||||||
<template v-if="checkDomainLoading">
|
<template v-if="checkDomainLoading">
|
||||||
@@ -83,7 +83,7 @@ Vue.component('Domain', {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="record in dnsRecordsModal.records">
|
<tr v-for="record in dnsRecordsModal.records">
|
||||||
<td>{{ record.class }}</td>
|
<td v-if="record">{{ record.class }}</td>
|
||||||
<td>{{ record.type }} {{ record.pri ? '(' + record.pri + ')' : '' }}</td>
|
<td>{{ record.type }} {{ record.pri ? '(' + record.pri + ')' : '' }}</td>
|
||||||
<td>{{ record.host }}</td>
|
<td>{{ record.host }}</td>
|
||||||
<td>{{ record.value }}</td>
|
<td>{{ record.value }}</td>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
|
|
||||||
.tt-table-card {
|
.tt-table-card {
|
||||||
overflow: auto;
|
/*overflow: auto;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-card .page-link {
|
.tt-table-card .page-link {
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-pagination-wrapper {
|
.tt-table-pagination-wrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
@@ -48,28 +49,86 @@
|
|||||||
grid-gap: 4px;
|
grid-gap: 4px;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-pagination {
|
.tt-table-pagination {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-page-item {
|
.tt-table-page-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-page-item.disabled {
|
.tt-table-page-item.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-page-item.active {
|
.tt-table-page-item.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-select {
|
.tt-table-select {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-table-text-center {
|
.tt-table-text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-pointer {
|
.tt-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-table.table-sm > tbody > tr > td * {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-table.table-sm > tbody > tr {
|
||||||
|
line-height: 1.15 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=number]::-webkit-inner-spin-button,
|
||||||
|
input[type=number]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*table {*/
|
||||||
|
/* width: 100%; !* Ensure table takes full available width *!*/
|
||||||
|
/* table-layout: auto; !* Crucial for automatic column scaling *!*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
/*td {*/
|
||||||
|
/* white-space: nowrap; !* Prevent text from wrapping within cells *!*/
|
||||||
|
/* overflow: hidden; !* Hide overflowing content *!*/
|
||||||
|
/* text-overflow: ellipsis; !* Show ellipsis (...) for long content *!*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.tt-table-top-pagination-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 486px) {
|
||||||
|
.tt-table-top-pagination-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-table-top-pagination-container > * {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-table-top-pagination-container > *:first-child {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,16 @@ Vue.component('tt-autocomplete', {
|
|||||||
template: `
|
template: `
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<slot name="prepend"></slot>
|
<slot name="prepend"></slot>
|
||||||
<label :for="label">{{ label }}</label>
|
<label v-if="label" :for="label">{{ label }}</label>
|
||||||
<div class="autocomplete position-relative">
|
<div class="autocomplete position-relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control form-control-sm"
|
||||||
v-model="displayValue"
|
v-model="displayValue"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
style="padding-right: 30px;"
|
:style="{'padding-right': $slots.append ? '30px' : '0'}"
|
||||||
/>
|
/>
|
||||||
<slot name="append"></slot>
|
<slot name="append"></slot>
|
||||||
|
|
||||||
@@ -20,9 +20,9 @@ Vue.component('tt-autocomplete', {
|
|||||||
|
|
||||||
<div v-show="isLoading" class="loader"></div>
|
<div v-show="isLoading" class="loader"></div>
|
||||||
|
|
||||||
<template v-show="showSuggestions && items.length > 0 && isLoading !== true">
|
<template v-show="showSuggestions && displayingItems.length > 0 && isLoading !== true">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in items.slice(0, 10)"
|
v-for="(item) in displayingItems.slice(0, 10)"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:class="{'active': value === item.value}"
|
:class="{'active': value === item.value}"
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@@ -32,15 +32,16 @@ Vue.component('tt-autocomplete', {
|
|||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
</li>
|
</li>
|
||||||
<!-- display more search results available define it more precisely in German -->
|
<!-- display more search results available define it more precisely in German -->
|
||||||
<li v-show="items.length > 10" class="dropdown-item disabled">
|
<li v-show="displayingItems.length > 10" class="dropdown-item disabled">
|
||||||
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
Mehr Suchergebnisse vorhanden. Bitte genauer eingeben
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<li v-show="items.length === 0 && isLoading === false && displayValue.length > 3" class="dropdown-item disabled">
|
<li v-show="displayingItems.length === 0 && isLoading === false && displayValue.length > 3"
|
||||||
|
class="dropdown-item disabled">
|
||||||
Keine Suchergebnisse vorhanden.
|
Keine Suchergebnisse vorhanden.
|
||||||
</li>
|
</li>
|
||||||
<li v-show="displayValue.length < 3" class="dropdown-item disabled">
|
<li v-show="displayValue.length < 3" class="dropdown-item disabled">
|
||||||
Bitte mindestens 3 Zeichen eingeben
|
Bitte mindestens 3 Zeichen eingeben
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,19 +55,29 @@ Vue.component('tt-autocomplete', {
|
|||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
apiUrl: String,
|
apiUrl: String,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
if (this.value) {
|
if (this.value && this.apiUrl) {
|
||||||
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
|
const response = await axios.get(`${this.apiUrl}&autocomplete=1&searchedID=${this.value}`);
|
||||||
this.displayValue = response.data[0].text;
|
this.displayValue = response.data[0].text;
|
||||||
|
} else if (this.value) {
|
||||||
|
const selectedItem = this.items.find(item => item.value === this.value);
|
||||||
|
this.displayValue = selectedItem ? selectedItem.text : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(this.$slots.append ? 'append' : 'no append');
|
||||||
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: [],
|
displayingItems: [],
|
||||||
displayValue: '',
|
displayValue: '',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showSuggestions: false,
|
showSuggestions: false,
|
||||||
@@ -76,7 +87,7 @@ Vue.component('tt-autocomplete', {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value(newValue) {
|
value(newValue) {
|
||||||
const selectedItem = this.items.find(item => item.value === newValue);
|
const selectedItem = this.displayingItems.find(item => item.value === newValue);
|
||||||
this.displayValue = selectedItem ? selectedItem.text : '';
|
this.displayValue = selectedItem ? selectedItem.text : '';
|
||||||
},
|
},
|
||||||
apiUrl() {
|
apiUrl() {
|
||||||
@@ -86,6 +97,7 @@ Vue.component('tt-autocomplete', {
|
|||||||
methods: {
|
methods: {
|
||||||
onInput(event) {
|
onInput(event) {
|
||||||
this.displayValue = event.target.value;
|
this.displayValue = event.target.value;
|
||||||
|
this.$emit('input', undefined);
|
||||||
this.fetchSuggestions();
|
this.fetchSuggestions();
|
||||||
},
|
},
|
||||||
onFocus() {
|
onFocus() {
|
||||||
@@ -97,7 +109,28 @@ Vue.component('tt-autocomplete', {
|
|||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
fetchSuggestions() {
|
fetchSuggestions() {
|
||||||
if (!this.apiUrl || this.displayValue.length < 3) return;
|
if (this.displayValue.length < 3) {
|
||||||
|
this.$set(this, 'displayingItems', []);
|
||||||
|
return this.displayingItems = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.apiUrl) {
|
||||||
|
|
||||||
|
const isNegated = this.displayValue.startsWith('!');
|
||||||
|
const substrings = (isNegated ? this.displayValue.slice(1) : this.displayValue).split(' ').map(s => s.toLowerCase());
|
||||||
|
this.displayingItems = this.items.filter(item => {
|
||||||
|
let substringMatch = true;
|
||||||
|
for (var k = 0, klen = substrings.length; k < klen; ++k) {
|
||||||
|
if (!item.text.toLowerCase().includes(substrings[k])) {
|
||||||
|
substringMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (isNegated && !substringMatch) || (!isNegated && substringMatch);
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
clearTimeout(this.fetchSuggestionsDebounceTimer);
|
||||||
@@ -107,9 +140,9 @@ Vue.component('tt-autocomplete', {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${this.displayValue}`);
|
const response = await axios.get(`${this.apiUrl}&autocomplete=1&q=${this.displayValue}`);
|
||||||
if (response.data?.status === 'error') {
|
if (response.data?.status === 'error') {
|
||||||
this.items = [];
|
this.displayingItems = [];
|
||||||
} else {
|
} else {
|
||||||
this.items = response.data
|
this.displayingItems = response.data
|
||||||
}
|
}
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ Vue.component('tt-icon-select', {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// Dynamically add CSS to disable default dropdown caret
|
if (! document.getElementById('tt-icon-select-style')) {
|
||||||
const style = document.createElement('style');
|
// Dynamically add CSS to disable default dropdown caret
|
||||||
style.innerHTML = `.tt-select .dropdown-toggle::after { display: none; }`;
|
const style = document.createElement('style');
|
||||||
document.body.appendChild(style);
|
style.id = 'tt-icon-select-style';
|
||||||
|
style.innerHTML = `.tt-select .dropdown-toggle::after { display: none; }`;
|
||||||
|
document.body.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('click', this.handleClick);
|
document.addEventListener('click', this.handleClick);
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ Vue.component('tt-number-range', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, template: `
|
}, template: `
|
||||||
<div style="display:grid;grid-template-columns: 75px 25px 75px;grid-gap: 4px;justify-content: center">
|
<div style="display:grid;grid-template-columns: minmax(35px, 75px) minmax(20px, 25px) minmax(35px, 75px);;grid-gap: 4px;justify-content: center">
|
||||||
<slot name="prepend"></slot>
|
<slot name="prepend"></slot>
|
||||||
<input type="number"
|
<input type="number"
|
||||||
|
style="-webkit-appearance: none;
|
||||||
|
-moz-appearance: textfield;"
|
||||||
class="form-control form-control-sm"
|
class="form-control form-control-sm"
|
||||||
v-model.number="inputValueFrom"
|
v-model.number="inputValueFrom"
|
||||||
@input="updateValue"
|
@input="updateValue"
|
||||||
@@ -60,3 +62,5 @@ Vue.component('tt-number-range', {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,44 @@
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} ttTableColumnConfig
|
* @typedef {Object} ttTableColumnConfig
|
||||||
* @property {string} text - The display text of the column.
|
* @property {string} text - The display text of the column header.
|
||||||
* @property {string} key - The unique key of the column.
|
* @property {string} key - The unique key of the column (used for data access and filtering).
|
||||||
* @property {string} filter - Indicates if filtering is enabled for the column.
|
* @property {string} [filter] - (Optional) Determines the type of filter applied to the column. Default is 'search'.
|
||||||
* @property {boolean} sortable - Indicates if sorting is enabled for the column.
|
* Possible values:
|
||||||
* @property {string} class - The CSS class(es) applied to the column.
|
* - 'search': Basic text search (case-insensitive).
|
||||||
|
* - 'iconSelect': Filter with icons and associated text (requires 'filterOptions').
|
||||||
|
* - 'numberRange': Filter with numeric ranges (e.g., ">5", "10-20", etc.).
|
||||||
|
* - 'select': Dropdown filter with predefined options (requires 'filterOptions').
|
||||||
|
* - 'date': Date range filter.
|
||||||
|
* - 'false': Disables filtering for the column.
|
||||||
|
* @property {Array} [filterOptions] - (Optional) An array of filter options for 'iconSelect' or 'select' filters.
|
||||||
|
* Each option should be an object with 'text' (display text) and 'value' (filter value) properties.
|
||||||
|
* For 'iconSelect', an additional 'icon' property (CSS class for the icon) is required.
|
||||||
|
* @property {boolean} [sortable=true] - (Optional) Indicates whether the column is sortable. Default is true.
|
||||||
|
* @property {string} [class] - (Optional) Additional CSS classes to apply to the column.
|
||||||
|
* @property {string} [suffix] - (Optional) Additional CSS classes to apply to the column.
|
||||||
|
* @property {string} [prefix] - (Optional) Additional CSS classes to apply to the column.
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ttTableConfig
|
||||||
|
* @property {string} key - A unique key for the table instance (used for saving/loading settings).
|
||||||
|
* @property {string} tableHeader - The main header text displayed above the table.
|
||||||
|
* @property {ttTableColumnConfig[]} headers - An array of column configuration objects (see `ttTableColumnConfig` typedef).
|
||||||
|
* @property {Function} [customRowClass] - (Optional) A function that returns a CSS class string based on row data.
|
||||||
|
* @property {Function} [expandCondition] - (Optional) A function that determines if a row is expandable.
|
||||||
|
* @property {number} [defaultPageSize=10] - (Optional) The default number of rows to display per page.
|
||||||
|
* @property {Function} [customExcelProcessor] - (Optional) A function to preprocess row data before exporting to Excel.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
Vue.component('tt-table-pagination', {
|
Vue.component('tt-table-pagination', {
|
||||||
props: {
|
props: {
|
||||||
pagination: {
|
pagination: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
default: () => ({page: 1, per_page: 10, total_rows: 0, filtered_available: 0, total_pages: 1})
|
||||||
},
|
}, reverse: {type: Boolean, default: false}
|
||||||
reverse: {type: Boolean, default: false}
|
}, computed: {
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pagesToDisplay() {
|
pagesToDisplay() {
|
||||||
const range = 2;
|
const range = 2;
|
||||||
const start = Math.max(this.pagination.page - range, 1);
|
const start = Math.max(this.pagination.page - range, 1);
|
||||||
@@ -26,65 +48,61 @@ Vue.component('tt-table-pagination', {
|
|||||||
pages.push(i);
|
pages.push(i);
|
||||||
}
|
}
|
||||||
return pages.length === 0 ? [1] : pages;
|
return pages.length === 0 ? [1] : pages;
|
||||||
},
|
}, pageInfoText() {
|
||||||
pageInfoText() {
|
|
||||||
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
|
const start = Math.min(this.pagination.page * this.pagination.per_page - this.pagination.per_page + 1, this.pagination.total_rows);
|
||||||
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
|
const end = Math.min(this.pagination.page * this.pagination.per_page, this.pagination.total_rows);
|
||||||
const total = this.pagination.total_rows === this.pagination.filtered_available
|
const total = this.pagination.total_rows === this.pagination.filtered_available ? this.pagination.total_rows : `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
|
||||||
? this.pagination.total_rows
|
|
||||||
: `${this.pagination.filtered_available} (${this.pagination.total_rows})`;
|
|
||||||
return `${start} bis ${end} von ${total}`;
|
return `${start} bis ${end} von ${total}`;
|
||||||
}
|
}
|
||||||
},
|
}, methods: {
|
||||||
methods: {
|
|
||||||
fetchRows(page) {
|
fetchRows(page) {
|
||||||
this.$emit('fetch-rows', page);
|
this.$emit('fetch-rows', page);
|
||||||
}
|
}
|
||||||
},
|
}, template: `
|
||||||
template: `
|
<div class="tt-table-pagination-container">
|
||||||
<div class="tt-table-pagination-container">
|
<div v-if="pagination && typeof pagination.total_rows === 'number'" class="tt-table-pagination-wrapper">
|
||||||
<div v-if="pagination && typeof pagination.total_rows === 'number'" class="tt-table-pagination-wrapper">
|
|
||||||
<span class="tt-table-text-center" v-text="pageInfoText"
|
<span class="tt-table-text-center" v-text="pageInfoText"
|
||||||
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 1 }"></span>
|
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 1 }"></span>
|
||||||
|
|
||||||
|
|
||||||
<ul class="pagination tt-table-pagination" :style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
|
<ul class="pagination tt-table-pagination" :style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 1 }">
|
||||||
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
|
<li class="page-item tt-table-page-item" v-bind:class="{ disabled: pagination.page === 1 }">
|
||||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
|
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
|
||||||
<span aria-hidden="true">«</span>
|
<span aria-hidden="true">«</span>
|
||||||
<span class="sr-only">First</span>
|
<span class="sr-only">First</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
|
<li class="page-item tt-table-page-item" v-for="pageNumber in pagesToDisplay"
|
||||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
|
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }">
|
||||||
<a class="page-link"
|
<a class="page-link"
|
||||||
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
|
v-bind:class="{ 'active': pageNumber === pagination.page, 'disabled': pageNumber === pagination.page }"
|
||||||
href="#"
|
href="#"
|
||||||
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item tt-table-page-item"
|
<li class="page-item tt-table-page-item"
|
||||||
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
||||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
||||||
aria-label="Last">
|
aria-label="Last">
|
||||||
<span aria-hidden="true">»</span>
|
<span aria-hidden="true">»</span>
|
||||||
<span class="sr-only">Last</span>
|
<span class="sr-only">Last</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
<span class="tt-table-text-center" :style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
|
<span class="tt-table-text-center"
|
||||||
|
:style="{ 'grid-row': reverse ? 2 : 1, 'grid-column': 2 }">Einträge pro Seite</span>
|
||||||
|
|
||||||
|
|
||||||
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
|
<select v-model="pagination.per_page" v-on:change="fetchRows(1)"
|
||||||
class="form-control form-control-sm tt-table-select"
|
class="form-control form-control-sm tt-table-select"
|
||||||
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
|
:style="{ 'grid-row': reverse ? 1 : 2, 'grid-column': 2 }">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,7 +110,7 @@ Vue.component('tt-table-pagination', {
|
|||||||
Vue.component('tt-table', {
|
Vue.component('tt-table', {
|
||||||
template: `
|
template: `
|
||||||
<div class="card tt-table-card" v-if="columns && pagination">
|
<div class="card tt-table-card" v-if="columns && pagination">
|
||||||
<div class="card-body">
|
<div class="card-body" ref="tableContainer">
|
||||||
<!-- Top Buttons -->
|
<!-- Top Buttons -->
|
||||||
<div
|
<div
|
||||||
style="display:grid; grid-template-columns: auto auto auto auto auto; grid-gap: 8px; padding-bottom: 8px">
|
style="display:grid; grid-template-columns: auto auto auto auto auto; grid-gap: 8px; padding-bottom: 8px">
|
||||||
@@ -100,58 +118,30 @@ Vue.component('tt-table', {
|
|||||||
</div>
|
</div>
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<div
|
<div class="tt-table-top-pagination-container">
|
||||||
style="display:grid; grid-template-columns: 1fr 1fr;padding-bottom: 8px;align-items:center; justify-content: space-between">
|
|
||||||
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
||||||
<div style="display:flex;align-items: center;">
|
<div style="display:flex;align-items: center;">
|
||||||
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
|
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
|
||||||
@click="resetTable" class="fa-solid fa-trash-undo"
|
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
|
||||||
style="font-size: 24px;margin-right: 8px;cursor: pointer; color: var(--orange)"></i>
|
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
|
||||||
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
|
||||||
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
||||||
style="font-size: 24px;margin-left: 8px;cursor: pointer; color: var(--success)"></i>
|
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
|
||||||
|
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="pagination && typeof pagination.total_rows === 'number'"
|
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||||
style="display:grid; grid-template-rows: auto auto; grid-template-columns: auto auto; grid-auto-flow: column; grid-gap: 4px; justify-content: end">
|
v-if="pagination"></tt-table-pagination>
|
||||||
<ul class="pagination" style="margin: 0">
|
|
||||||
<li class="page-item" v-bind:class="{ disabled: pagination.page === 1 }">
|
|
||||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
|
|
||||||
<span aria-hidden="true">«</span>
|
|
||||||
<span class="sr-only">First</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" v-for="pageNumber in pagesToDisplay"
|
|
||||||
v-bind:class="{ 'active disabled': pageNumber === pagination.page }">
|
|
||||||
<a class="page-link" v-bind:class="{ 'active disabled': pageNumber === pagination.page }" href="#"
|
|
||||||
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
|
|
||||||
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
|
|
||||||
aria-label="Last">
|
|
||||||
<span aria-hidden="true">»</span>
|
|
||||||
<span class="sr-only">Last</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<span class="text-center"
|
|
||||||
v-text="Math.min(pagination.page * pagination.per_page - pagination.per_page + 1, pagination.filtered_available)
|
|
||||||
+ ' bis ' + Math.min(pagination.page * pagination.per_page, pagination.filtered_available) + ' von ' + (pagination.total_rows === pagination.filtered_available ? pagination.total_rows : pagination.filtered_available + ' ('+pagination.total_rows+')')"></span>
|
|
||||||
<select v-model="pagination.per_page" v-on:change="fetchRows(1)" class="form-control form-control-sm">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select>
|
|
||||||
<span class="text-center">Einträge pro Seite</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
<table
|
<table
|
||||||
|
ref="table"
|
||||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
: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">
|
<thead style="border-width: 2px">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" v-for="column in columns"
|
<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;' : '')">
|
:style="'vertical-align: top; text-align: center;' + (column.filter === 'dateRange' ? 'min-width: 260px;' : '')">
|
||||||
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
|
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
|
||||||
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
||||||
@@ -169,6 +159,10 @@ Vue.component('tt-table', {
|
|||||||
<tt-select v-else-if="column.filter === 'select'"
|
<tt-select v-else-if="column.filter === 'select'"
|
||||||
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||||
v-model="filters[column.key]"></tt-select>
|
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>
|
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -181,16 +175,18 @@ Vue.component('tt-table', {
|
|||||||
style="height: 150px">
|
style="height: 150px">
|
||||||
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="(row) in (ssr === false ? computedRows : rows)">
|
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
|
||||||
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
||||||
@click="$emit('row-click', row)"
|
@click="$emit('row-click', row)"
|
||||||
>
|
>
|
||||||
<template v-for="(column, key) in columns">
|
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
|
||||||
<td :class="{ 'text-center': column.filter === 'iconSelect', [columns[key].class]: true }">
|
<td :class="{ 'text-center': column.filter === 'iconSelect', [columns[key].class]: true }">
|
||||||
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
|
<!-- 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))"
|
<i v-if="key === Object.keys(columns)[0] &&
|
||||||
@click.stop="toggleExpand(row.id)"
|
($scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row)) || hiddenColumns.length > 0)"
|
||||||
:class="isExpanded(row.id) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
|
|
||||||
|
@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>
|
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
|
||||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||||
<span
|
<span
|
||||||
@@ -199,13 +195,35 @@ Vue.component('tt-table', {
|
|||||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
: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>
|
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||||
<span v-else
|
<span v-else
|
||||||
v-html="row[key] === null || typeof row[key] === 'undefined' ? null : row[key]?.toString()?.replace('\\n', '<br>')"></span>
|
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||||
|
|
||||||
</slot>
|
</slot>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="isExpanded(row.id) && $scopedSlots.expandedRow">
|
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
|
||||||
<td :colspan="Object.keys(columns).length">
|
<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 )">
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<slot name="expandedRow" :row="row"></slot>
|
<slot name="expandedRow" :row="row"></slot>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -234,7 +252,6 @@ Vue.component('tt-table', {
|
|||||||
return {
|
return {
|
||||||
window: window,
|
window: window,
|
||||||
moment: window.moment,
|
moment: window.moment,
|
||||||
XLSX: window.XLSX,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
rows: null,
|
rows: null,
|
||||||
rawRows: null,
|
rawRows: null,
|
||||||
@@ -244,11 +261,13 @@ Vue.component('tt-table', {
|
|||||||
disableDebounce: false,
|
disableDebounce: false,
|
||||||
latestFetchTimestamp: null,
|
latestFetchTimestamp: null,
|
||||||
order: {
|
order: {
|
||||||
key: null,
|
key: null, order: 'asc' // default sort order
|
||||||
order: 'asc' // default sort order
|
|
||||||
},
|
},
|
||||||
expandedRows: {},
|
expandedRows: {},
|
||||||
isInitialised: false
|
isInitialised: false,
|
||||||
|
hiddenColumns: [],
|
||||||
|
originalColumnWidths: {},
|
||||||
|
originalTableWidth: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -274,6 +293,8 @@ Vue.component('tt-table', {
|
|||||||
* @async
|
* @async
|
||||||
*/
|
*/
|
||||||
async fetchData(page = 0) {
|
async fetchData(page = 0) {
|
||||||
|
this.expandedRows = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.ssr === false) {
|
if (this.ssr === false) {
|
||||||
const response = await axios.get(this.fetchUrl);
|
const response = await axios.get(this.fetchUrl);
|
||||||
@@ -332,8 +353,7 @@ Vue.component('tt-table', {
|
|||||||
} else {
|
} else {
|
||||||
await this.fetchData(page); // Directly call fetchData without debounce
|
await this.fetchData(page); // Directly call fetchData without debounce
|
||||||
}
|
}
|
||||||
},
|
}, saveSettingsToLocalStorage() {
|
||||||
saveSettingsToLocalStorage() {
|
|
||||||
if (this.isInitialised === false) return;
|
if (this.isInitialised === false) return;
|
||||||
|
|
||||||
const filters = Object.entries(this.filters).reduce((acc, [key, value]) => {
|
const filters = Object.entries(this.filters).reduce((acc, [key, value]) => {
|
||||||
@@ -353,21 +373,17 @@ Vue.component('tt-table', {
|
|||||||
filters,
|
filters,
|
||||||
paginationPerPage: this.pagination.per_page,
|
paginationPerPage: this.pagination.per_page,
|
||||||
order: this.order.key ? this.order : undefined,
|
order: this.order.key ? this.order : undefined,
|
||||||
expandedRows: this.expandedRows
|
|
||||||
}));
|
}));
|
||||||
},
|
}, parseSettingsFromLocalStorage() {
|
||||||
parseSettingsFromLocalStorage() {
|
|
||||||
const settings = JSON.parse(localStorage.getItem(`tt-table-${this.config.key}`) || '{}');
|
const settings = JSON.parse(localStorage.getItem(`tt-table-${this.config.key}`) || '{}');
|
||||||
if (settings) {
|
if (settings) {
|
||||||
this.disableDebounce = true;
|
this.disableDebounce = true;
|
||||||
this.filters = settings.filters || {};
|
this.filters = settings.filters || {};
|
||||||
this.pagination.per_page = parseInt(settings.paginationPerPage) || this.config.defaultPageSize || 10;
|
this.pagination.per_page = parseInt(settings.paginationPerPage) || this.config.defaultPageSize || 10;
|
||||||
this.order = settings.order || {key: null, order: 'asc'};
|
this.order = settings.order || {key: null, order: 'asc'};
|
||||||
this.expandedRows = settings.expandedRows || {};
|
|
||||||
}
|
}
|
||||||
return !!settings;
|
return !!settings;
|
||||||
},
|
}, setOrder(key) {
|
||||||
setOrder(key) {
|
|
||||||
if (this.order.key === key) {
|
if (this.order.key === key) {
|
||||||
// if current order is desc then set key to null
|
// if current order is desc then set key to null
|
||||||
if (this.order.order === 'desc') {
|
if (this.order.order === 'desc') {
|
||||||
@@ -382,16 +398,12 @@ Vue.component('tt-table', {
|
|||||||
this.order.key = key;
|
this.order.key = key;
|
||||||
this.order.order = 'asc';
|
this.order.order = 'asc';
|
||||||
}
|
}
|
||||||
},
|
}, getSortIconClass(key) {
|
||||||
getSortIconClass(key) {
|
|
||||||
if (this.order.key === key) {
|
if (this.order.key === key) {
|
||||||
return this.order.order === 'asc' ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
|
return this.order.order === 'asc' ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
|
||||||
}
|
}
|
||||||
return 'fa fa-sort'; // default icon when not sorted
|
return 'fa fa-sort'; // default icon when not sorted
|
||||||
},
|
}, async exportToExcel() {
|
||||||
async exportToExcel() {
|
|
||||||
|
|
||||||
// create script and await downloading: /plugins/xlsx/xlsx.min.js
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = '/plugins/xlsx/xlsx.min.js';
|
script.src = '/plugins/xlsx/xlsx.min.js';
|
||||||
@@ -400,29 +412,41 @@ Vue.component('tt-table', {
|
|||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function defaultExcelProcessor(rows) {
|
||||||
|
return rows.map(row => {
|
||||||
|
const parsedRow = {};
|
||||||
|
|
||||||
const wb = this.XLSX.utils.book_new();
|
for (const key in row) {
|
||||||
|
if (!this.columns[key]) continue;
|
||||||
|
|
||||||
let data = typeof this.config.customExcelProcessor === 'function' ? this.config.customExcelProcessor(this.rawRows) : JSON.parse(JSON.stringify(this.rawRows));
|
if (this.columns[key] && this.columns[key].filter === 'iconSelect') {
|
||||||
|
parsedRow[this.columns[key].text] = this.columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text;
|
||||||
|
} else if (this.columns[key] && this.columns[key].filter === 'date') {
|
||||||
|
parsedRow[this.columns[key].text] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
|
||||||
|
} else {
|
||||||
|
console.log(key)
|
||||||
|
parsedRow[this.columns[key].text] = row[key];
|
||||||
|
}
|
||||||
|
|
||||||
// convert all columns with date with momentjs
|
|
||||||
|
|
||||||
data = data.map(row => {
|
|
||||||
for (const key in row) {
|
|
||||||
if (this.columns[key].filter === 'date') {
|
|
||||||
row[key] = this.moment(row[key]).format('DD.MM.YYYY HH:mm');
|
|
||||||
}
|
}
|
||||||
}
|
return parsedRow;
|
||||||
return row;
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const ws = this.XLSX.utils.json_to_sheet(data);
|
const wb = this.window.XLSX.utils.book_new();
|
||||||
|
|
||||||
|
|
||||||
|
let data = typeof this.config.customExcelProcessor === 'function' ?
|
||||||
|
this.config.customExcelProcessor(JSON.parse(JSON.stringify(this.rawRows))) :
|
||||||
|
defaultExcelProcessor.call(this, JSON.parse(JSON.stringify(this.rawRows)));
|
||||||
|
|
||||||
|
const ws = this.window.XLSX.utils.json_to_sheet(data);
|
||||||
|
|
||||||
for (const cell in ws) {
|
for (const cell in ws) {
|
||||||
if (cell.startsWith('!')) continue; // Skip non-cell properties like '!ref'
|
if (cell.startsWith('!')) continue; // Skip non-cell properties like '!ref'
|
||||||
const cellValue = ws[cell].v;
|
const cellValue = ws[cell].v;
|
||||||
if (cellValue.toString().includes('\n')) {
|
if (cellValue.toString().includes('\n')) {
|
||||||
console.log('Found newline in cell:', cell, cellValue);
|
// console.log('Found newline in cell:', cell, cellValue);
|
||||||
if (!ws[cell].s) ws[cell].s = {};
|
if (!ws[cell].s) ws[cell].s = {};
|
||||||
ws[cell].s.alignment = {wrapText: true, vertical: 'center'};
|
ws[cell].s.alignment = {wrapText: true, vertical: 'center'};
|
||||||
} else {
|
} else {
|
||||||
@@ -430,26 +454,54 @@ Vue.component('tt-table', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
|
this.window.XLSX.utils.book_append_sheet(wb, ws, "Export");
|
||||||
this.XLSX.writeFile(wb, 'export.xlsx');
|
this.window.XLSX.writeFile(wb, 'export.xlsx');
|
||||||
},
|
}, resetTable() {
|
||||||
resetTable() {
|
|
||||||
this.$emit('reset-table');
|
this.$emit('reset-table');
|
||||||
this.filters = {};
|
this.filters = {};
|
||||||
this.order = {key: null, order: 'asc'};
|
this.order = {key: null, order: 'asc'};
|
||||||
this.expandedRows = {};
|
this.expandedRows = {};
|
||||||
this.disableDebounce = true;
|
this.disableDebounce = true;
|
||||||
window.notify('success', 'Filter zurückgesetzt');
|
window.notify('success', 'Filter zurückgesetzt');
|
||||||
},
|
}, toggleExpand(index) {
|
||||||
toggleExpand(index) {
|
|
||||||
this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows, index, true);
|
this.expandedRows[index] ? this.$delete(this.expandedRows, index) : this.$set(this.expandedRows, index, true);
|
||||||
},
|
}, isExpanded(index) {
|
||||||
isExpanded(index) {
|
|
||||||
return !!this.expandedRows[index];
|
return !!this.expandedRows[index];
|
||||||
|
},
|
||||||
|
async handleResponsiveColumns() {
|
||||||
|
const tableContainerComputedStyle = window.getComputedStyle(this.$refs.tableContainer);
|
||||||
|
let viewportWidth = this.$refs.tableContainer.offsetWidth -
|
||||||
|
parseInt(tableContainerComputedStyle.paddingLeft) -
|
||||||
|
parseInt(tableContainerComputedStyle.paddingRight);
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an array of columns sorted by priority
|
||||||
|
const columns = Object.values(this.columns).sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
// Iterate over all columns and check if the viewport width is smaller than the column width and hide the column
|
||||||
|
for (let i = 0; i < columns.length; i++) {
|
||||||
|
viewportWidth -= this.originalColumnWidths[columns[i].key];
|
||||||
|
|
||||||
|
if (i === 0) continue;
|
||||||
|
|
||||||
|
else if (viewportWidth < 0) {
|
||||||
|
this.hiddenColumns.push(columns[i].key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, watch: {
|
}, watch: {
|
||||||
filters: {
|
filters: {
|
||||||
handler: function (newVal, oldVal) {
|
handler: function () {
|
||||||
if (!this.isInitialised) return;
|
if (!this.isInitialised) return;
|
||||||
|
|
||||||
if (this.ssr) {
|
if (this.ssr) {
|
||||||
@@ -457,17 +509,15 @@ Vue.component('tt-table', {
|
|||||||
}
|
}
|
||||||
this.saveSettingsToLocalStorage();
|
this.saveSettingsToLocalStorage();
|
||||||
}, deep: true
|
}, deep: true
|
||||||
},
|
}, 'pagination.per_page': {
|
||||||
'pagination.per_page': {
|
|
||||||
handler: function (newVal, oldVal) {
|
handler: function (newVal, oldVal) {
|
||||||
if (!this.isInitialised) return;
|
if (!this.isInitialised) return;
|
||||||
if (newVal === oldVal) return
|
if (newVal === oldVal) return
|
||||||
|
|
||||||
this.saveSettingsToLocalStorage();
|
this.saveSettingsToLocalStorage();
|
||||||
}, deep: true
|
}, deep: true
|
||||||
},
|
}, order: {
|
||||||
order: {
|
handler: function () {
|
||||||
handler: function (newVal, oldVal) {
|
|
||||||
if (!this.isInitialised) return;
|
if (!this.isInitialised) return;
|
||||||
|
|
||||||
if (this.ssr) {
|
if (this.ssr) {
|
||||||
@@ -475,13 +525,26 @@ Vue.component('tt-table', {
|
|||||||
}
|
}
|
||||||
this.saveSettingsToLocalStorage();
|
this.saveSettingsToLocalStorage();
|
||||||
}, deep: true
|
}, deep: true
|
||||||
},
|
}, expandedRows: {
|
||||||
expandedRows: {
|
handler: function () {
|
||||||
handler: function (newVal, oldVal) {
|
|
||||||
if (!this.isInitialised) return;
|
if (!this.isInitialised) return;
|
||||||
|
|
||||||
this.saveSettingsToLocalStorage();
|
this.saveSettingsToLocalStorage();
|
||||||
}, deep: true
|
}, deep: true
|
||||||
|
},
|
||||||
|
rows: {
|
||||||
|
handler: async function () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResponsiveColumns()
|
||||||
|
})
|
||||||
|
}, deep: true
|
||||||
|
},
|
||||||
|
computedRows: {
|
||||||
|
handler: async function () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResponsiveColumns()
|
||||||
|
})
|
||||||
|
}, deep: true
|
||||||
}
|
}
|
||||||
}, computed: {
|
}, computed: {
|
||||||
/**
|
/**
|
||||||
@@ -489,18 +552,23 @@ Vue.component('tt-table', {
|
|||||||
* @return {ttTableColumnConfig} The columns configuration.
|
* @return {ttTableColumnConfig} The columns configuration.
|
||||||
*/
|
*/
|
||||||
columns() {
|
columns() {
|
||||||
|
let i = this.config.headers.length;
|
||||||
return this.config.headers.reduce((columns, column) => {
|
return this.config.headers.reduce((columns, column) => {
|
||||||
if (!column.key) {
|
if (!column.key) {
|
||||||
console.warn('WARN: tt-table: Column text or key is not defined:', column);
|
// console.warn('WARN: tt-table: Column text or key is not defined:', column);
|
||||||
return columns; // Continue to the next iteration without modifying the accumulator
|
return columns; // Continue to the next iteration without modifying the accumulator
|
||||||
}
|
}
|
||||||
|
|
||||||
columns[column.key] = {
|
columns[column.key] = {
|
||||||
text: column.text,
|
text: column.text,
|
||||||
key: column.key,
|
key: column.key,
|
||||||
filter: column.filter !== undefined ? column.filter : 'search',
|
filter: column.filter !== undefined ? column.filter : 'search',
|
||||||
filterOptions: column.filterOptions || undefined,
|
filterOptions: column.filterOptions || undefined,
|
||||||
sortable: column.sortable !== undefined ? column.sortable : true,
|
sortable: column.sortable !== undefined ? column.sortable : true,
|
||||||
class: column.class !== undefined ? column.class : ''
|
class: column.class !== undefined ? column.class : '',
|
||||||
|
prefix: column.prefix || '',
|
||||||
|
suffix: column.suffix || '',
|
||||||
|
priority: column.priority + 100 || i--
|
||||||
};
|
};
|
||||||
return columns;
|
return columns;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -526,10 +594,9 @@ Vue.component('tt-table', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return pagesArray.length === 0 ? [1] : pagesArray;
|
return pagesArray.length === 0 ? [1] : pagesArray;
|
||||||
},
|
}, computedRows() {
|
||||||
computedRows() {
|
|
||||||
if (!this.rawRows || this.ssr === true) return null;
|
if (!this.rawRows || this.ssr === true) return null;
|
||||||
console.time('Filtering and pagination');
|
// console.time('Filtering and pagination');
|
||||||
|
|
||||||
function handleRangeFilter(filter, value) {
|
function handleRangeFilter(filter, value) {
|
||||||
if (filter[0] === '<') {
|
if (filter[0] === '<') {
|
||||||
@@ -567,6 +634,7 @@ Vue.component('tt-table', {
|
|||||||
const header = headers[filter];
|
const header = headers[filter];
|
||||||
|
|
||||||
const filterValue = filters[filter];
|
const filterValue = filters[filter];
|
||||||
|
if (!filterValue) continue;
|
||||||
if (filterValue === '') continue;
|
if (filterValue === '') continue;
|
||||||
if (filterValue === "!") continue;
|
if (filterValue === "!") continue;
|
||||||
|
|
||||||
@@ -596,7 +664,7 @@ Vue.component('tt-table', {
|
|||||||
match = false;
|
match = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (header.filter === 'select' || header.filter === 'iconSelect') {
|
} else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
|
||||||
if (filterValue === '') continue;
|
if (filterValue === '') continue;
|
||||||
if (filterValue !== row[header.key].toString()) {
|
if (filterValue !== row[header.key].toString()) {
|
||||||
match = false;
|
match = false;
|
||||||
@@ -644,7 +712,7 @@ Vue.component('tt-table', {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd('Filtering and pagination');
|
// console.timeEnd('Filtering and pagination');
|
||||||
// Pagination and slice logic
|
// Pagination and slice logic
|
||||||
this.pagination.total_pages = Math.ceil(output.length / this.pagination.per_page);
|
this.pagination.total_pages = Math.ceil(output.length / this.pagination.per_page);
|
||||||
this.pagination.filtered_available = output.length;
|
this.pagination.filtered_available = output.length;
|
||||||
@@ -656,14 +724,12 @@ Vue.component('tt-table', {
|
|||||||
return this.rows;
|
return this.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
}, async created() {
|
||||||
async created() {
|
|
||||||
if (this.config.hasOwnProperty('defaultPageSize') && this.config.defaultPageSize) {
|
if (this.config.hasOwnProperty('defaultPageSize') && this.config.defaultPageSize) {
|
||||||
this.pagination = {page: 1, per_page: this.config.defaultPageSize, total_rows: null, total_pages: 1};
|
this.pagination = {page: 1, per_page: this.config.defaultPageSize, total_rows: null, total_pages: 1};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.parseSettingsFromLocalStorage()
|
this.parseSettingsFromLocalStorage()
|
||||||
|
|
||||||
if (!this.disableInitialFetch) {
|
if (!this.disableInitialFetch) {
|
||||||
@@ -675,8 +741,24 @@ Vue.component('tt-table', {
|
|||||||
// if sticky is true then add style element to style thead sticky
|
// if sticky is true then add style element to style thead sticky
|
||||||
if (this.sticky) {
|
if (this.sticky) {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.innerHTML = `table thead th { position: sticky; top: 0; z-index: 1; background-color: white; }`;
|
// use header#topnav as top to stick to but if window is resized then check if header#topnav height is changed
|
||||||
|
|
||||||
|
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
|
||||||
|
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||||
|
style.id = 'tt-table-sticky-header';
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
const headerHeight = document.querySelector('header#topnav')?.offsetHeight || 0;
|
||||||
|
style.innerHTML = `table thead th { position: sticky; top: ${headerHeight}px; z-index: 1; background-color: white; }`;
|
||||||
|
})
|
||||||
|
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.debounce(this.handleResponsiveColumns, 100));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user