diff --git a/application/controllers/dashboard/Config.php b/application/controllers/dashboard/Config.php
index 60225fa11..ae177091e 100644
--- a/application/controllers/dashboard/Config.php
+++ b/application/controllers/dashboard/Config.php
@@ -130,7 +130,7 @@ class Config extends Auth_Controller
$this->terminateWithJsonError('override could not be saved');
}
- $this->outputJsonSuccess(array('msg' => 'override successfully stored.'));
+ $this->outputJsonSuccess(array('msg' => 'override successfully stored.', 'data' => $override_decoded));
}
public function removeWidgetFromUserOverride()
diff --git a/application/controllers/dashboard/Widget.php b/application/controllers/dashboard/Widget.php
new file mode 100644
index 000000000..f1f0614d7
--- /dev/null
+++ b/application/controllers/dashboard/Widget.php
@@ -0,0 +1,162 @@
+ 1,
+ "name" => 'Widget 1',
+ "description" => 'Das ist ein Test Widget',
+ "icon" => 'https://upload.wikimedia.org/wikipedia/commons/8/8a/Farben-Testbild.svg',
+ "file" => 'DashboardWidget/Widget1.js',
+ "arguments" => [
+ "test" => 2
+ ],
+ "size" => [
+ "width" => [ "max" => 3 ],
+ "height" => [ "max" => 3 ]
+ ]
+ ]
+ ];
+
+ public function __construct()
+ {
+ parent::__construct(
+ array(
+ 'index' => 'dashboard/benutzer:r',
+ 'getWidgetsForDashboard' => 'dashboard/benutzer:rw',
+ 'addWidgetToUserOverride' => 'dashboard/benutzer:rw',
+ 'updateWidgetsToUserOverride' => 'dashboard/benutzer:rw',
+ 'removeWidgetFromUserOverride' => 'dashboard/benutzer:rw'
+ )
+ );
+
+ $this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
+ }
+
+ public function index()
+ {
+ $widget_id = $this->input->get('id');
+
+ foreach ($this->demoData as $widget) {
+ if ($widget["id"] == $widget_id)
+ return $this->outputJsonSuccess($widget);
+ }
+ return $this->outputJsonSuccess([
+ "id" => 0,
+ "name" => 'Widget Not Found',
+ "file" => 'DashboardWidget/Default.js',
+ "arguments" => [
+ "className" => 'alert-danger',
+ "title" => 'Widget Not Found',
+ "msg" => 'The widget with the id ' . $widget_id . ' could not be found'
+ ],
+ "size" => [
+ "width" => 1,
+ "height" => 1
+ ]
+ ]);
+ }
+
+ public function getWidgetsForDashboard()
+ {
+ $db = $this->input->get('db');
+
+ $this->outputJsonSuccess($this->demoData);
+ }
+
+ public function addWidgetToUserOverride()
+ {
+ $input = json_decode($this->input->raw_input_stream);
+ $dashboard_kurzbz = $input->db;
+ $uid = $input->uid;
+ $funktion = $input->funktion;
+
+ $override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
+
+ $override_decoded = json_decode($override->override, true);
+ $widgetid = isset($input->widgetid) ? array('widgetid' => $input->widgetid)
+ : $this->DashboardLib->generateWidgetId($dashboard_kurzbz);
+
+ $override_decoded[$funktion][$widgetid['widgetid']] = $input->widget;
+
+ $override->override = json_encode($override_decoded);
+
+ $result = $this->DashboardLib->insertOrUpdateOverride($override);
+ if( isError($result) ) {
+ http_response_code(500);
+ $this->terminateWithJsonError('override could not be saved');
+ }
+
+ $this->outputJsonSuccess($widgetid['widgetid']);
+ }
+
+ public function updateWidgetsToUserOverride()
+ {
+ $input = json_decode($this->input->raw_input_stream, true);
+ $dashboard_kurzbz = $input['db'];
+ $uid = $input['uid'];
+ $funktion = $input['funktion'];
+
+ $override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
+
+ $override_decoded = json_decode($override->override, true);
+ if ($override_decoded[$funktion])
+ $override_decoded[$funktion] = array_replace_recursive($override_decoded[$funktion], $input['widgets']);
+ else
+ $override_decoded[$funktion] = $input['widgets'];
+
+ $override->override = json_encode($override_decoded);
+
+ $result = $this->DashboardLib->insertOrUpdateOverride($override);
+ if( isError($result) ) {
+ http_response_code(500);
+ $this->terminateWithJsonError('override could not be saved');
+ }
+
+ $this->outputJsonSuccess(['msg' => 'override successfully updated.']);
+ }
+
+ public function removeWidgetFromUserOverride()
+ {
+ $input = json_decode($this->input->raw_input_stream);
+ $dashboard_kurzbz = $input->db;
+ $uid = $input->uid;
+ $funktion = $input->funktion;
+ $widgetid = $input->widgetid;
+
+ $override = $this->DashboardLib->getOverride($dashboard_kurzbz, $uid);
+ if( empty($override) ) {
+ http_response_code(404);
+ $this->terminateWithJsonError('userconfig for dashboard '
+ . $dashboard_kurzbz . ' not found.');
+ }
+
+ $override_decoded = json_decode($override->override, true);
+
+ if( array_key_exists($widgetid, $override_decoded[$funktion]) )
+ {
+ unset($override_decoded[$funktion][$widgetid]);
+ }
+ else
+ {
+ http_response_code(404);
+ $this->terminateWithJsonError('widgetid ' . $widgetid . ' not found in funktion ' . $funktion);
+ }
+
+ $override->override = json_encode($override_decoded);
+ $result = $this->DashboardLib->insertOrUpdateOverride($override, $uid);
+ if( isError($result) )
+ {
+ http_response_code(500);
+ $this->terminateWithJsonError('failed to remove widget');
+ }
+ $this->outputJsonSuccess(array('msg' => 'override successfully updated.'));
+ }
+
+}
diff --git a/public/css/components/dashboard.css b/public/css/components/dashboard.css
new file mode 100644
index 000000000..91379200c
--- /dev/null
+++ b/public/css/components/dashboard.css
@@ -0,0 +1,16 @@
+.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;
+}
+@media(max-width: 577px) {
+ .draganddropcontainer {
+ grid-template-columns:repeat(2,1fr);
+ }
+}
diff --git a/public/js/components/Dashboard/Dashboard.js b/public/js/components/Dashboard/Dashboard.js
new file mode 100644
index 000000000..04976da9c
--- /dev/null
+++ b/public/js/components/Dashboard/Dashboard.js
@@ -0,0 +1,170 @@
+import DashboardSection from "./Section.js";
+import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
+
+export default {
+ props: [
+ "dashboard",
+ "apiurl"
+ ],
+ data: () => ({
+ sections: [],
+ widgets: [],
+ isLoading: 0,
+ tmpCreate: null,
+ }),
+ components: {
+ DashboardSection
+ },
+ computed: {
+ listReady() {
+ return this.widgets.length && !this.isLoading;
+ }
+ },
+ methods: {
+ widgetAdd(section_name, widget) {
+ if (!this.widgets.length) {
+ axios.get(this.apiurl + '/Widget/getWidgetsForDashboard', {params:{
+ db: this.dashboard
+ }}).then(res => {
+ //console.log(res.data.retval);
+ this.widgets = res.data.retval;
+ }).catch(err => console.error('ERROR:', err));
+ }
+ this.tmpCreate = {section_name,widget};
+ this.listModal.show();
+ },
+ widgetCreate(widget) {
+ this.isLoading = 1;
+ this.tmpCreate.widget.widget = widget;
+ axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
+ db: this.dashboard,
+ uid: 'ma0168',
+ funktion_kurzbz: this.tmpCreate.section_name,
+ widgets: [this.tmpCreate.widget]
+ }).then(result => {
+ let newId = 0;
+ let sec = result.data.retval.data.widgets[this.tmpCreate.section_name];
+ for (var i in sec) {
+ newId = i;
+ break;
+ }
+ console.log(newId);
+ this.tmpCreate.widget.id = newId;
+ this.sections.forEach(section => {
+ if (section.name == this.tmpCreate.section_name)
+ section.widgets.push(this.tmpCreate.widget);
+ });
+ }).catch(error => {
+ console.error('ERROR: ', error);
+ alert('ERROR: ' + error.response.data.retval);
+ }).finally(() => {
+ this.listModal.hide();
+ this.isLoading = 0;
+ });
+ },
+ widgetUpdate(section_name, payload) {
+ payload = payload[section_name];
+ for (var k in payload) {
+ for (var i in this.sections) {
+ if (this.sections[i].name == section_name) {
+ for (var wid in this.sections[i].widgets) {
+ if (this.sections[i].widgets[wid].id == k) {
+ payload[k] = {...this.sections[i].widgets[wid], ...payload[k]};
+ break;
+ }
+ }
+ break;
+ }
+ }
+ payload[k].widgetid = k;
+ }
+ return axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
+ db: this.dashboard,
+ uid: 'ma0168',
+ funktion_kurzbz: section_name,
+ widgets: payload
+ }).then(result => {
+ this.sections.forEach(section => {
+ if (section.name == section_name) {
+ section.widgets.forEach((widget, i) => {
+ if (payload[widget.id]) {
+ // TODO(chris): revert placement on failure
+ delete payload[widget.id].place; // TODO(chris): find out why overwriting place bugs out
+ section.widgets[i] = {...widget, ...payload[widget.id]};
+ }
+ });
+ }
+ });
+ }).catch(error => {
+ console.error('ERROR: ', error);
+ alert('ERROR: ' + error.response.data.retval);
+ });
+ },
+ widgetRemove(section_name, id) {
+ axios.post(this.apiurl + '/Config/removeWidgetFromUserOverride', {
+ db: this.dashboard,
+ uid: 'ma0168',
+ funktion_kurzbz: section_name,
+ widgetid: id
+ }).then(result => {
+ this.sections.forEach(section => {
+ if (section.name == section_name)
+ section.widgets = section.widgets.filter(widget => widget.id != id);
+ });
+ }).catch(error => {
+ console.error('ERROR: ', error);
+ alert('ERROR: ' + error.response.data.retval);
+ });
+ }
+ },
+ created() {
+ CachedWidgetLoader.setPath(this.apiurl + '/Widget');
+ axios.get(this.apiurl + '/Config', {params:{
+ db: this.dashboard,
+ uid: 'ma0168'
+ }}).then(res => {
+ //console.log(res.data.retval);
+ for (var name in res.data.retval.widgets) {
+ let widgets = [];
+ 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]);
+ }
+ this.sections.push({
+ name: name,
+ widgets: widgets
+ });
+ }
+ }).catch(err => console.error('ERROR:', err));
+ },
+ mounted() {
+ this.listModal = new bootstrap.Modal(this.$refs.widgetlist);
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
![]()
+
+
{{ widget.name }}
+
{{ widget.description }}
+
+
+
+
+
+
+
+
+
+
`
+}
\ No newline at end of file
diff --git a/public/js/components/Dashboard/Item.js b/public/js/components/Dashboard/Item.js
new file mode 100644
index 000000000..4434311ff
--- /dev/null
+++ b/public/js/components/Dashboard/Item.js
@@ -0,0 +1,138 @@
+import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
+
+export default {
+ components: {},
+ data: () => ({
+ component: '',
+ arguments: null,
+ configModal: null,
+ target: false,
+ widget: null,
+ tmpConfig: {},
+ isLoading: false,
+ hasConfig: true
+ }),
+ emits: [
+ "change",
+ "remove",
+ "dragstart",
+ "resizestart",
+ ],
+ props: [
+ "id",
+ "config",
+ "width",
+ "height",
+ "custom",
+ "hidden",
+ "editMode"
+ ],
+ computed: {
+ isResizeable() {
+ if (!this.widget)
+ return false;
+ return this.widget.size.width.max || this.widget.size.height.max;
+ },
+ ready() {
+ return this.component && this.arguments !== null
+ }
+ },
+ methods: {
+ mouseDown(e) {
+ this.target = e.target;
+ },
+ startDrag(e) {
+ if (this.$refs.dragHandle.contains(this.target)) {
+ this.$emit('dragstart', e);
+ } else if (this.isResizeable && this.$refs.resizeHandle.contains(this.target)) {
+ if (this.isResizeable)
+ this.$emit('resizestart', e);
+ else
+ e.preventDefault();
+ } else {
+ e.preventDefault();
+ }
+ },
+ openConfig() {
+ this.tmpConfig = {...this.arguments};
+ this.configModal.show();
+ },
+ setConfig(hasConfig) {
+ this.hasConfig = hasConfig;
+ },
+ changeConfig() {
+ this.isLoading = true;
+ let config = {...this.tmpConfig};
+ this.sendChangeConfig(config);
+ },
+ changeConfigManually() {
+ let config = {...this.arguments};
+ this.sendChangeConfig(config);
+ },
+ sendChangeConfig(config) {
+ for (var k in config) {
+ if (this.widget.arguments[k] == config[k]) {
+ delete config[k];
+ }
+ }
+ this.$emit('change', config);
+ }
+ },
+ watch: {
+ config() {
+ this.arguments = {...this.widget.arguments, ...this.config};
+ this.tmpConfig = {...this.arguments};
+ this.configModal.hide();
+ this.isLoading = false;
+ }
+ },
+ async created() {
+ this.widget = await CachedWidgetLoader.loadWidget(this.id);
+ let component = (await import('../' + this.widget.file)).default;
+ this.$options.components['widget' + this.widget.id] = component;
+ this.component = 'widget' + this.widget.id;
+ this.arguments = {...this.widget.arguments, ...this.config};
+ this.tmpConfig = {...this.arguments};
+ },
+ mounted() {
+ this.configModal = new bootstrap.Modal(this.$refs.config);
+ },
+ template: ``
+}
\ No newline at end of file
diff --git a/public/js/components/Dashboard/Section.js b/public/js/components/Dashboard/Section.js
new file mode 100644
index 000000000..9ea40fdc1
--- /dev/null
+++ b/public/js/components/Dashboard/Section.js
@@ -0,0 +1,417 @@
+import DashboardItem from "./Item.js";
+import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
+
+// TODO(chris): handle overflow (moving outside the box)
+export default {
+ data: () => ({
+ gridWidth: 0,
+ containerRect: {top:0,left:0},
+ changeHeight: 1,
+ movedObjects: [],
+ editMode: 0,
+ gridXLast: 0,
+ gridYLast: 0,
+ }),
+ props: [
+ "name",
+ "widgets"
+ ],
+ emits: [
+ "widgetAdd",
+ "widgetUpdate",
+ "widgetRemove"
+ ],
+ components: {
+ DashboardItem
+ },
+ computed: {
+ items() {
+ this.widgets.forEach((item,i) => item.index = i);
+ return this.widgets;
+ },
+ itemCoords() {
+ 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;
+ }
+ }
+ });
+ 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;
+ for (var i = 0; i < w; i++) {
+ for (var 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 (var i = 0; i < w; i++) {
+ for (var 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 + item.h - 1));
+ // TODO(chris): the extraline should only be present if all slots are occupied
+ return minH + this.editMode;
+ },
+ gridOccupiers() {
+ 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;
+ }
+ },
+ methods: {
+ addWidget(evt) {
+ if (evt.target != this.$refs.container || !this.editMode)
+ return;
+ const rect = this.containerRect;
+ const gridX = Math.floor(this.gridWidth * (evt.clientX - rect.left) / this.$refs.container.clientWidth) + 1;
+ const gridY = Math.floor(this.gridHeight * (evt.clientY - rect.top) / this.$refs.container.clientHeight) + 1;
+ if (this.gridOccupiers[gridY * this.gridWidth + gridX] === undefined) {
+ let widget = { widget: 1, config: {}, place: {}, custom: 1 };
+ widget.place[this.gridWidth] = {
+ x: gridX,
+ y: gridY,
+ w: 1,
+ h: 1
+ };
+ this.$emit('widgetAdd', this.name, widget);
+ }
+ },
+ 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];
+ },
+ startDrag(evt, item) {
+ 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';
+ evt.dataTransfer.setData('itemAction', 'm');
+ evt.dataTransfer.setData('itemId', item.index);
+ evt.dataTransfer.setData('itemW', this.itemCoords[item.index].w);
+ evt.dataTransfer.setData('itemH', this.itemCoords[item.index].h);
+ },
+ startResize(evt, item) {
+ 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';
+ evt.dataTransfer.setData('itemAction', 'r');
+ evt.dataTransfer.setData('itemId', item.index);
+ evt.dataTransfer.setData('itemX', this.itemCoords[item.index].x);
+ evt.dataTransfer.setData('itemY', 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 = evt.dataTransfer.getData('itemAction');
+ const rect = this.containerRect;
+ 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 = gridX + 1;
+ y = gridY + 1;
+ w = parseInt(evt.dataTransfer.getData('itemW'));
+ h = parseInt(evt.dataTransfer.getData('itemH'));
+
+ if (x + w > this.gridWidth + 1)
+ x = this.gridWidth + 1 - w;
+
+ id = evt.dataTransfer.getData('itemID');
+ this.occupyFields(id, x, y, w, h);
+
+ this.itemCoords[id].x = x;
+ this.itemCoords[id].y = y;
+ } else if (action == 'r') {
+ x = parseInt(evt.dataTransfer.getData('itemX'));
+ y = parseInt(evt.dataTransfer.getData('itemY'));
+ 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 = evt.dataTransfer.getData('itemID');
+ let widget = CachedWidgetLoader.getWidget(this.items[id].widget);
+ if (widget) {
+ let minmaxW = widget.size.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.size.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(evt) {
+ 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 = evt.dataTransfer.getData('itemId');
+
+ const action = evt.dataTransfer.getData('itemAction');
+ 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++
+ },
+ removeWidget(item, revert) {
+ if (item.custom) {
+ if (confirm('Are you sure you want to delete this widget?')) {
+ this.$emit('widgetRemove', this.name, item.id);
+ }
+ } else {
+ let update = {};
+ update[item.id] = { hidden: !revert };
+ this.updatePreset(update);
+ }
+ },
+ saveConfig(config, item) {
+ let payload = {};
+ payload[item.id] = { config };console.log(payload);
+ this.updatePreset(payload);
+ },
+ updatePreset(update) {
+ let payload = {};
+ payload[this.name] = update;
+ this.$emit('widgetUpdate', this.name, payload);
+ }
+ },
+ mounted() {
+ let self = this;
+ let cont = self.$refs.container;
+ self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
+ self.containerRect = cont.getBoundingClientRect();
+
+ window.addEventListener('resize', () => {
+ for (const child of cont.children) {
+ child.style.display = 'none';
+ }
+ self.gridWidth = window.getComputedStyle(cont).getPropertyValue('grid-template-columns').split(" ").length;
+ self.containerRect = cont.getBoundingClientRect();
+ for (const child of cont.children) {
+ child.style.display = '';
+ }
+ });
+ },
+ template: ``
+}
\ No newline at end of file
diff --git a/public/js/components/DashboardWidget/Abstract.js b/public/js/components/DashboardWidget/Abstract.js
new file mode 100644
index 000000000..8d0416e28
--- /dev/null
+++ b/public/js/components/DashboardWidget/Abstract.js
@@ -0,0 +1,12 @@
+export default {
+ props: [
+ "config",
+ "width",
+ "height",
+ "setConfig",
+ "configMode"
+ ],
+ emits: [
+ "change" // TODO(chris): do we need this?
+ ]
+}
diff --git a/public/js/components/DashboardWidget/Default.js b/public/js/components/DashboardWidget/Default.js
new file mode 100644
index 000000000..29e3880dd
--- /dev/null
+++ b/public/js/components/DashboardWidget/Default.js
@@ -0,0 +1,14 @@
+import AbstractWidget from './Abstract';
+
+export default {
+ mixins: [
+ AbstractWidget
+ ],
+ created() {
+ this.$emit('setConfig', false)
+ },
+ template: ``
+}
\ No newline at end of file
diff --git a/public/js/composables/Dashboard/CachedWidgetLoader.js b/public/js/composables/Dashboard/CachedWidgetLoader.js
new file mode 100644
index 000000000..df9ff6abe
--- /dev/null
+++ b/public/js/composables/Dashboard/CachedWidgetLoader.js
@@ -0,0 +1,29 @@
+let __widgets = {};
+let __widgetsStarted = {};
+let __path = '';
+
+export default {
+ getWidget(id) {
+ return __widgets[id];
+ },
+ loadWidget(id) {
+ if (__widgets[id])
+ return Promise.resolve(__widgets[id]);
+ if (__widgetsStarted[id])
+ return __widgetsStarted[id];
+ if (!__path)
+ return Promise.reject('Widget could not be loaded because there is no path yet!');
+
+ __widgetsStarted[id] = new Promise((resolve, reject) => {
+ axios.get(__path, {params:{id}}).then(res => {
+ __widgets[id] = res.data.retval;
+ __widgetsStarted[id] = undefined;
+ resolve(__widgets[id]);
+ }).catch(error => reject(error.response.data.retval.error));
+ });
+ return __widgetsStarted[id];
+ },
+ setPath(path) {
+ __path = path;
+ }
+}
\ No newline at end of file