Merge branch 'feature-24647/Konfigurierbare_Dashboards' into feature-25999/C4

This commit is contained in:
cgfhtw
2023-07-19 11:15:40 +02:00
26 changed files with 912 additions and 540 deletions
-1
View File
@@ -59,5 +59,4 @@ class Test extends Auth_Controller
if (!$this->_uid) show_error('User authentification failed');
}
}
+5 -7
View File
@@ -9,7 +9,7 @@ class Api extends Auth_Controller
array(
'index' => 'dashboard/admin:rw',
'getNews' => 'dashboard/benutzer:r',
'getAmpeln' => 'dashboard/benutzer:r',
'getAmpeln' => 'dashboard/benutzer:r',
)
);
@@ -34,9 +34,8 @@ class Api extends Auth_Controller
$result = $this->NewsModel->getAll($limit);
if(hasData($result))
if (hasData($result))
{
$this->outputJson(getData($result), REST_Controller::HTTP_OK);
}
else
@@ -49,21 +48,20 @@ class Api extends Auth_Controller
/**
* Get Ampeln.
*/
public function getAmpeln(){
public function getAmpeln()
{
$this->load->model('content/Ampel_model', 'AmpelModel');
$result = $this->AmpelModel->getByUser($this->_uid);
if(hasData($result))
if (hasData($result))
{
$this->outputJson(getData($result), REST_Controller::HTTP_OK);
}
else
{
$this->terminateWithJsonError('fehler entdeckt');
}
}
/**
+26 -34
View File
@@ -18,9 +18,9 @@ class Config extends Auth_Controller
'removeWidgetFromPreset' => 'dashboard/admin:rw',
'addWidgetsToUserOverride' => 'dashboard/benutzer:rw',
'removeWidgetFromUserOverride' => 'dashboard/benutzer:rw',
'Funktionen' => 'dashboard/admin:r',
'Preset' => 'dashboard/admin:r',
'PresetBatch' => 'dashboard/admin:r'
'funktionen' => 'dashboard/admin:r',
'preset' => 'dashboard/admin:r',
'presetBatch' => 'dashboard/admin:r'
)
);
@@ -29,7 +29,7 @@ class Config extends Auth_Controller
$this->load->model('ressource/Funktion_model', 'FunktionModel');
}
public function index()
public function index()
{
$dashboard_kurzbz = $this->input->get('db');
$uid = $this->AuthLib->getAuthObj()->username;
@@ -46,7 +46,7 @@ class Config extends Auth_Controller
$this->outputJsonSuccess($mergedconfig);
}
public function genWidgetId()
public function genWidgetId()
{
$dashboard_kurzbz = $this->input->get('db');
$widgetid = $this->DashboardLib->generateWidgetId($dashboard_kurzbz);
@@ -55,23 +55,22 @@ class Config extends Auth_Controller
));
}
public function addWidgetsToPreset()
public function addWidgetsToPreset()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$preset = $this->DashboardLib->getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz);
$preset = $this->DashboardLib->getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz);
$preset_decoded = json_decode($preset->preset, true);
$this->DashboardLib->addWidgetsToWidgets($preset_decoded['widgets'],
$dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$this->DashboardLib->addWidgetsToWidgets($preset_decoded['widgets'], $dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$preset->preset = json_encode($preset_decoded);
$result = $this->DashboardLib->insertOrUpdatePreset($preset);
if( isError($result) ) {
if (isError($result)) {
http_response_code(500);
$this->terminateWithJsonError('preset could not be saved');
}
@@ -87,16 +86,13 @@ class Config extends Auth_Controller
$widgetid = $input->widgetid;
$preset = $this->DashboardLib->getPreset($dashboard_kurzbz, $funktion_kurzbz);
if( $preset === null ) {
if ($preset === null) {
http_response_code(404);
$this->terminateWithJsonError('preset for dashboard '
. $dashboard_kurzbz . ' and funktion ' . $funktion_kurzbz
. ' not found.');
$this->terminateWithJsonError('preset for dashboard ' . $dashboard_kurzbz . ' and funktion ' . $funktion_kurzbz . ' not found.');
}
$preset_decoded = json_decode($preset->preset, true);
if (!$this->DashboardLib->removeWidgetFromWidgets($preset_decoded['widgets'],
$funktion_kurzbz, $widgetid))
if (!$this->DashboardLib->removeWidgetFromWidgets($preset_decoded['widgets'], $funktion_kurzbz, $widgetid))
{
http_response_code(404);
$this->terminateWithJsonError('widgetid ' . $widgetid . ' not found');
@@ -104,32 +100,31 @@ class Config extends Auth_Controller
$preset->preset = json_encode($preset_decoded);
$result = $this->DashboardLib->insertOrUpdatePreset($preset);
if( isError($result) )
if (isError($result))
{
http_response_code(500);
$this->terminateWithJsonError('failed to remove widget');
$this->terminateWithJsonError('failed to remove widget');
}
$this->outputJsonSuccess(array('msg' => 'preset successfully updated.'));
}
public function addWidgetsToUserOverride()
public function addWidgetsToUserOverride()
{
$input = json_decode($this->input->raw_input_stream);
$dashboard_kurzbz = $input->db;
$funktion_kurzbz = $input->funktion_kurzbz;
$uid = $this->AuthLib->getAuthObj()->username;
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override_decoded = json_decode($override->override, true);
$this->DashboardLib->addWidgetsToWidgets($override_decoded['widgets'],
$dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$this->DashboardLib->addWidgetsToWidgets($override_decoded['widgets'], $dashboard_kurzbz, $funktion_kurzbz, $input->widgets);
$override->override = json_encode($override_decoded);
$result = $this->DashboardLib->insertOrUpdateOverride($override);
if( isError($result) ) {
if (isError($result)) {
http_response_code(500);
$this->terminateWithJsonError('override could not be saved');
}
@@ -146,16 +141,14 @@ class Config extends Auth_Controller
$widgetid = $input->widgetid;
$override = $this->DashboardLib->getOverride($dashboard_kurzbz, $uid);
if( empty($override) ) {
if (empty($override)) {
http_response_code(404);
$this->terminateWithJsonError('userconfig for dashboard '
. $dashboard_kurzbz . ' not found.');
$this->terminateWithJsonError('userconfig for dashboard ' . $dashboard_kurzbz . ' not found.');
}
$override_decoded = json_decode($override->override, true);
if( !$this->DashboardLib->removeWidgetFromWidgets($override_decoded['widgets'],
$funktion_kurzbz, $widgetid) )
if (!$this->DashboardLib->removeWidgetFromWidgets($override_decoded['widgets'], $funktion_kurzbz, $widgetid))
{
http_response_code(404);
$this->terminateWithJsonError('widgetid ' . $widgetid . ' not found');
@@ -163,15 +156,15 @@ class Config extends Auth_Controller
$override->override = json_encode($override_decoded);
$result = $this->DashboardLib->insertOrUpdateOverride($override, $uid);
if( isError($result) )
if (isError($result))
{
http_response_code(500);
$this->terminateWithJsonError('failed to remove widget');
$this->terminateWithJsonError('failed to remove widget');
}
$this->outputJsonSuccess(array('msg' => 'override successfully updated.'));
}
public function Funktionen()
public function funktionen()
{
$funktionen = $this->FunktionModel->load();
@@ -185,7 +178,7 @@ class Config extends Auth_Controller
return $this->outputJsonSuccess(getData($funktionen) ?: []);
}
public function Preset()
public function preset()
{
$db = $this->input->get('db');
$funktion = $this->input->get('funktion');
@@ -198,7 +191,7 @@ class Config extends Auth_Controller
return $this->outputJsonSuccess(json_decode($conf->preset, true));
}
public function PresetBatch()
public function presetBatch()
{
$db = $this->input->get('db');
$funktionen = $this->input->get('funktionen');
@@ -220,5 +213,4 @@ class Config extends Auth_Controller
return $this->outputJsonSuccess($result);
}
}
@@ -12,9 +12,9 @@ class Dashboard extends Auth_Controller
parent::__construct(
array(
'index' => 'dashboard/admin:r',
'Create' => 'dashboard/admin:rw',
'Update' => 'dashboard/admin:rw',
'Delete' => 'dashboard/admin:rw'
'create' => 'dashboard/admin:rw',
'update' => 'dashboard/admin:rw',
'delete' => 'dashboard/admin:rw'
)
);
@@ -22,7 +22,7 @@ class Dashboard extends Auth_Controller
$this->load->model('dashboard/Dashboard_model', 'DashboardModel');
}
public function index()
public function index()
{
$result = $this->DashboardModel->load();
@@ -36,7 +36,7 @@ class Dashboard extends Auth_Controller
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Create()
public function create()
{
$input = $this->getPostJSON();
@@ -52,7 +52,7 @@ class Dashboard extends Auth_Controller
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Update()
public function update()
{
$input = $this->getPostJSON();
@@ -68,7 +68,7 @@ class Dashboard extends Auth_Controller
return $this->outputJsonSuccess(getData($result) ?: []);
}
public function Delete()
public function delete()
{
$input = $this->getPostJSON();
@@ -83,5 +83,4 @@ class Dashboard extends Auth_Controller
return $this->outputJsonSuccess(getData($result) ?: []);
}
}
@@ -17,6 +17,7 @@ class DashboardDemo extends Auth_Controller
parent::__construct(
array(
'index' => 'user:r',
'admin' => 'dashboard/admin:rw'
)
);
@@ -35,6 +36,13 @@ class DashboardDemo extends Auth_Controller
$this->load->view('dashboard/dashboard_demo.php', []);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
public function admin()
{
$this->load->view('dashboard/dashboard_demo_admin.php', []);
}
// -----------------------------------------------------------------------------------------------------------------
// Private methods
@@ -47,5 +55,4 @@ class DashboardDemo extends Auth_Controller
if (!$this->_uid) show_error('User authentification failed');
}
}
+7 -6
View File
@@ -23,7 +23,7 @@ class Widget extends Auth_Controller
$this->load->model('dashboard/Dashboard_Widget_model', 'DashboardWidgetModel');
}
public function index()
public function index()
{
$widget_id = $this->input->get('id');
@@ -48,7 +48,7 @@ class Widget extends Auth_Controller
return $this->outputJsonSuccess(current(getData($widget)));
}
public function getAll()
public function getAll()
{
$dashboard_id = $this->input->get('dashboard_id');
$result = $this->WidgetModel->getWithAllowedForDashboard($dashboard_id);
@@ -59,7 +59,7 @@ class Widget extends Auth_Controller
$this->outputJsonSuccess(getData($result) ?: []);
}
public function getWidgetsForDashboard()
public function getWidgetsForDashboard()
{
$db = $this->input->get('db');
$result = $this->WidgetModel->getForDashboard($db);
@@ -74,7 +74,8 @@ class Widget extends Auth_Controller
$this->outputJsonSuccess(getData($result) ?: []);
}
public function setAllowed() {
public function setAllowed()
{
$input = $this->getPostJSON();
$dashboard_id = $input->dashboard_id;
@@ -83,12 +84,12 @@ class Widget extends Auth_Controller
if ($action == 'add') {
$result = $this->DashboardWidgetModel->insert([
'dashboard_id' => $dashboard_id,
'dashboard_id' => $dashboard_id,
'widget_id' => $widget_id
]);
} elseif ($action == 'delete') {
$result = $this->DashboardWidgetModel->delete([
'dashboard_id' => $dashboard_id,
'dashboard_id' => $dashboard_id,
'widget_id' => $widget_id
]);
} else {
@@ -1,5 +1,8 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
use \stdClass as stdClass;
/**
* Description of DashboardLib
*
@@ -14,7 +17,7 @@ class DashboardLib
private $_ci; // CI instance
public function __construct($params=null)
public function __construct()
{
// Loads CI instance
$this->_ci =& get_instance();
@@ -24,28 +27,27 @@ class DashboardLib
$this->_ci->load->model('dashboard/Dashboard_Override_model', 'DashboardOverrideModel');
}
public function generateWidgetId($dashboard_kurzbz='')
public function generateWidgetId($dashboard_kurzbz = '')
{
$dashboard_kurzbz = (!empty($dashboard_kurzbz)) ? $dashboard_kurzbz
: self::DEFAULT_DASHBOARD_KURZBZ;
$widgetid_input = time() . '_' . $dashboard_kurzbz . '_'
. bin2hex(random_bytes(self::WIDGET_ID_RANDOM_BYTES));
$dashboard_kurzbz = (!empty($dashboard_kurzbz)) ? $dashboard_kurzbz : self::DEFAULT_DASHBOARD_KURZBZ;
$widgetid_input = time() . '_' . $dashboard_kurzbz . '_' . bin2hex(random_bytes(self::WIDGET_ID_RANDOM_BYTES));
$widgetid = md5($widgetid_input);
return $widgetid;
}
public function getDashboardByKurzbz($dashboard_kurzbz)
public function getDashboardByKurzbz($dashboard_kurzbz)
{
$dashboard = null;
$result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz);
if( isSuccess($result) && ($dashboards = getData($result)) )
if (hasData($result))
{
$dashboard = $dashboards[0];
return current(getData($result));
}
return $dashboard;
return null;
}
public function getMergedConfig($dashboard_id, $uid)
public function getMergedConfig($dashboard_id, $uid)
{
$defaultconfig = $this->getDefaultConfig($dashboard_id, $uid);
$userconfig = $this->getUserConfig($dashboard_id, $uid);
@@ -60,15 +62,15 @@ class DashboardLib
$res_presets = $this->_ci->DashboardPresetModel->getPresets($dashboard_id, $uid);
$defaultconfig = array();
if( isSuccess($res_presets) && hasData($res_presets) )
if (hasData($res_presets))
{
$presets = getData($res_presets);
foreach ($presets as $presetobj)
{
if( null !== ($preset = json_decode($presetobj->preset, true)) )
$preset = json_decode($presetobj->preset, true);
if (null !== $preset)
{
$defaultconfig = array_replace_recursive($defaultconfig,
$preset);
$defaultconfig = array_replace_recursive($defaultconfig, $preset);
}
}
}
@@ -79,25 +81,25 @@ class DashboardLib
public function getUserConfig($dashboard_id, $uid)
{
$res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid);
$userconfig = array();
if( isSuccess($res_userconfig) && hasData($res_userconfig) )
if (hasData($res_userconfig))
{
$data = getData($res_userconfig);
if( null !== ($decodedconfig = json_decode($data[0]->override, true)) )
$decodedconfig = json_decode(current($data)->override, true);
if (null !== $decodedconfig)
{
$userconfig = $decodedconfig;
return $decodedconfig;
}
}
return $userconfig;
return [];
}
public function getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid)
public function getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid)
{
$override = $this->getOverride($dashboard_kurzbz, $uid);
if( null !== $override ) {
return $override;
if (null !== $override) {
return $override;
}
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
@@ -110,11 +112,13 @@ class DashboardLib
return $emptyoverride;
}
public function getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz)
public function getPresetOrCreateEmptyPreset($dashboard_kurzbz, $funktion_kurzbz)
{
if ($funktion_kurzbz === self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL)
$funktion_kurzbz = null;
$preset = $this->getPreset($dashboard_kurzbz, $funktion_kurzbz);
if( null !== $preset ) {
return $preset;
if (null !== $preset) {
return $preset;
}
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
@@ -123,50 +127,49 @@ class DashboardLib
$emptypreset->dashboard_id = $dashboard->dashboard_id;
$emptypreset->funktion_kurzbz = $funktion_kurzbz;
$section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
$emptypreset->preset = '{"widgets": {"' . $funktion_kurzbz . '": {}}}';
$emptypreset->preset = '{"widgets": {"' . $section . '": {}}}';
return $emptypreset;
}
public function getPreset($dashboard_kurzbz, $section)
public function getPreset($dashboard_kurzbz, $section)
{
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$preset = null;
$funktion_kurzbz = ($section === self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL) ? null : $section;
$result = $this->_ci->DashboardPresetModel
->getPresetByDashboardAndFunktion($dashboard->dashboard_id, $funktion_kurzbz);
if( isSuccess($result) && hasData($result) && ($presets = getData($result)) )
if (hasData($result))
{
$preset = $presets[0];
return current(getData($result));
}
return $preset;
return null;
}
public function getOverride($dashboard_kurzbz, $uid)
public function getOverride($dashboard_kurzbz, $uid)
{
$dashboard = $this->getDashboardByKurzbz($dashboard_kurzbz);
$override = null;
$result = $this->_ci->DashboardOverrideModel
->getOverride($dashboard->dashboard_id, $uid);
if( isSuccess($result) && hasData($result) && ($overrides = getData($result)) )
if (hasData($result))
{
$override = $overrides[0];
return current(getData($result));
}
return $override;
return null;
}
public function insertOrUpdatePreset($preset)
public function insertOrUpdatePreset($preset)
{
if( isset($preset->preset_id) && $preset->preset_id > 0 )
if (isset($preset->preset_id) && $preset->preset_id > 0)
{
$result = $this->_ci->DashboardPresetModel->update($preset->preset_id, $preset);
} else
}
else
{
$result = $this->_ci->DashboardPresetModel->insert($preset);
}
@@ -174,12 +177,13 @@ class DashboardLib
return $result;
}
public function insertOrUpdateOverride($override)
public function insertOrUpdateOverride($override)
{
if( isset($override->override_id) && $override->override_id > 0 )
if (isset($override->override_id) && $override->override_id > 0)
{
$result = $this->_ci->DashboardOverrideModel->update($override->override_id, $override);
} else
}
else
{
$result = $this->_ci->DashboardOverrideModel->insert($override);
}
@@ -199,10 +203,10 @@ class DashboardLib
}
}
public function addWidgetToWidgets(&$widgets, $section, $widget, $widgetid)
public function addWidgetToWidgets(&$widgets, $section, $widget, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if( !isset($widgets[$section]) || !is_array($widgets[$section]) )
if (!isset($widgets[$section]) || !is_array($widgets[$section]))
{
$widgets[$section] = array();
}
@@ -210,10 +214,10 @@ class DashboardLib
$widgets[$section][$widgetid] = $widget;
}
public function removeWidgetFromWidgets(&$widgets, $section, $widgetid)
public function removeWidgetFromWidgets(&$widgets, $section, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if(isset($widgets[$section]) && isset($widgets[$section][$widgetid]) )
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if (isset($widgets[$section]) && isset($widgets[$section][$widgetid]))
{
unset($widgets[$section][$widgetid]);
if(empty($widgets[$section]) && $section !== self::USEROVERRIDE_SECTION) {
@@ -60,7 +60,7 @@ EOSQL;
* @param string funktion_kurzbz
* @return array
*/
public function getPresetByDashboardAndFunktion($dashboard_id, $funktion_kurzbz)
public function getPresetByDashboardAndFunktion($dashboard_id, $funktion_kurzbz)
{
return $this->loadWhere(array('dashboard_id' => $dashboard_id, 'funktion_kurzbz' => $funktion_kurzbz));
}
@@ -12,5 +12,4 @@ class Dashboard_Widget_model extends DB_Model
$this->pk = ['dashboard_id', 'widget_id'];
$this->hasSequence = false;
}
}
@@ -29,5 +29,4 @@ class Widget_model extends DB_Model
return $this->loadWhere(['dashboard_kurzbz' => $db]);
}
}
@@ -1,5 +1,6 @@
<?php
$this->load->view('templates/FHC-Header',
$this->load->view(
'templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
@@ -0,0 +1,32 @@
<?php
$this->load->view(
'templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
'fontawesome6' => true,
'axios027' => true,
'restclient' => true,
'vue3' => true,
'customJSModules' => ['public/js/apps/DashboardAdmin.js'],
'customCSSs' => [
'public/css/components/dashboard.css'
],
'navigationcomponent' => true
)
);
?>
<div id="main">
<core-navigation-cmpt :add-side-menu-entries="appSideMenuEntries"></core-navigation-cmpt>
<div id="content">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
</div>
<dashboard-admin dashboard="CIS" apiurl="<?= site_url('dashboard'); ?>"></dashboard-admin>
</div>
</div>
<?php $this->load->view('templates/FHC-Footer'); ?>
+2 -1
View File
@@ -1,5 +1,6 @@
<?php
$this->load->view('templates/FHC-Header',
$this->load->view(
'templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
+2 -1
View File
@@ -1,5 +1,6 @@
<?php
$this->load->view('templates/FHC-Header',
$this->load->view(
'templates/FHC-Header',
array(
'title' => 'FH-Complete',
'bootstrap5' => true,
+14 -8
View File
@@ -1,19 +1,25 @@
@import './calendar.css';
.empty-tile-hover {
height: 100%;
width: 100%;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1448 1512"><path fill="lightgray" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 344V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H248v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/></svg>');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
cursor:pointer;
}
.alert-danger .form-check-input:checked {
border-color: #842029;
background-color: #842029;
}
.draganddropcontainer {
grid-template-columns:repeat(4,1fr);
gap: 1rem;
place-items: stretch;
place-content: stretch;
:root {
--fhc-dashboard-grid-size: 4;
}
@media(max-width: 577px) {
.draganddropcontainer {
grid-template-columns:repeat(2,1fr);
:root {
--fhc-dashboard-grid-size: 2;
}
}
@@ -26,4 +32,4 @@
}
.cursor-move {
cursor: move;
}
}
-4
View File
@@ -8,9 +8,5 @@ Vue.createApp({
components: {
CoreNavigationCmpt,
CoreDashboard
/*,
"CoreFilterCmpt": CoreFilterCmpt,
"verticalsplit": verticalsplit,
"searchbar": searchbar*/
}
}).mount('#main');
@@ -1,5 +1,4 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import CoreDashboard from '../components/Dashboard/Dashboard.js';
import DashboardAdmin from '../components/Dashboard/Admin.js';
Vue.createApp({
@@ -8,11 +7,7 @@ Vue.createApp({
}),
components: {
CoreNavigationCmpt,
DashboardAdmin,
CoreDashboard/*,
"CoreFilterCmpt": CoreFilterCmpt,
"verticalsplit": verticalsplit,
"searchbar": searchbar*/
DashboardAdmin
},
mounted() {
}
+3 -3
View File
@@ -35,7 +35,7 @@ export default {
BsPrompt.popup('New Dashboard name').then(
name => {
_name = name;
return axios.post(this.apiurl + '/Dashboard/Create', {
return axios.post(this.apiurl + '/Dashboard/create', {
dashboard_kurzbz: name
})
}
@@ -51,14 +51,14 @@ export default {
},
dashboardUpdate(dashboard) {
// TODO(chris): Loading or message
axios.post(this.apiurl + '/Dashboard/Update', dashboard).then(() => {
axios.post(this.apiurl + '/Dashboard/update', dashboard).then(() => {
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
old.dashboard_kurzbz = dashboard.dashboard_kurzbz;
old.beschreibung = dashboard.beschreibung;
}).catch(err => console.error('ERROR:', err));
},
dashboardDelete(dashboard_id) {
axios.post(this.apiurl + '/Dashboard/Delete', {dashboard_id}).then(() => {
axios.post(this.apiurl + '/Dashboard/delete', {dashboard_id}).then(() => {
this.current = -1;
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
}).catch(err => console.error('ERROR:', err));
@@ -29,6 +29,7 @@ export default {
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
delete widget.custom;
widget.preset = 1;
let loading = {...widget};
loading.loading = true;
this.sections.forEach(section => {
@@ -119,7 +120,7 @@ export default {
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
this.sections = [];
this.tmpLoading = funktionen.join('###');
axios.get(this.apiurl + '/Config/PresetBatch', {params: {
axios.get(this.apiurl + '/Config/presetBatch', {params: {
db: this.dashboard,
funktionen
}}).then(res => {
@@ -142,7 +143,7 @@ export default {
}
},
created() {
axios.get(this.apiurl + '/Config/Funktionen').then(res => {
axios.get(this.apiurl + '/Config/funktionen').then(res => {
//console.log(res.data.retval);
this.funktionen = {general: 'GENERAL'};
res.data.retval.forEach(funktion => {
+7 -2
View File
@@ -70,7 +70,7 @@ export default {
if (this.sections[i].widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1})
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1,preset:1})
if (payload[k][prop])
delete payload[k][prop];
break;
@@ -126,14 +126,19 @@ export default {
//console.log(res.data.retval);
for (var name in res.data.retval.widgets) {
let widgets = [];
let remove = [];
for (var wid in res.data.retval.widgets[name]) {
res.data.retval.widgets[name][wid].id = wid;
widgets.push(res.data.retval.widgets[name][wid]);
if (res.data.retval.widgets[name][wid].custom || res.data.retval.widgets[name][wid].preset)
widgets.push(res.data.retval.widgets[name][wid]);
else
remove.push(wid);
}
this.sections.push({
name: name,
widgets: widgets
});
remove.forEach(wid => this.widgetRemove(name, wid));
}
}).catch(err => console.error('ERROR:', err));
},
+6 -5
View File
@@ -97,14 +97,15 @@ export default {
this.arguments = {...this.widget.arguments, ...this.config};
this.tmpConfig = {...this.arguments};
},
template: `<div v-if="loading">
template: `
<div v-if="loading">
<div class="d-flex justify-content-center align-items-center h-100">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</div>
<div v-else-if="!hidden || editMode" class="dashboard-item card overflow-hidden" :class="arguments ? arguments.className : ''" @mousedown="mouseDown($event)" @dragstart="startDrag($event)" :draggable="!!editMode">
<div v-else-if="!hidden || editMode" class="dashboard-item card overflow-hidden h-100" :class="arguments && arguments.className ? arguments.className : ''">
<div v-if="editMode && widget" class="card-header d-flex ps-0 pe-2">
<span ref="dragHandle" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
<span drag-action="move" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
<span class="col">{{ widget.setup.name }}</span>
<a v-if="hasConfig" class="col-auto px-1" href="#" @click.prevent="openConfig"><i class="fa-solid fa-gear"></i></a>
<a v-if="custom" class="col-auto px-1" href="#" @click.prevent="$emit('remove')">
@@ -114,7 +115,7 @@ export default {
<input class="form-check-input ms-0" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
</div>
</div>
<div v-if="ready" class="card-body overflow-hidden" :class="arguments ? arguments.bodyClass : ''">
<div v-if="ready" class="card-body overflow-hidden">
<component :is="component" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
</div>
<div v-else class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
@@ -132,7 +133,7 @@ export default {
</template>
</bs-modal>
<div v-if="editMode && isResizeable" class="card-footer d-flex justify-content-end p-0">
<span ref="resizeHandle" class="col-auto px-1 cursor-nw-resize" @dragstart.prevent="$emit('resize')"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
<span drag-action="resize" class="col-auto px-1 cursor-nw-resize"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
</div>
</div>`
}
+74 -398
View File
@@ -1,14 +1,16 @@
import BsConfirm from "../Bootstrap/Confirm.js";
import DropGrid from '../Drop/Grid.js'
import DashboardItem from "./Item.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
// TODO(chris): handle overflow (moving outside the box)
export default {
components: {
DropGrid,
DashboardItem
},
inject: {
adminMode: {
type: Boolean,
default: false
}
},
@@ -23,371 +25,43 @@ export default {
],
data() {
return {
gridWidth: 0,
changeHeight: 1,
movedObjects: [],
editMode: this.adminMode ? 1 : 0,
gridXLast: -1, // NOTE(chris): 0 based
gridYLast: -1,
dragging: 0,
dataTransfer: {},
gridAddFound: false,
gridXAdd: -1, // NOTE(chris): 0 based
gridYAdd: -1
gridWidth: 1,
editMode: this.adminMode
}
},
computed: {
items() {
this.widgets.forEach((item,i) => item.index = i);
return this.widgets;
},
itemCoords() { // NOTE(chris): 1 based
if (!this.gridWidth)
return [];
let itemCoords = this.items.map(item => item.place[this.gridWidth] || this.createItemPlacement(item));
// TODO(chris): verify positions & sizes
let occupiers = [];
let wrongPlacedItems = [];
let gridWidth = this.gridWidth;
this.items.forEach(item => {
let x = item._x !== undefined ? item._x : itemCoords[item.index].x;
let y = item._y !== undefined ? item._y : itemCoords[item.index].y;
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
// TODO(chris): check with and height params here?
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
var c = (y+j-1) * gridWidth + (x+i-1);
// NOTE(chris): check for overlaping items
if (occupiers[c] !== undefined) {
//console.log('try to add ' + item.index + ' to ' + x + '/' + y + ', but ' + occupiers[c] + ' is already there');
// NOTE(chris): remove possible other entries of this item
for (var c2 = c; c2; c2--)
if (occupiers[c2] == item.index)
occupiers[c2] = undefined;
wrongPlacedItems.push(item);
return;
}
occupiers[c] = item.index;
}
}
return this.widgets.map(item => {
return {...item, ...(item.place[this.gridWidth] || {})};
});
wrongPlacedItems.forEach(item => {
let w = item._w !== undefined ? item._w : itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : itemCoords[item.index].h;
for (var c = 0; c < occupiers.length + gridWidth; c++) {
if (occupiers[c] === undefined) {
var occupied = false, i, j;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
if (occupiers[c + i + j * gridWidth] !== undefined) {
i = w;
occupied = true;
break;
}
}
}
if (!occupied) {
item.place[gridWidth].x = c%gridWidth + 1;
item.place[gridWidth].y = Math.floor(c/gridWidth) + 1;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
occupiers[c + i + j * gridWidth] = item.index;
}
}
return;
}
}
}
});
return itemCoords;
},
gridHeight() {
if (!this.gridWidth || !this.changeHeight)
return 0;
let minH = 0;
this.itemCoords.forEach((item,i) => minH = Math.max(minH, (!this.editMode && this.items[i].hidden) ? 0 : item.y - 1 + item.h));
// TODO(chris): the extraline should only be present if all slots are occupied
if (minH == 0 && this.editMode)
return 1;
return minH + this.editMode*this.dragging;
},
gridOccupiers() { // NOTE(chris): 0 based
let occupiers = [];
let gridWidth = this.gridWidth;
this.items.forEach(item => {
let x = item._x !== undefined ? item._x : this.itemCoords[item.index].x;
let y = item._y !== undefined ? item._y : this.itemCoords[item.index].y;
let w = item._w !== undefined ? item._w : this.itemCoords[item.index].w;
let h = item._h !== undefined ? item._h : this.itemCoords[item.index].h;
for (var i = 0; i < w; i++) {
for (var j = 0; j < h; j++) {
var c = (y+j-1) * gridWidth + (x+i-1);
occupiers[c] = item.index;
}
}
});
return occupiers;
},
cssBg() {
if (!this.editMode || this.dragging || !this.gridAddFound)
return 'transparent';
let x = this.gridXAdd, y = this.gridYAdd, h = this.gridHeight-1 || 1, w = this.gridWidth-1 || 1;
return `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1448 1512"><path fill="lightgray" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 344V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H248v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/></svg>')` + (100 * x/w) + '% ' + (100 * y/h) + '%/' + (100/this.gridWidth) + '% ' + (100/this.gridHeight) + '% no-repeat;cursor:pointer';
}
},
methods: {
addWidget(evt) {
if (evt.target != this.$refs.container || !this.editMode)
return;
const rect = this.$refs.container.getBoundingClientRect();
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.gridOccupiers[gridY * this.gridWidth + gridX] === undefined) {
let widget = { widget: 1, config: {}, place: {}, custom: 1 };
widget.place[this.gridWidth] = {
x: gridX+1,
y: gridY+1,
w: 1,
h: 1
};
this.$emit('widgetAdd', this.name, widget);
checkResizeLimit(item, w, h) {
// NOTE(chris): widgets needs to be loaded for this to work
let widget = CachedWidgetLoader.getWidget(item.widget);
if (widget) {
let minmaxW = widget.setup.width;
if (minmaxW.max)
minmaxW.min = minmaxW.min || 1;
else
minmaxW = {min:minmaxW,max:minmaxW};
if (w < minmaxW.min)
w = minmaxW.min;
if (w > minmaxW.max)
w = minmaxW.max;
let minmaxH = widget.setup.height;
if (minmaxH.max)
minmaxH.min = minmaxH.min || 1;
else
minmaxH = {min:minmaxH,max:minmaxH};
if (h < minmaxH.min)
h = minmaxH.min;
if (h > minmaxH.max)
h = minmaxH.max;
}
},
createItemPlacement(item) {
// TODO(chris): create correct default placement if it is not there
item.place[this.gridWidth] = {x:1,y:1,w:1,h:1};
/*var freeList = [], nextId = 0;
this.items.forEach(item => {
if (!item.place[this.gridWidth]) {
if (!this.gridWidth) {
item.place[this.gridWidth] = {x:1,y:nextId++,w:1,h:1};
} else {
// TODO(chris): IMPLEMENT widths & heights
if (freeList[nextId])
while (freeList[++nextId]);
freeList[nextId] = 1;
item.place[this.gridWidth] = {x:(nextId%this.gridWidth)+1,y:Math.floor(nextId/this.gridWidth)+1,w:1,h:1};
}
}
});*/
return item.place[this.gridWidth];
},
onMouseMove(evt) {
if (!this.editMode || this.dragging) {
this.gridXAdd = this.gridYAdd = -1;
return;
}
const rect = this.$refs.container.getBoundingClientRect();
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.gridXAdd == gridX && this.gridYAdd == gridY)
return;
// TODO(chris): only mark it when its not occupied
this.gridXAdd = gridX;
this.gridYAdd = gridY;
this.gridAddFound = (this.gridOccupiers[gridX + gridY * this.gridWidth] === undefined);
},
onMouseLeave() {
this.gridXAdd = this.gridYAdd = -1;
this.gridAddFound = false;
},
startDrag(evt, item) {
this.dragging = 1;
this.gridXLast = -1;
this.gridYLast = -1;
item._x = this.itemCoords[item.index].x;
item._y = this.itemCoords[item.index].y;
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
this.dataTransfer = {
action: 'm',
id: item.index,
w: this.itemCoords[item.index].w,
h: this.itemCoords[item.index].h
}
},
startResize(evt, item) {
this.dragging = 1;
this.gridXLast = -1;
this.gridYLast = -1;
item._w = this.itemCoords[item.index].w;
item._h = this.itemCoords[item.index].h;
evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
this.dataTransfer = {
action: 'r',
id: item.index,
x: this.itemCoords[item.index].x,
y: this.itemCoords[item.index].y
}
},
occupyFields(id, x, y, w, h) {
var c;
while ((c = this.movedObjects.pop())) {
if (this.items[c]._y !== undefined) {
this.items[c].place[this.gridWidth].y = this.items[c]._y;
this.items[c]._y = undefined;
}
}
var move = {};
move[id] = this.items[id];
this.getOccupiedItems(x,y,w,h,move);
h = y + h;
y = 0;
for (x in move) {
if (x != id) {
c = move[x]._y !== undefined ? move[x]._y : this.itemCoords[x].y;
if (c < h)
y = Math.max(h-c, y);
}
}
for (x in move) {
if (x != id) {
this.movedObjects.push(x);
if (move[x]._y === undefined) {
move[x]._y = this.itemCoords[x].y;
}
move[x].place[this.gridWidth].y = move[x]._y + y;
}
}
},
getOccupiedItems(x, y, w, h, move) {
var i, j, c;
for (i = 0; i < w; i++) {
for (j = 0; j < h; j++) {
c = (y+j-1) * this.gridWidth + (x+i-1);
if (this.gridOccupiers[c] !== undefined && !move[this.gridOccupiers[c]]) {
move[this.gridOccupiers[c]] = this.items[this.gridOccupiers[c]];
c = this.itemCoords[this.gridOccupiers[c]];
this.getOccupiedItems(c.x, c.y + 1, c.w, c.h, move);
}
}
}
},
onDragOver(evt) {
let id, x, y, w, h;
const action = this.dataTransfer.action;
const rect = this.$refs.container.getBoundingClientRect();
const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.gridXLast == gridX && this.gridYLast == gridY)
return;
this.gridXLast = gridX;
this.gridYLast = gridY;
if (action == 'm') {
x = Math.max(gridX + 1, 1);
y = Math.max(gridY + 1, 1);
w = parseInt(this.dataTransfer.w);
h = parseInt(this.dataTransfer.h);
if (x + w > this.gridWidth + 1)
x = this.gridWidth + 1 - w;
id = this.dataTransfer.id;
this.occupyFields(id, x, y, w, h);
this.itemCoords[id].x = x;
this.itemCoords[id].y = y;
} else if (action == 'r') {
x = parseInt(this.dataTransfer.x);
y = parseInt(this.dataTransfer.y);
w = gridX + 2 - x;
h = gridY + 2 - y;
w = Math.max(1, w);
h = Math.max(1, h);
if (x + w > this.gridWidth + 1)
w = this.gridWidth + 1 - x;
id = this.dataTransfer.id;
let widget = CachedWidgetLoader.getWidget(this.items[id].widget);
if (widget) {
let minmaxW = widget.setup.width;
if (minmaxW.max)
minmaxW.min = minmaxW.min || 1;
else
minmaxW = {min:minmaxW,max:minmaxW};
if (w < minmaxW.min)
w = minmaxW.min;
if (w > minmaxW.max)
w = minmaxW.max;
let minmaxH = widget.setup.height;
if (minmaxH.max)
minmaxH.min = minmaxH.min || 1;
else
minmaxH = {min:minmaxH,max:minmaxH};
if (h < minmaxH.min)
h = minmaxH.min;
if (h > minmaxH.max)
h = minmaxH.max;
}
this.occupyFields(id, x, y, w, h);
this.itemCoords[id].w = w;
this.itemCoords[id].h = h;
}
},
onDrop() {
this.dragging = 0;
this.gridXLast = -1;
this.gridYLast = -1;
let id = 0;
let update = {};
while ((id = this.movedObjects.pop())) {
if (this.items[id]._y !== undefined) {
if (this.itemCoords[id].y != this.items[id]._y) {
update[this.items[id].id] = {place:{}};
update[this.items[id].id].place[this.gridWidth] = {y:this.itemCoords[id].y};
}
this.items[id]._y = undefined;
}
}
id = this.dataTransfer.id;
const action = this.dataTransfer.action;
update[this.items[id].id] = {place:{}};
update[this.items[id].id].place[this.gridWidth] = {};
if (action == 'm') {
if (this.items[id]._x !== undefined) {
if (this.itemCoords[id].x != this.items[id]._x) {
update[this.items[id].id].place[this.gridWidth].x = this.itemCoords[id].x;
}
this.items[id]._x = undefined;
}
if (this.items[id]._y !== undefined) {
if (this.itemCoords[id].y != this.items[id]._y) {
update[this.items[id].id].place[this.gridWidth].y = this.itemCoords[id].y;
}
this.items[id]._y = undefined;
}
} else if (action == 'r') {
update[this.items[id].id].place[this.gridWidth].w = this.itemCoords[id].w;
update[this.items[id].id].place[this.gridWidth].h = this.itemCoords[id].h;
}
if (update[this.items[id].id].place[this.gridWidth].x === undefined &&
update[this.items[id].id].place[this.gridWidth].y === undefined &&
update[this.items[id].id].place[this.gridWidth].w === undefined &&
update[this.items[id].id].place[this.gridWidth].h === undefined) {
delete update[this.items[id].id].place[this.gridWidth];
}
this.updatePreset(update);
// TODO(chris): find better way to trigger change for gridHeight
this.changeHeight++
return [w, h];
},
removeWidget(item, revert) {
if (item.custom) {
@@ -400,9 +74,32 @@ export default {
},
saveConfig(config, item) {
let payload = {};
payload[item.id] = { config };console.log(payload);
payload[item.id] = { config };
this.updatePreset(payload);
},
updatePositions(updated) {
let result = {};
updated.forEach(update => {
let item = {...update.item};
if (!item.place[this.gridWidth])
item.place[this.gridWidth] = {x: 0, y: 0, w: 1, h: 1};
delete item.x;
delete item.y;
delete item.w;
delete item.h;
if (update.x !== undefined)
item.place[this.gridWidth].x = update.x;
if (update.y !== undefined)
item.place[this.gridWidth].y = update.y;
if (update.w !== undefined)
item.place[this.gridWidth].w = update.w;
if (update.h !== undefined)
item.place[this.gridWidth].h = update.h;
result[item.id] = item;
});
this.updatePreset(result);
},
updatePreset(update) {
let payload = {};
payload[this.name] = update;
@@ -412,55 +109,34 @@ export default {
mounted() {
let self = this;
let cont = self.$refs.container;
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
window.addEventListener('resize', () => {
for (const child of cont.children) {
child.style.display = 'none';
}
self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
for (const child of cont.children) {
child.style.display = '';
}
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
});
},
template: `<div class="dashboard-section">
template: `
<div class="dashboard-section" ref="container">
<h3 class="d-flex">
<span class="col">{{name}}</span>
<button class="col-auto btn" @click.prevent="editMode = editMode ? 0 : 1"><i class="fa-solid fa-gear"></i></button>
<button class="col-auto btn" @click.prevent="editMode = !editMode"><i class="fa-solid fa-gear"></i></button>
</h3>
<div :h="gridHeight" :w="gridWidth" class="position-relative" :style="'height:0;padding-bottom:' + (gridHeight * 100/gridWidth) + '%'">
<div ref="container"
class="position-absolute top-0 left-0 w-100 h-100 draganddropcontainer"
:style="'display:grid;grid-template-rows:repeat('+gridHeight+',1fr);background:'+cssBg"
@click="addWidget($event)"
@drop="onDrop($event, 1)"
@dragover.prevent="onDragOver"
@dragenter.prevent
@mousemove="onMouseMove"
@mouseleave="onMouseLeave">
<dashboard-item
v-for="item in items"
:key="item.id"
:id="item.widget"
<drop-grid v-model:cols="gridWidth" :items="items" :active="editMode" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @rearrange-items="updatePositions">
<template v-slot="item">
<dashboard-item
:id="item.widget"
:loading="item.loading"
:config="item.config"
:custom="item.custom"
:hidden="item.hidden"
:editMode="editMode"
:x="itemCoords[item.index] ? itemCoords[item.index].x : -1"
:y="itemCoords[item.index] ? itemCoords[item.index].y : -1"
:style="itemCoords[item.index] ? {'grid-column-start':itemCoords[item.index].x,'grid-column-end':itemCoords[item.index].x+itemCoords[item.index].w,'grid-row-start':itemCoords[item.index].y,'grid-row-end':itemCoords[item.index].y+itemCoords[item.index].h} : {}"
:width="itemCoords[item.index] ? itemCoords[item.index].w : 0"
:height="itemCoords[item.index] ? itemCoords[item.index].h : 0"
@dragstart="startDrag($event, item)"
@resizestart="startResize($event, item)"
:config="item.config"
:custom="item.custom"
:hidden="item.hidden"
:editMode="editMode"
@change="saveConfig($event, item)"
@remove="removeWidget(item, $event)">
</dashboard-item>
</div>
</div>
</template>
<template #empty-tile-hover="{x,y}">
<div class="empty-tile-hover" @click="$emit('widgetAdd', name, { widget: 1, config: {}, place: {[gridWidth]: {x,y,w:1,h:1}}, custom: 1 })"></div>
</template>
</drop-grid>
</div>`
}
}
+377
View File
@@ -0,0 +1,377 @@
// TODO(chris): Comments
import GridItem from './Grid/Item.js';
import GridLogic from '../../composables/GridLogic.js';
const MODE_IDLE = 0;
const MODE_MOVE = 1;
const MODE_RESIZE = 2;
export default {
components: {
GridItem
},
inject: {
},
props: {
cols: Number,
items: Array,
resizeLimit: Function,
active: {
type: Boolean,
default: true
},
marginForExtraRow: {
type: Number,
default: 0
}
},
emits: [
"rearrangeItems",
"newItem"
],
data() {
return {
x: -1,
y: -1,
mode: MODE_IDLE,
grid: null,
dragGrid: null,
permUpdates: [],
positionUpdates: null,
fixedPositionUpdates: null,
draggedOffset: [0,0],
draggedItem: null,
additionalRow: null
}
},
computed: {
rows() {
if ((this.mode == MODE_MOVE || this.mode == MODE_RESIZE) && this.dragGrid)
return this.dragGrid.h;
return this.grid ? this.grid.h : 1;
},
gridStyle() {
const addH = this.active ? this.marginForExtraRow : 0;
return {
'--fhc-dg-row-height': 100/(this.rows + addH) + '%',
'--fhc-dg-col-width': 100/this.cols + '%',
'padding-bottom': 100 * (this.rows + addH)/this.cols + '%'
}
},
indexedItems() {
return this.items.map(
(item, index) => {
return {
index: index,
x: item.x,
y: item.y,
w: item.w,
h: item.h,
weight: item.weight || 0,
data: item
}
}
);
},
prePlacedItems() {
if (!this.fixedPositionUpdates)
return this.indexedItems;
return this.indexedItems.map(item => {
if (!this.fixedPositionUpdates[item.index])
return item;
return {
index: item.index,
weight: item.weight,
data: item.data,
x: this.fixedPositionUpdates[item.index].x === undefined ? item.x : this.fixedPositionUpdates[item.index].x,
y: this.fixedPositionUpdates[item.index].y === undefined ? item.y : this.fixedPositionUpdates[item.index].y,
w: this.fixedPositionUpdates[item.index].w === undefined ? item.w : this.fixedPositionUpdates[item.index].w,
h: this.fixedPositionUpdates[item.index].h === undefined ? item.h : this.fixedPositionUpdates[item.index].h
};
});
},
placedItems() {
if (!this.positionUpdates)
return this.prePlacedItems;
return this.prePlacedItems.map(item => {
if (!this.positionUpdates[item.index])
return item;
return {
index: item.index,
weight: item.weight,
data: item.data,
x: this.positionUpdates[item.index].x === undefined ? item.x : this.positionUpdates[item.index].x,
y: this.positionUpdates[item.index].y === undefined ? item.y : this.positionUpdates[item.index].y,
w: this.positionUpdates[item.index].w === undefined ? item.w : this.positionUpdates[item.index].w,
h: this.positionUpdates[item.index].h === undefined ? item.h : this.positionUpdates[item.index].h
};
});
},
showEmptyTileHover() {
if (!this.active || !this.grid || this.mode != MODE_IDLE || this.x < 0 || this.y < 0 || this.x >= this.cols || this.y >= this.rows)
return false;
return this.grid.isFreeSlot(this.x, this.y);
}
},
watch: {
active(active) {
if (!active)
this.dragCancel();
},
cols() {
this.dragCancel();
},
indexedItems: {
handler(value) {
this.dragCancel();
const updated = this.createNewGrid(value);
this.fixedPositionUpdates = updated;
if (updated.length)
this.$emit('rearrangeItems', updated.filter(v => v));
},
immediate: true,
deep: true
}
},
methods: {
createNewGrid(items) {
this.grid = new GridLogic(this.cols);
const result = [];
[...items].sort((a, b) => a.weight > b.weight).forEach(item => {
if (item.x + item.w > this.cols) {
let targetW = this.cols-item.x,
targetX = undefined;
if (this.resizeLimit) {
[targetW] = this.resizeLimit(item.data, targetW, item.h);
}
if (targetW < 1)
targetW = 1;
if (targetW > this.cols)
targetW = this.cols;
if (item.x + targetW > this.cols) {
targetX = this.cols - targetW;
}
if (targetW == item.w)
targetW = undefined;
result[item.index] = {
item: item.data,
x: targetX,
w: targetW
};
}
item.frame = this.grid.getItemFrame(item);
this.convertGridResultToUpdate(this.grid.add(item), result, items);
});
this.grid.clearWeights();
return result;
},
convertGridResultToUpdate(input, output, baseArray) {
if (!input)
return;
if (!baseArray)
baseArray = this.indexedItems;
input.forEach(item => {
let result = {
item: baseArray[item.index].data
};
if (item.x !== undefined)
result.x = item.x;
if (item.y !== undefined)
result.y = item.y;
if (item.w !== undefined)
result.w = item.w;
if (item.h !== undefined)
result.h = item.h;
output[item.index] = result;
});
},
mouseLeave() {
if (this.mode == MODE_IDLE) {
this.x = -1;
this.y = -1;
if (this.additionalRow !== null) {
this.grid.h = this.additionalRow;
this.additionalRow = null;
}
}
},
updateCursor(evt) {
if (!this.active) {
this.x = this.y = -1;
return false;
}
const addH = this.active ? this.marginForExtraRow : 0;
const rect = this.$refs.container.getBoundingClientRect();
if (!evt.clientX && !evt.clientY && evt.touches) {
evt.clientX = evt.touches[0].clientX;
evt.clientY = evt.touches[0].clientY;
}
const gridX = Math.floor(this.cols * (evt.clientX - rect.left) / this.$refs.container.clientWidth);
const gridY = Math.floor((this.rows + addH) * (evt.clientY - rect.top) / this.$refs.container.clientHeight);
if (this.x == gridX && this.y == gridY)
return false;
if (this.mode == MODE_IDLE) {
if (this.additionalRow === null && this.y == this.rows-1 && gridY == this.rows) {
this.additionalRow = this.grid.h;
this.grid.h += 1;
} else if (this.additionalRow !== null && gridY != this.rows - 1) {
this.grid.h = this.additionalRow;
this.additionalRow = null;
}
}
this.x = gridX;
this.y = gridY;
return true;
},
_dragStart(evt) {
if (evt.dataTransfer) {
evt.dataTransfer.setDragImage(evt.target, -99999, -99999);
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
}
},
startMove(evt, item) {
if (!this.active)
return;
this._dragStart(evt);
this.mode = MODE_MOVE;
this.updateCursor(evt);
this.draggedItem = item;
this.draggedOffset = [item.x - this.x, item.y - this.y];
},
startResize(evt, item) {
if (!this.active)
return;
this._dragStart(evt);
this.mode = MODE_RESIZE;
this.draggedItem = item;
},
dragOver(evt) {
if (!this.active)
return this.dragCancel();
if (this.updateCursor(evt)) {
switch(this.mode) {
case MODE_MOVE: {
evt.preventDefault();
this.dragGrid = new GridLogic(this.grid);
let x = this.x + this.draggedOffset[0];
let y = this.y + this.draggedOffset[1];
if (x < 0) {
this.draggedOffset[0] -= x;
x = 0;
} else if (x + this.draggedItem.w > this.cols) {
this.draggedOffset[0] += this.cols - this.draggedItem.w - x;
x = this.cols - this.draggedItem.w;
}
if (y < 0) {
this.draggedOffset[1] -= y;
y = 0;
}
this.positionUpdates = this.dragGrid.move(this.draggedItem, x, y);
break;
}
case MODE_RESIZE: {
evt.preventDefault();
this.dragGrid = new GridLogic(this.grid);
let w = Math.min(this.cols - this.draggedItem.x, Math.max(1, this.x - this.draggedItem.x + 1));
let h = Math.max(1, this.y - this.draggedItem.y + 1);
if (this.resizeLimit)
[w, h] = this.resizeLimit(this.draggedItem.data, w, h);
this.positionUpdates = this.dragGrid.resize(this.draggedItem, w, h);
break;
}
}
}
},
dragCancel() {
this.mode = MODE_IDLE;
this.positionUpdates = null;
this.draggedOffset = [0,0],
this.draggedItem = null;
},
dragEnd() {
if (this.mode == MODE_IDLE)
return;
if (!this.active || this.x < 0 || this.y < 0 || this.x >= this.cols)
return this.dragCancel();
this.mode = MODE_IDLE;
let updated = [];
this.convertGridResultToUpdate(this.positionUpdates, updated);
updated = this._updateFixedPositions(updated);
if (updated.length)
this.$emit('rearrangeItems', updated.filter(v => v));
},
_updateFixedPositions(updated) {
updated.forEach((item, index) => {
if (!this.fixedPositionUpdates[index])
this.fixedPositionUpdates[index] = item;
else
this.fixedPositionUpdates[index] = {...this.fixedPositionUpdates[index], ...item};
});
let additionalUpdates = this.createNewGrid(this.prePlacedItems);
if (additionalUpdates.length) {
// NOTE(chris): this should never happen but it's here for safety
additionalUpdates.forEach((item, index) => updated[index] = item);
return this._updateFixedPositions(updated);
}
return updated;
},
emptyTileClicked() {
this.$emit('newItem', this.x, this.y);
}
},
template: `
<div
ref="container"
class="drop-grid position-relative h-0"
:style="gridStyle"
@touchmove="dragOver"
@touchend="dragCancel"
@dragover.prevent="dragOver"
@drop="dragEnd"
@mousemove="updateCursor"
@mouseleave="mouseLeave">
<grid-item
v-for="item in placedItems"
:key="item.id"
:item="item"
@start-move="startMove"
@start-resize="startResize"
@end-drag="dragCancel"
@drop-drag="dragEnd"
class="position-absolute"
:active="active"
:style="{
top: 'calc(' + item.y + ' * var(--fhc-dg-row-height))',
left: 'calc(' + item.x + ' * var(--fhc-dg-col-width))',
width: 'calc(' + item.w + ' * var(--fhc-dg-col-width))',
height: 'calc(' + item.h + ' * var(--fhc-dg-row-height))'
}">
<template v-slot="item">
<slot v-bind="item.data"></slot>
</template>
</grid-item>
<div
v-if="showEmptyTileHover"
class="position-absolute d-flex justify-content-center align-items-center"
:style="{
cursor: 'pointer',
top: 'calc(' + y + ' * var(--fhc-dg-row-height))',
left: 'calc(' + x + ' * var(--fhc-dg-col-width))',
width: 'var(--fhc-dg-col-width)',
height: 'var(--fhc-dg-row-height)'
}"
@click="emptyTileClicked">
<slot v-bind="{x,y}" name="empty-tile-hover"></slot>
</div>
</div>`
}
+71
View File
@@ -0,0 +1,71 @@
export default {
components: {
},
inject: {
},
props: {
item: Object,
active: Boolean
},
emits: [
"startMove",
"startResize",
"endDrag",
"dropDrag"
],
data() {
return {
dragAction: '',
dragging: false
}
},
computed: {
},
methods: {
registerDragAction(evt) {
if (evt.target.hasAttribute('drag-action')) {
this.dragAction = evt.target.getAttribute('drag-action');
} else {
let parent = evt.target.closest('[drag-action]');
if (parent) {
this.dragAction = parent.getAttribute('drag-action');
} else {
this.dragAction = '';
}
}
},
tryDragStart(evt, item) {
let dragAction = this.dragAction || evt.target.getAttribute('drag-action');
if (dragAction) {
this.dragging = true;
if (dragAction == 'move')
return this.$emit('startMove', evt, item);
else if (dragAction == 'resize')
return this.$emit('startResize', evt, item);
}
evt.preventDefault();
},
touchDragEnd(evt) {
if (!this.dragging)
return evt.preventDefault();
this.dragging = false;
this.$emit('dropDrag', evt);
},
test(evt) {
let dragAction = this.dragAction || evt.target.getAttribute('drag-action');
if (dragAction) {
this.dragging = true;
}
}
},
template: `
<div class="drop-grid-item"
@mousedown="registerDragAction"
@touchstart="tryDragStart($event, item)"
@touchend="touchDragEnd"
@dragstart="tryDragStart($event, item)"
@dragend="$emit('endDrag', $event)"
:draggable="active">
<slot v-bind="item"></slot>
</div>`
}
+208
View File
@@ -0,0 +1,208 @@
// TODO(chris): Comments
const DIR_UP = 0;
const DIR_LEFT = 1;
const DIR_RIGHT = 2;
const DIR_DOWN = 3;
class GridLogic {
constructor(w) {
if (w.w) {
this.w = w.w;
this.h = w.h;
this.data = [...w.data];
this.grid = [...w.grid];
} else {
this.w = w;
this.h = 1;
this.data = [];
this.grid = [];
}
}
isFreeSlot(x, y) {
const i = y*this.w + x;
return !this.grid[i] && this.grid[i] !== 0;
}
add(item, prefer) {
let occupiers = this.getItemsInFrame(item.frame);
if (!occupiers.length) {
item.frame.forEach(f => this.grid[f] = item.index);
this.data[item.index] = item;
this.h = Math.max(this.h, item.y + item.h);
return [];
} else {
const intermGrid = new GridLogic(this);
item.frame.forEach(f => intermGrid.grid[f] = -1);
const possiblities = intermGrid.tryMoving(occupiers, prefer);
if (possiblities.length) {
const bestOption = possiblities.sort((a,b) => {
if (a.cost === undefined)
a.cost = a.moves.reduce((a, v) => a * v.cost, 1);
if (b.cost === undefined)
b.cost = b.moves.reduce((a, v) => a * v.cost, 1);
return a.cost > b.cost;
});
const result = [];
bestOption[0].moves.forEach(move => {
const currItem = {...this.data[move.index]};
currItem.x += move.x;
currItem.y += move.y;
currItem.frame = this.getItemFrame(currItem);
this.h = Math.max(this.h, currItem.y + currItem.h);
this.remove(currItem);
currItem.frame.forEach(f => this.grid[f] = currItem.index);
this.data[move.index] = currItem;
result[move.index] = {
index: currItem.index,
x: currItem.x,
y: currItem.y
};
});
item.frame.forEach(f => this.grid[f] = item.index);
this.data[item.index] = item;
return result;
} else {
console.error('FATAL', "can't arrange item on grid");
}
}
}
move(item, x, y) {
if (item.x == x && item.y == y)
return [];
this.remove(item);
let prefer = undefined;
if (item.x == x) {
if (y-item.y > 0)
prefer = DIR_UP;
else
prefer = DIR_DOWN;
} else if (item.y == y) {
if (x-item.x > 0)
prefer = DIR_LEFT;
else
prefer = DIR_RIGHT;
}
const currItem = {...item};
currItem.x = x;
currItem.y = y;
currItem.frame = this.getItemFrame(currItem);
const updates = this.add(currItem, prefer);
updates[item.index] = {index: item.index, x, y};
return updates;
}
resize(item, w, h) {
if (item.w == w && item.h == h)
return [];
this.remove(item);
const currItem = {...item};
currItem.w = w;
currItem.h = h;
currItem.frame = this.getItemFrame(currItem);
const updates = this.add(currItem);
updates[item.index] = {index: item.index, w, h};
return updates;
}
tryMoving(index, prefer) {
if (Array.isArray(index)) {
index.forEach(i => this.remove({index:i}));
let possiblities = [{grid: this, moves: []}];
index.forEach(i => {
let newPoss = [];
possiblities.forEach(possiblity => {
possiblity.grid.tryMoving(i, prefer).forEach(p => {
possiblity.moves
p.moves = [...p.moves, ...possiblity.moves];
newPoss.push(p)
});
});
possiblities = newPoss;
});
return possiblities;
}
const directions = [DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT];
this.remove({index});
const weight = 1 + .2*(this.data[index].weight || 0);
return directions.reduce((result, dir) => {
let res = this.tryMovingInDirection(dir, index, 1, (prefer === dir ? .5 : 1 + dir*.1) * weight);
if (!res)
return result;
return [...result, ...res];
}, []).filter(p => p);
}
tryMovingInDirection(dir, index, amount, cost) {
const move = {index, x:0, y: 0, cost: cost};
let targetframe;
switch(dir) {
case DIR_UP:
if (this.data[index].y - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-this.w*amount);
move.y = -amount;
break;
case DIR_DOWN:
if (this.data[index].y + this.data[index].h + amount > this.h)
cost += .4;
targetframe = this.data[index].frame.map(i => i+this.w*amount);
move.y = amount;
break;
case DIR_LEFT:
if (this.data[index].x - amount < 0)
return false;
targetframe = this.data[index].frame.map(i => i-amount);
move.x = -amount;
break;
case DIR_RIGHT:
if (this.data[index].x + this.data[index].w + amount > this.w)
return false;
targetframe = this.data[index].frame.map(i => i+amount);
move.x = amount;
break;
}
const occupiers = this.getItemsInFrame(targetframe);
if (occupiers.includes(-1)) {
return this.tryMovingInDirection(dir, index, amount+1, cost);
}
const intermGrid = new GridLogic(this);
targetframe.forEach(f => intermGrid.grid[f] = -1);
if (!occupiers.length) {
return [{grid: intermGrid, moves: [move]}];
}
const possiblities = intermGrid.tryMoving(occupiers).map(possiblity => possiblity.moves.unshift(move) && possiblity);
return possiblities.length ? possiblities : false;
}
clearWeights() {
this.data.forEach(item => item.weight = undefined);
}
getItemsInFrame(frame) {
return frame.map(i => this.grid[i]).filter((v,i,a) => (v || v === 0) && a.indexOf(v) === i);
}
remove(item) {
this.grid = this.grid.map(i => i != item.index ? i : undefined);
}
getItemFrame(item) {
const frame = [];
for (let i = 0; i < item.w; i++)
for (let j = 0; j < item.h; j++)
frame.push(i + item.x + (j + item.y) * this.w);
return frame;
}
debug() {
return this.grid;
}
}
export default GridLogic;
@@ -369,6 +369,7 @@ GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE dashboard.tbl_dashboard_benutzer_over
-- Name: SEQUENCE tbl_dashboard_benutzer_override_override_id_seq; Type: ACL; Schema: dashboard; Owner: fhcomplete
--
GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_benutzer_override_override_id_seq TO web;
GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_benutzer_override_override_id_seq TO vilesci;
@@ -399,6 +400,7 @@ GRANT ALL ON SEQUENCE dashboard.tbl_dashboard_preset_preset_id_seq TO vilesci;
--
GRANT SELECT ON TABLE dashboard.tbl_dashboard_widget TO web;
GRANT SELECT,INSERT,DELETE,UPDATE ON TABLE dashboard.tbl_dashboard_widget TO vilesci;
--
@@ -406,6 +408,7 @@ GRANT SELECT ON TABLE dashboard.tbl_dashboard_widget TO web;
--
GRANT SELECT ON TABLE dashboard.tbl_widget TO web;
GRANT SELECT,INSERT,UPDATE ON TABLE dashboard.tbl_widget TO vilesci;
--