mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-23 02:28:44 +02:00
N°8641 - Dashboard editor front-end first commit for Form SDK integration.
* No dashlet edition * Dashboard are not persisted * Unable to load a dashboard from an endpoint (refresh) * Grid library need proper npm integration
This commit is contained in:
80
js/layouts/dashboard/dashboard-grid-slot.js
Normal file
80
js/layouts/dashboard/dashboard-grid-slot.js
Normal file
@@ -0,0 +1,80 @@
|
||||
class IboGridSlot extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
connectedCallback() {
|
||||
|
||||
/** @type {string} unique cell id */
|
||||
this.id = '';
|
||||
|
||||
/** @type {number} */
|
||||
this.iPosX = this.getAttribute('gs-x') ? parseInt(this.getAttribute('gs-x'), 10) : 0;
|
||||
|
||||
/** @type {number} */
|
||||
this.iPostY = this.getAttribute('gs-y') ? parseInt(this.getAttribute('gs-y'), 10) : 0;
|
||||
|
||||
/** @type {number} */
|
||||
this.iWidth = this.getAttribute('gs-w') ? parseInt(this.getAttribute('gs-w'), 10) : 1;
|
||||
|
||||
/** @type {number} */
|
||||
this.iHeight = this.getAttribute('gs-h') ? parseInt(this.getAttribute('gs-h'), 10) : 1;
|
||||
|
||||
/** @type {IboDashlet|null} contained dashlet id */
|
||||
this.sDashletId = null;
|
||||
/** @type {IboDashlet|null} contained dashlet element */
|
||||
this.oDashlet = this.querySelector('ibo-dashlet') || null;
|
||||
|
||||
/** @type {Object} freeform metadata */
|
||||
this.meta = {};
|
||||
}
|
||||
|
||||
static MakeNew(oDashletElem, aOptions = {}) {
|
||||
const oSlot = document.createElement('ibo-dashboard-grid-slot');
|
||||
oDashletElem.classList.add("grid-stack-item-content");
|
||||
|
||||
oSlot.appendChild(oDashletElem);
|
||||
oSlot.classList.add("grid-stack-item");
|
||||
oSlot.oDashlet = oDashletElem;
|
||||
|
||||
return oSlot;
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
const oDashlet = this.oDashlet;
|
||||
|
||||
const aSlotData = {
|
||||
x: this.iPosX,
|
||||
y: this.iPostY,
|
||||
w: this.iWidth,
|
||||
h: this.iHeight
|
||||
};
|
||||
|
||||
const aDashletData = oDashlet ? oDashlet.Serialize() : {};
|
||||
|
||||
return {...aSlotData, ...aDashletData};
|
||||
}
|
||||
|
||||
static observedAttributes = ['gs-x', 'gs-y', 'gs-w', 'gs-h'];
|
||||
|
||||
|
||||
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
switch (name) {
|
||||
case 'gs-x':
|
||||
this.iPosX = parseInt(newValue, 10) || 0;
|
||||
break;
|
||||
case 'gs-y':
|
||||
this.iPostY = parseInt(newValue, 10) || 0;
|
||||
break;
|
||||
case 'gs-w':
|
||||
this.iWidth = parseInt(newValue, 10) || 1;
|
||||
break;
|
||||
case 'gs-h':
|
||||
this.iHeight = parseInt(newValue, 10) || 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard-grid-slot', IboGridSlot);
|
||||
113
js/layouts/dashboard/dashboard-grid.js
Normal file
113
js/layouts/dashboard/dashboard-grid.js
Normal file
@@ -0,0 +1,113 @@
|
||||
class IboGrid extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
||||
/** @type {number} */
|
||||
this.columnsCount = 12;
|
||||
/** @type {boolean} unused yet*/
|
||||
this.bEditable = false;
|
||||
/** @type {GridStack|null} GridStack instance */
|
||||
this.oGrid = null;
|
||||
/** @type {Array<IboGridSlot>} */
|
||||
this.aSlots = [];
|
||||
|
||||
|
||||
this.SetupGrid();
|
||||
}
|
||||
|
||||
SetupGrid() {
|
||||
let aCandidateSlots = Array.from(this.querySelectorAll('ibo-dashboard-grid-slot'));
|
||||
aCandidateSlots.forEach(oSlot => {
|
||||
const aAttrs = ['gs-x', 'gs-y', 'gs-w', 'gs-h', 'id'];
|
||||
aAttrs.forEach(sAttr => {
|
||||
const sVal = oSlot.getAttribute(sAttr) || oSlot.getAttribute('data-'+sAttr);
|
||||
if (sVal !== null) {
|
||||
oSlot.setAttribute(sAttr, sVal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.oGrid = GridStack.init({
|
||||
column: this.columnsCount,
|
||||
marginTop: 8,
|
||||
marginLeft: 8,
|
||||
marginRight: 0,
|
||||
marginBottom: 0,
|
||||
resizable: {handles: 'all'},
|
||||
disableDrag: true,
|
||||
disableResize: true,
|
||||
float: true
|
||||
}, this);
|
||||
}
|
||||
|
||||
getSlots() {
|
||||
return this.oGrid.getGridItems();
|
||||
}
|
||||
|
||||
SetEditable(bIsEditable) {
|
||||
this.bEditable = bIsEditable;
|
||||
if (this.oGrid !== null) {
|
||||
this.oGrid.enableMove(bIsEditable);
|
||||
this.oGrid.enableResize(bIsEditable);
|
||||
}
|
||||
}
|
||||
|
||||
AddDashlet(sDashlet, aOptions = {}) {
|
||||
// Get the dashlet as an object
|
||||
const oParser = new DOMParser();
|
||||
const oDocument = oParser.parseFromString(sDashlet, 'text/html');
|
||||
const oDashlet = oDocument.body.firstChild;
|
||||
|
||||
const oSlot = IboGridSlot.MakeNew(oDashlet);
|
||||
|
||||
this.append(oSlot);
|
||||
|
||||
let aDefaultOptions = {
|
||||
autoPosition: !(aOptions.hasOwnProperty('x') || aOptions.hasOwnProperty('y')),
|
||||
}
|
||||
this.oGrid.makeWidget(oSlot, Object.assign(aDefaultOptions, aOptions));
|
||||
|
||||
return oDashlet.sDashletId;
|
||||
}
|
||||
|
||||
CloneDashlet(sDashletId) {
|
||||
const aSlots = this.getSlots();
|
||||
for (let oSlot of aSlots) {
|
||||
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
|
||||
const sWidth = oSlot.iWidth;
|
||||
const sHeight = oSlot.iHeight;
|
||||
|
||||
// TODO 3.3: Should ask a rendered dashlet for its content to avoid duplicating IDs
|
||||
// Still we'll position it automatically and take care of height/width
|
||||
|
||||
const sDashletContent = oSlot.oDashlet.innerHTML;
|
||||
this.AddDashlet(sDashletContent, {
|
||||
'w': sWidth,
|
||||
'h': sHeight
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoveDashlet(sDashletId) {
|
||||
const aSlots = this.getSlots();
|
||||
for (let oSlot of aSlots) {
|
||||
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
|
||||
this.oGrid.removeWidget(oSlot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Serialize() {
|
||||
const aSlots = this.getSlots();
|
||||
return aSlots.map(oSlot => {
|
||||
return oSlot.Serialize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard-grid', IboGrid);
|
||||
274
js/layouts/dashboard/dashboard.js
Normal file
274
js/layouts/dashboard/dashboard.js
Normal file
@@ -0,0 +1,274 @@
|
||||
class IboDashboard extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/** @type {string} */
|
||||
this.sId = "";
|
||||
/** @type {string} */
|
||||
this.sTitle = "";
|
||||
/** @type {boolean} unused yet */
|
||||
this.isEditable = false;
|
||||
/** @type {boolean} */
|
||||
this.bEditMode = false;
|
||||
/** @type {boolean} unused yet */
|
||||
this.bAutoRefresh = false;
|
||||
/** @type {number} unused yet */
|
||||
this.iRefreshRate = 0;
|
||||
/** @type {IboGrid|null} */
|
||||
this.oGrid = null;
|
||||
/** @type {string|null} unused yet */
|
||||
this.refreshUrl = null;
|
||||
/** @type {string|null} unused yet */
|
||||
this.csrfToken = null;
|
||||
/** @type {number} Payload schema version */
|
||||
this.schemaVersion = 2;
|
||||
|
||||
/** @type {object|null} Last saved state for cancel functionality, unused yet */
|
||||
this.aLastSavedState = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.sId = this.getAttribute("id");
|
||||
this.bEditMode = (this.getAttribute("data-edit-mode") === "edit");
|
||||
this.SetupGrid();
|
||||
|
||||
this.BindEvents();
|
||||
}
|
||||
|
||||
BindEvents() {
|
||||
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.ToggleEditMode();
|
||||
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.classList.toggle('active', this.GetEditMode());
|
||||
});
|
||||
|
||||
this.querySelector('[data-role="ibo-button"][name="save"]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.Save()
|
||||
});
|
||||
|
||||
this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO 3.3: Implement cancel functionality (reload last saved state)
|
||||
this.SetEditMode(false)
|
||||
});
|
||||
}
|
||||
SetupGrid() {
|
||||
if(this.oGrid !== null){
|
||||
return;
|
||||
}
|
||||
|
||||
this.oGrid = this.querySelector('ibo-dashboard-grid');
|
||||
}
|
||||
|
||||
|
||||
GetEditMode() {
|
||||
return this.bEditMode;
|
||||
}
|
||||
|
||||
ToggleEditMode(){
|
||||
this.SetEditMode(!this.bEditMode);
|
||||
}
|
||||
|
||||
SetEditMode(bEditMode) {
|
||||
this.bEditMode = bEditMode;
|
||||
|
||||
this.oGrid.SetEditable(this.bEditMode);
|
||||
if(this.bEditMode){
|
||||
this.aLastSavedState = this.Serialize();
|
||||
this.setAttribute("data-edit-mode", "edit");
|
||||
}
|
||||
else{
|
||||
this.setAttribute("data-edit-mode", "view");
|
||||
}
|
||||
}
|
||||
|
||||
AddNewDashlet(sDashletClass, aDashletOptions = {}) {
|
||||
const sNewDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.new_dashlet&dashlet_class='+encodeURIComponent(sDashletClass);
|
||||
fetch(sNewDashletUrl)
|
||||
.then(async data => {
|
||||
const sDashletId = this.oGrid.AddDashlet(await data.text(), aDashletOptions);
|
||||
|
||||
// TODO 3.3 Either open the dashlet form right away, or just enter edit mode
|
||||
this.EditDashlet(sDashletId);
|
||||
|
||||
const sGetashletFormUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet_form&dashlet_class='+encodeURIComponent(sDashletClass);
|
||||
fetch(sGetashletFormUrl)
|
||||
.then(async formData => {
|
||||
const sFormData = await formData.text();
|
||||
|
||||
this.HideDashletTogglers();
|
||||
this.SetDashletForm(sFormData);
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
HideDashletTogglers() {
|
||||
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
|
||||
aTogglers.classList.add('ibo-is-hidden');
|
||||
}
|
||||
|
||||
SetDashletForm(sFormData) {
|
||||
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
|
||||
oFormContainer.innerHTML = sFormData;
|
||||
oFormContainer.classList.remove('ibo-is-hidden');
|
||||
}
|
||||
|
||||
EditDashlet(sDashletId) {
|
||||
// TODO 3.3: Implement dashlet editing when forms are ready
|
||||
console.log("Edit dashlet: "+sDashletId);
|
||||
|
||||
// Create backdrop to block interactions with other dashlets
|
||||
if(this.oGrid.querySelector('.ibo-dashboard--grid--backdrop') === null) {
|
||||
const oBackdrop = document.createElement("div");
|
||||
oBackdrop.classList.add('ibo-dashboard--grid--backdrop');
|
||||
this.oGrid.append(oBackdrop);
|
||||
}
|
||||
|
||||
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'edit');
|
||||
|
||||
// Disable dashboard buttons so we need to finish this edition first
|
||||
}
|
||||
|
||||
CloneDashlet(sDashletId) {
|
||||
this.oGrid.CloneDashlet(sDashletId);
|
||||
}
|
||||
|
||||
RemoveDashlet(sDashletId) {
|
||||
this.oGrid.RemoveDashlet(sDashletId);
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
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 aSerializedGrid = this.oGrid.Serialize();
|
||||
return {
|
||||
schema_version: this.schemaVersion,
|
||||
id: this.sId,
|
||||
title: sDashboardTitle,
|
||||
refresh_rate: sDashboardRefreshRate,
|
||||
dashlets: aSerializedGrid
|
||||
};
|
||||
}
|
||||
|
||||
Save() {
|
||||
const aPayload = this.Serialize();
|
||||
// TODO 3.3: Implement saving dashboard state to server when backend is ready
|
||||
// May try to save as serialized PHP if XML format is not yet decided
|
||||
console.log(aPayload);
|
||||
|
||||
this.SetEditMode(false);
|
||||
this.aLastSavedState = aPayload;
|
||||
}
|
||||
|
||||
|
||||
async Load(aSaveState) {
|
||||
// TODO 3.3: Implement loading dashboard state either from server or local payload
|
||||
// aSaveState expected shape (example):
|
||||
// { schema_version: 1, id: "...", title: "...", refresh_rate: "...", dashlets: [ { x, y, w, h, id, type, formData, content? }, ... ] }
|
||||
|
||||
if (!aSaveState || typeof aSaveState !== 'object') {
|
||||
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;
|
||||
// keep element id attribute in sync
|
||||
this.setAttribute('id', this.sId);
|
||||
}
|
||||
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 --
|
||||
this.SetupGrid();
|
||||
if (this.oGrid) {
|
||||
this.oGrid.Clear();
|
||||
}
|
||||
|
||||
// -- 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
|
||||
if (!sDashletHtml) {
|
||||
sDashletHtml = `<div class="ibo-dashlet--placeholder">Missing dashlet content for type: ${slotPayload.type || 'unknown'}</div>`;
|
||||
}
|
||||
|
||||
// Add dashlet at requested position/size
|
||||
const aOptions = {};
|
||||
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);
|
||||
|
||||
// If payload contains dashlet metadata (id/type/formData) — apply it
|
||||
if (slotPayload.id || slotPayload.type) {
|
||||
const dashletElem = oSlot.oDashlet || oSlot.querySelector('ibo-dashlet');
|
||||
if (dashletElem) {
|
||||
if (slotPayload.id) dashletElem.sDashletId = slotPayload.id;
|
||||
if (slotPayload.type) dashletElem.type = slotPayload.type;
|
||||
if (slotPayload.formData) {
|
||||
dashletElem.ApplyFormData(slotPayload.formData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save loaded state as last saved so cancel can restore it
|
||||
this.aLastSavedState = aSaveState;
|
||||
|
||||
// Exit edit-mode: loading a previous state implies not being in edit mode by default
|
||||
this.SetEditMode(false);
|
||||
}
|
||||
|
||||
DisplayError(sMessage, sSeverity = 'error') {
|
||||
// TODO 3.3: Make this real
|
||||
this.setAttribute("data-edit-mode", "error");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashboard', IboDashboard);
|
||||
59
js/layouts/dashboard/dashlet.js
Normal file
59
js/layouts/dashboard/dashlet.js
Normal file
@@ -0,0 +1,59 @@
|
||||
class IboDashlet extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
/** @type {string} */
|
||||
this.sDashletId = this.GetDashletId();
|
||||
/** @type {string} */
|
||||
this.type = this.GetDashletType();
|
||||
/** @type {Object} */
|
||||
this.formData = {};
|
||||
/** @type {Object} unused yet */
|
||||
this.meta = {};
|
||||
|
||||
this.BindEvents();
|
||||
}
|
||||
BindEvents() {
|
||||
// Bind any dashlet-specific events here
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-edit"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.EditDashlet(this.sDashletId);
|
||||
});
|
||||
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-clone"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.CloneDashlet(this.sDashletId);
|
||||
});
|
||||
|
||||
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-remove"]')?.addEventListener('click', (e) => {
|
||||
this.closest('ibo-dashboard')?.RemoveDashlet(this.sDashletId);
|
||||
});
|
||||
}
|
||||
|
||||
GetDashletId() {
|
||||
return this.getAttribute('data-dashlet-id');
|
||||
}
|
||||
|
||||
GetDashletType() {
|
||||
return this.getAttribute("data-dashlet-type") || "";
|
||||
}
|
||||
|
||||
static MakeNew(sDashlet) {
|
||||
const oDashlet = document.createElement('ibo-dashlet');
|
||||
oDashlet.innerHTML = sDashlet;
|
||||
|
||||
return oDashlet;
|
||||
}
|
||||
|
||||
Serialize() {
|
||||
const aDashletData = {
|
||||
id: this.sDashletId,
|
||||
type: this.type,
|
||||
formData: this.formData,
|
||||
};
|
||||
|
||||
return aDashletData;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ibo-dashlet', IboDashlet);
|
||||
Reference in New Issue
Block a user