Allow dashboard to restore state (either saved state or from backend). Restore old state on edition cancel

This commit is contained in:
Stephen Abello
2026-01-15 11:51:49 +01:00
parent 27c16a782c
commit 5cfe7fa6eb
4 changed files with 85 additions and 99 deletions

View File

@@ -39,7 +39,7 @@ class IboGridSlot extends HTMLElement {
return oSlot; return oSlot;
} }
Serialize() { Serialize(bIncludeHtml = false) {
const oDashlet = this.oDashlet; const oDashlet = this.oDashlet;
const aSlotData = { const aSlotData = {
@@ -49,7 +49,7 @@ class IboGridSlot extends HTMLElement {
height: this.iHeight height: this.iHeight
}; };
const aDashletData = oDashlet ? oDashlet.Serialize() : {}; const aDashletData = oDashlet ? oDashlet.Serialize(bIncludeHtml) : {};
return { return {
...aSlotData, ...aSlotData,

View File

@@ -143,11 +143,16 @@ class IboGrid extends HTMLElement {
} }
} }
} }
Serialize() {
ClearGrid() {
this.oGrid.removeAll();
}
Serialize(bIncludeHtml = false) {
const aSlots = this.getSlots(); const aSlots = this.getSlots();
return aSlots.reduce((aAccumulator, oSlot) => { return aSlots.reduce((aAccumulator, oSlot) => {
aAccumulator[oSlot.oDashlet.sDashletId] = oSlot.Serialize(); aAccumulator[oSlot.oDashlet.sDashletId] = oSlot.Serialize(bIncludeHtml);
return aAccumulator; return aAccumulator;
}, {}); }, {});
} }

View File

@@ -49,8 +49,10 @@ class IboDashboard extends HTMLElement {
this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => { this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
// TODO 3.3: Implement cancel functionality (reload last saved state) if (this.aLastSavedState) {
this.SetEditMode(false) this.Load(this.aLastSavedState);
}
this.SetEditMode(false);
}); });
// TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching // TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching
@@ -84,7 +86,7 @@ class IboDashboard extends HTMLElement {
if(this.bEditMode){ if(this.bEditMode){
// TODO 3.3 If we are in default dashboard display, change to custom to allow editing // TODO 3.3 If we are in default dashboard display, change to custom to allow editing
// TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode // TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode
this.aLastSavedState = this.Serialize(); this.aLastSavedState = this.Serialize(true);
this.setAttribute("data-edit-mode", "edit"); this.setAttribute("data-edit-mode", "edit");
} }
else{ else{
@@ -250,11 +252,21 @@ class IboDashboard extends HTMLElement {
this.oGrid.RemoveDashlet(sDashletId); this.oGrid.RemoveDashlet(sDashletId);
} }
Serialize() { RefreshFromBackend(bCustomDashboard = false) {
const sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&custom=${bCustomDashboard ? 'true' : 'false'}`;
fetch(sLoadDashboardUrl)
.then(async oResponse => {
const oDashletData = await oResponse.json();
this.Load(oDashletData);
}
)
}
Serialize(bIncludeHtml = false) {
const sDashboardTitle = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]').value; const sDashboardTitle = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]').value;
const sDashboardRefreshRate = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]').value; const sDashboardRefreshRate = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]').value;
const aSerializedGrid = this.oGrid.Serialize(); const aSerializedGrid = this.oGrid.Serialize(bIncludeHtml);
return { return {
schema_version: this.schemaVersion, schema_version: this.schemaVersion,
id: this.sId, id: this.sId,
@@ -275,8 +287,8 @@ class IboDashboard extends HTMLElement {
const res = await data.json(); const res = await data.json();
if(res.status === 'ok') { if(res.status === 'ok') {
CombodoToast.OpenToast(res.message, 'success'); CombodoToast.OpenToast(res.message, 'success');
this.aLastSavedState = this.Serialize(true);
this.SetEditMode(false); this.SetEditMode(false);
this.aLastSavedState = aPayload;
} else { } else {
CombodoToast.OpenToast(res.message, 'error'); CombodoToast.OpenToast(res.message, 'error');
} }
@@ -284,105 +296,70 @@ class IboDashboard extends HTMLElement {
} }
async Load(aSaveState) { Load(aSaveState) {
// TODO 3.3: Implement loading dashboard state either from server or local payload try {
// aSaveState expected shape (example): // Validate schema version
// { schema_version: 1, id: "...", title: "...", refresh_rate: "...", dashlets: [ { x, y, w, h, id, type, formData, content? }, ... ] } if (aSaveState.schema_version !== this.schemaVersion) {
CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error');
return false;
}
if (!aSaveState || typeof aSaveState !== 'object') { // Update dashboard data
this.DisplayError('Invalid dashboard save state payload');
return;
}
if (aSaveState.schema_version !== this.schemaVersion) {
this.DisplayError('Load schema version mismatch');
}
// set basic props
if (aSaveState.id) {
this.sId = aSaveState.id; this.sId = aSaveState.id;
// keep element id attribute in sync this.sTitle = aSaveState.title || "";
this.setAttribute('id', this.sId); this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0;
}
if (typeof aSaveState.title !== 'undefined') {
this.sTitle = aSaveState.title;
const titleInput = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]');
if (titleInput) titleInput.value = this.sTitle;
}
if (typeof aSaveState.refresh_rate !== 'undefined') {
this.iRefreshRate = Number(aSaveState.refresh_rate) || 0;
const sel = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]');
if (sel) sel.value = aSaveState.refresh_rate;
}
// -- clear existing grid -- // Update form inputs if they exist
this.SetupGrid(); const oTitleInput = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]');
if (this.oGrid) { if (oTitleInput) {
this.oGrid.Clear(); oTitleInput.value = this.sTitle;
}
// -- iterate dashlets and re-create them --
const aDashlets = Array.isArray(aSaveState.dashlets) ? aSaveState.dashlets : [];
for (const slotPayload of aDashlets) {
// ensure minimal shape
const x = Number(slotPayload.x) || undefined;
const y = Number(slotPayload.y) || undefined;
const w = Number(slotPayload.w) || undefined;
const h = Number(slotPayload.h) || undefined;
// If the payload includes the full HTML markup for the dashlet, use it.
// Otherwise, attempt to fetch markup from server based on dashlet type.
let sDashletHtml = null;
if (slotPayload.content) {
sDashletHtml = slotPayload.content;
} else if (slotPayload.type) {
try {
// Use the same endpoint you use for new dashlet creation as a fallback.
// Your server may provide an endpoint that accepts type and returns pre-rendered HTML.
const sUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.new_dashlet&dashlet_class=' + encodeURIComponent(slotPayload.type);
const res = await fetch(sUrl);
if (res.ok) {
sDashletHtml = await res.text();
} else {
console.warn('Failed to fetch dashlet HTML for type', slotPayload.type, res.status);
}
} catch (e) {
console.warn('Error fetching dashlet HTML for type', slotPayload.type, e);
}
} }
// if still missing markup, use a placeholder const oRefreshSelect = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]');
if (!sDashletHtml) { if (oRefreshSelect) {
sDashletHtml = `<div class="ibo-dashlet--placeholder">Missing dashlet content for type: ${slotPayload.type || 'unknown'}</div>`; oRefreshSelect.value = aSaveState.refresh;
} }
// Add dashlet at requested position/size // Clear existing grid
const aOptions = {}; this.ClearGrid();
if (typeof x !== 'undefined') aOptions.x = x;
if (typeof y !== 'undefined') aOptions.y = y;
if (typeof w !== 'undefined') aOptions.w = w;
if (typeof h !== 'undefined') aOptions.h = h;
const oSlot = this.oGrid.AddDashlet(sDashletHtml, aOptions); // Load dashlets
const aDashletSlots = aSaveState.pos_dashlets || {};
// If payload contains dashlet metadata (id/type/formData) — apply it for (const [sDashletId, aDashletData] of Object.entries(aDashletSlots)) {
if (slotPayload.id || slotPayload.type) { const iPosX = aDashletData.position_x;
const dashletElem = oSlot.oDashlet || oSlot.querySelector('ibo-dashlet'); const iPosY = aDashletData.position_y;
if (dashletElem) { const iWidth = aDashletData.width;
if (slotPayload.id) dashletElem.sDashletId = slotPayload.id; const iHeight = aDashletData.height;
if (slotPayload.type) dashletElem.type = slotPayload.type; const aDashlet = aDashletData.dashlet;
if (slotPayload.formData) {
dashletElem.ApplyFormData(slotPayload.formData); // We store the dashlet component in the HTML, not only the rendered dashlet
} const sDashetHtml = aDashlet.html;
}
// Add dashlet to grid with its position and size
this.oGrid.AddDashlet(sDashetHtml, {
x: iPosX,
y: iPosY,
w: iWidth,
h: iHeight,
autoPosition: false
});
} }
// Update last saved state
this.aLastSavedState = aSaveState;
return true;
} catch (error) {
console.error('Error loading dashboard state:', error);
CombodoToast.OpenToast('Error loading dashboard state', 'error');
return false;
} }
}
// Save loaded state as last saved so cancel can restore it ClearGrid() {
this.aLastSavedState = aSaveState; this.oGrid.ClearGrid();
// Exit edit-mode: loading a previous state implies not being in edit mode by default
this.SetEditMode(false);
} }
DisplayError(sMessage, sSeverity = 'error') { DisplayError(sMessage, sSeverity = 'error') {

View File

@@ -49,14 +49,18 @@ class IboDashlet extends HTMLElement {
return oDashlet; return oDashlet;
} }
Serialize() { Serialize(bIncludeHtml = false) {
// TODO 3.3 Should we use getters ? // TODO 3.3 Should we use getters ?
const aDashletData = { let aDashletData = {
id: this.sDashletId, id: this.sDashletId,
type: this.sType, type: this.sType,
properties: JSON.parse(this.formData), properties: JSON.parse(this.formData),
}; };
if(bIncludeHtml) {
aDashletData.html = this.outerHTML;
}
return aDashletData; return aDashletData;
} }
} }