GWM : improved some ui
This commit is contained in:
parent
948553aac7
commit
460ebf3e30
@ -554,7 +554,7 @@ class ChartController extends BaseController
|
||||
$clean[] = $h;
|
||||
}
|
||||
}
|
||||
$out['palette_colors'] = array_slice($clean, 0, 8);
|
||||
$out['palette_colors'] = array_slice($clean, 0, 12);
|
||||
}
|
||||
|
||||
return $out;
|
||||
|
||||
@ -91,6 +91,7 @@ class DashboardController extends BaseController
|
||||
'chartsList' => $charts,
|
||||
'dashBoot' => [
|
||||
'dashboardId' => $id,
|
||||
'dashboardName' => (string) ($dashboard['name'] ?? 'dashboard'),
|
||||
'workspaceTheme'=> (string) ($dashboard['theme'] ?? 'system'),
|
||||
'refreshSec' => max(0, (int) ($dashboard['refresh_interval'] ?? 0)),
|
||||
'widgets' => $this->widgetsBootPayload($widgets),
|
||||
@ -102,6 +103,7 @@ class DashboardController extends BaseController
|
||||
'urls' => [
|
||||
'chartData' => rtrim(base_url(), '/') . '/chart/',
|
||||
'chartEdit' => rtrim(base_url(), '/') . '/chart/edit/',
|
||||
'chartExportBase' => rtrim(base_url(), '/') . '/chart/',
|
||||
'saveLayout' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/layout',
|
||||
'addWidget' => rtrim(base_url(), '/') . '/dashboard/' . $id . '/widget',
|
||||
'delWidget' => rtrim(base_url(), '/') . '/dashboard/widget/',
|
||||
|
||||
@ -227,6 +227,7 @@ class PublicController extends BaseController
|
||||
|
||||
$dashBoot = [
|
||||
'dashboardId' => $dashId,
|
||||
'dashboardName' => (string) ($dashboard['name'] ?? 'dashboard'),
|
||||
'workspaceTheme'=> (string) ($dashboard['theme'] ?? 'system'),
|
||||
'refreshSec' => max(0, (int) ($dashboard['refresh_interval'] ?? 0)),
|
||||
'widgets' => $this->widgetsBootPayload($widgets),
|
||||
|
||||
@ -99,6 +99,10 @@ class ChartRenderer
|
||||
$y = trim((string) ($chart['y_field'] ?? ''));
|
||||
$group = trim((string) ($chart['group_field'] ?? ''));
|
||||
$valueF = trim((string) ($chart['value_field'] ?? ''));
|
||||
// Combo charts are designed as 2-metric single-series overlays; ignore grouping.
|
||||
if ($type === 'combo') {
|
||||
$group = '';
|
||||
}
|
||||
|
||||
$colors = $this->resolveColors($display);
|
||||
$baseFont = 'Inter, system-ui, sans-serif';
|
||||
@ -152,7 +156,8 @@ class ChartRenderer
|
||||
default => 'pie',
|
||||
};
|
||||
|
||||
$plotOptions = [];
|
||||
// Keep plotOptions as an object for Apex (not [] array) when no pie config is needed.
|
||||
$plotOptions = new \stdClass();
|
||||
if ($type === 'pie' || $type === 'donut') {
|
||||
$plotOptions['pie'] = [
|
||||
'donut' => [
|
||||
@ -294,6 +299,7 @@ class ChartRenderer
|
||||
'plotOptions' => [
|
||||
'bar' => [
|
||||
'horizontal' => true,
|
||||
'isFunnel' => true,
|
||||
'barHeight' => '75%',
|
||||
'distributed' => true,
|
||||
'borderRadius' => 6,
|
||||
@ -347,7 +353,7 @@ class ChartRenderer
|
||||
}
|
||||
|
||||
if ($type === 'heatmap') {
|
||||
if ($x === '' || $y === '') {
|
||||
if ($x === '' || $y === '' || $group === '') {
|
||||
return null;
|
||||
}
|
||||
$seriesNameCol = $group !== '' ? $group : '__single__';
|
||||
@ -436,7 +442,7 @@ class ChartRenderer
|
||||
|
||||
if ($type === 'combo') {
|
||||
$y2 = trim((string) ($display['secondary_y_field'] ?? ''));
|
||||
if ($y2 === '' || $group !== '') {
|
||||
if ($y2 === '') {
|
||||
return null;
|
||||
}
|
||||
$lineData = $this->columnValues($rows, $categories, $x, $y2, '');
|
||||
@ -512,7 +518,7 @@ class ChartRenderer
|
||||
'legend' => $legend,
|
||||
'plotOptions' => ['bar' => $plotBar],
|
||||
'fill' => $fill,
|
||||
'tooltip' => $this->sharedTooltip(),
|
||||
'tooltip' => $apexType === 'bar' ? $this->pointTooltip() : $this->sharedTooltip(),
|
||||
];
|
||||
}
|
||||
|
||||
@ -530,6 +536,20 @@ class ChartRenderer
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show only hovered datapoint in tooltip (used for bar charts).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function pointTooltip(): array
|
||||
{
|
||||
return [
|
||||
'shared' => false,
|
||||
'intersect' => true,
|
||||
'theme' => 'light',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $display
|
||||
* @return array<string, mixed>
|
||||
|
||||
@ -101,7 +101,7 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
|
||||
</div>
|
||||
|
||||
<div class="row g-3 align-items-start cb-cb-main-row">
|
||||
<div class="col-lg-5">
|
||||
<div class="col-lg-4">
|
||||
<div class="cb-cb-card cb-cb-config-card">
|
||||
<div class="cb-cb-config-tabs">
|
||||
<button type="button" class="cb-cb-tab-btn" :class="{ active: configTab === 'data' }" @click="configTab = 'data'">Data</button>
|
||||
@ -266,8 +266,8 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
|
||||
</div>
|
||||
<div class="mt-2" x-show="display.color_mode === 'custom'">
|
||||
<label class="form-label small">Swatches</label>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
<template x-for="idx in [0,1,2,3,4]" :key="idx">
|
||||
<div class="cb-cb-swatch-grid">
|
||||
<template x-for="idx in [0,1,2,3,4,5,6,7,8,9,10,11]" :key="idx">
|
||||
<input type="color" class="form-control form-control-color cb-cb-color-swatch" :title="'Color ' + (idx + 1)" x-model="display.palette_colors[idx]" @input="queueRenderPreview()">
|
||||
</template>
|
||||
</div>
|
||||
@ -320,7 +320,7 @@ $bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="col-lg-8">
|
||||
<div class="cb-cb-card cb-cb-preview-card">
|
||||
<div class="cb-cb-card-h">
|
||||
<span class="fw-bold">Live preview</span>
|
||||
|
||||
@ -232,11 +232,11 @@
|
||||
ensurePaletteColors() {
|
||||
const defs = ['#36a2eb', '#4bc0c0', '#ffcd56', '#ff9f40', '#9966ff', '#2f7ed8', '#26c6da', '#f87171'];
|
||||
if (!Array.isArray(this.display.palette_colors)) {
|
||||
this.display.palette_colors = defs.slice(0, 5);
|
||||
this.display.palette_colors = defs.slice(0);
|
||||
} else {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (this.display.palette_colors[i] == null || this.display.palette_colors[i] === '') {
|
||||
this.display.palette_colors[i] = defs[i] || '#111827';
|
||||
this.display.palette_colors[i] = defs[i % defs.length] || '#111827';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -395,7 +395,8 @@
|
||||
this.valueField = setIf(this.valueField, defaultY);
|
||||
}
|
||||
if (this.chartType === 'combo') {
|
||||
this.display.secondary_y_field = setIf(this.display.secondary_y_field, nums[1] || '');
|
||||
const alt = nums.find((n) => n !== (this.yField || defaultY)) || '';
|
||||
this.display.secondary_y_field = setIf(this.display.secondary_y_field, alt || this.yField || defaultY);
|
||||
}
|
||||
if (this.chartType === 'table') {
|
||||
this.xField = '';
|
||||
@ -408,9 +409,10 @@
|
||||
canRenderPreview() {
|
||||
if (!this.savedQueryId || !this.columns.length) return false;
|
||||
if (this.chartType === 'table') return true;
|
||||
if (this.chartType === 'heatmap') return !!(this.xField && this.yField && this.groupField);
|
||||
if (this.chartType === 'gauge' || this.chartType === 'kpi_card') return !!(this.valueField || this.yField);
|
||||
if (this.chartType === 'bubble') return !!(this.xField && this.yField && this.valueField);
|
||||
if (this.chartType === 'combo') return !!(this.xField && this.yField && this.display.secondary_y_field);
|
||||
if (this.chartType === 'combo') return !!(this.xField && this.yField);
|
||||
if (this.needsXY()) return !!(this.xField && this.yField);
|
||||
return true;
|
||||
},
|
||||
@ -528,6 +530,17 @@
|
||||
try { el._apexChart.destroy(); } catch (e) { /* */ }
|
||||
el._apexChart = null;
|
||||
}
|
||||
if (this.chartType === 'heatmap' && !this.groupField) {
|
||||
this.renderError = 'Heatmap needs Group / series field (row dimension).';
|
||||
this.renderMeta = '';
|
||||
if (el) {
|
||||
el.innerHTML = '<div class="d-flex align-items-center justify-content-center h-100 text-muted small p-4">Select Group / series to render heatmap.</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.chartType === 'combo' && !this.display.secondary_y_field) {
|
||||
this.display.secondary_y_field = this.yField || '';
|
||||
}
|
||||
this.renderLoading = true;
|
||||
this.renderError = '';
|
||||
const body = new URLSearchParams();
|
||||
@ -537,7 +550,7 @@
|
||||
body.append('chart_type', this.chartType);
|
||||
body.append('x_field', this.xField);
|
||||
body.append('y_field', this.yField);
|
||||
body.append('group_field', this.groupField);
|
||||
body.append('group_field', this.chartType === 'combo' ? '' : this.groupField);
|
||||
body.append('value_field', this.valueField);
|
||||
body.append('display_config', this.displayJson());
|
||||
try {
|
||||
|
||||
@ -260,6 +260,257 @@
|
||||
return '<label class="form-label small mb-0">' + escapeHtml(lab) + '</label><input type="text" class="form-control form-control-sm" data-cb-var-key="' + escapeHtml(k) + '" value="' + escapeHtml(globalVars[k] ?? '') + '"' + req + '>';
|
||||
}
|
||||
|
||||
function sanitizeFileName(raw) {
|
||||
const s = String(raw ?? '').trim();
|
||||
const cleaned = s.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
return cleaned || 'download';
|
||||
}
|
||||
|
||||
function getDashBackgroundColor() {
|
||||
const shell = document.getElementById('cbDashShell');
|
||||
return shell && shell.classList.contains('cb-theme-dark') ? '#0f172a' : '#ffffff';
|
||||
}
|
||||
|
||||
function downloadDataUri(dataUri, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUri;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForDashChartsLoaded(timeoutMs) {
|
||||
const deadline = Date.now() + Math.max(0, timeoutMs || 0);
|
||||
const chartWidgets = Array.isArray(boot.widgets) ? boot.widgets.filter((w) => w.widget_type === 'chart' && w.id) : [];
|
||||
if (!chartWidgets.length) return true;
|
||||
|
||||
while (Date.now() <= deadline) {
|
||||
let pending = false;
|
||||
for (const w of chartWidgets) {
|
||||
const loadEl = document.getElementById('cb-load-' + w.id);
|
||||
if (loadEl && !loadEl.hidden) { pending = true; break; }
|
||||
}
|
||||
if (!pending) return true;
|
||||
await sleep(250);
|
||||
}
|
||||
// Best-effort: export anyway.
|
||||
return false;
|
||||
}
|
||||
|
||||
async function elementToPngDataUri(el, scale) {
|
||||
if (typeof html2canvas === 'undefined') {
|
||||
throw new Error('html2canvas not available');
|
||||
}
|
||||
const canvas = await html2canvas(el, {
|
||||
scale: scale || 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: getDashBackgroundColor(),
|
||||
});
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
async function exportChartPng(w) {
|
||||
const chartEl = document.getElementById('cb-chart-' + w.id);
|
||||
const loadEl = document.getElementById('cb-load-' + w.id);
|
||||
if (loadEl && !loadEl.hidden) {
|
||||
alert('Chart is still loading. Please wait a moment.');
|
||||
return;
|
||||
}
|
||||
if (!chartEl) { alert('Chart not found.'); return; }
|
||||
|
||||
const name = sanitizeFileName(w.title || w.chart_name || 'chart') + '.png';
|
||||
const apex = chartEl._apexChart;
|
||||
try {
|
||||
if (apex && typeof apex.dataURI === 'function') {
|
||||
const res = await apex.dataURI({ scale: 2, width: chartEl.offsetWidth || undefined });
|
||||
const imgURI = res && res.imgURI ? res.imgURI : null;
|
||||
if (!imgURI) throw new Error('No imgURI from Apex.');
|
||||
downloadDataUri(imgURI, name);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataUri = await elementToPngDataUri(chartEl, 2);
|
||||
downloadDataUri(dataUri, name);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Could not export PNG.');
|
||||
}
|
||||
}
|
||||
|
||||
async function exportChartPdf(w) {
|
||||
const chartEl = document.getElementById('cb-chart-' + w.id);
|
||||
const loadEl = document.getElementById('cb-load-' + w.id);
|
||||
if (loadEl && !loadEl.hidden) {
|
||||
alert('Chart is still loading. Please wait a moment.');
|
||||
return;
|
||||
}
|
||||
if (!chartEl) { alert('Chart not found.'); return; }
|
||||
|
||||
const jsPDFCtor = window.jspdf && window.jspdf.jsPDF ? window.jspdf.jsPDF : null;
|
||||
if (!jsPDFCtor) { alert('PDF export is not available.'); return; }
|
||||
const name = sanitizeFileName(w.title || w.chart_name || 'chart') + '.pdf';
|
||||
|
||||
let imgDataUri = null;
|
||||
const apex = chartEl._apexChart;
|
||||
try {
|
||||
if (apex && typeof apex.dataURI === 'function') {
|
||||
const res = await apex.dataURI({ scale: 2, width: chartEl.offsetWidth || undefined });
|
||||
imgDataUri = res && res.imgURI ? res.imgURI : null;
|
||||
} else {
|
||||
imgDataUri = await elementToPngDataUri(chartEl, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Could not export PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!imgDataUri) { alert('Could not export PDF.'); return; }
|
||||
|
||||
const rect = chartEl.getBoundingClientRect();
|
||||
const orientation = rect.width >= rect.height ? 'landscape' : 'portrait';
|
||||
const doc = new jsPDFCtor({ orientation, unit: 'pt', format: 'a4' });
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
const margin = 18;
|
||||
|
||||
let img;
|
||||
try {
|
||||
img = await new Promise((resolve, reject) => {
|
||||
const i = new Image();
|
||||
i.onload = () => resolve(i);
|
||||
i.onerror = (err) => reject(err);
|
||||
i.src = imgDataUri;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Could not export PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ratio = img.height / Math.max(1, img.width);
|
||||
let imgW = pageW - margin * 2;
|
||||
let imgH = imgW * ratio;
|
||||
if (imgH > pageH - margin * 2) {
|
||||
imgH = pageH - margin * 2;
|
||||
imgW = imgH / ratio;
|
||||
}
|
||||
const x = (pageW - imgW) / 2;
|
||||
const y = (pageH - imgH) / 2;
|
||||
doc.addImage(imgDataUri, 'PNG', x, y, imgW, imgH);
|
||||
doc.save(name);
|
||||
}
|
||||
|
||||
async function exportDashboardPdf() {
|
||||
const btn = document.getElementById('cbBtnExportDashPdf');
|
||||
const btnLabel = btn ? btn.querySelector('span:not(.material-symbols-outlined)') : null;
|
||||
const oldLabel = btnLabel ? btnLabel.textContent : '';
|
||||
if (btn) btn.disabled = true;
|
||||
if (btnLabel) btnLabel.textContent = 'Exporting...';
|
||||
|
||||
try {
|
||||
if (typeof html2canvas === 'undefined') throw new Error('html2canvas missing');
|
||||
const jsPDFCtor = window.jspdf && window.jspdf.jsPDF ? window.jspdf.jsPDF : null;
|
||||
if (!jsPDFCtor) throw new Error('jsPDF missing');
|
||||
|
||||
await waitForDashChartsLoaded(10000);
|
||||
|
||||
const pageBreaks = document.getElementById('cbChkDashPdfPageBreaks')?.checked !== false;
|
||||
const shell = document.getElementById('cbDashShell');
|
||||
const dashName = sanitizeFileName(boot.dashboardName || (shell && shell.querySelector('h1') ? shell.querySelector('h1').textContent : 'dashboard'));
|
||||
const bg = getDashBackgroundColor();
|
||||
|
||||
// Hide widget export buttons so they don't pollute the PDF.
|
||||
const exportBtns = Array.from(document.querySelectorAll('.cb-dash-widget-export'));
|
||||
const prevDisplay = exportBtns.map((el) => el.style.display);
|
||||
exportBtns.forEach((el) => { el.style.display = 'none'; });
|
||||
|
||||
try {
|
||||
if (!pageBreaks) {
|
||||
const root = document.getElementById('cbGridStack') || shell;
|
||||
const doc = new jsPDFCtor({ orientation: 'portrait', unit: 'pt', format: 'a4' });
|
||||
const canvas = await html2canvas(root, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: bg,
|
||||
});
|
||||
const imgDataUri = canvas.toDataURL('image/png');
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
const margin = 18;
|
||||
const ratio = canvas.height / Math.max(1, canvas.width);
|
||||
let imgW = pageW - margin * 2;
|
||||
let imgH = imgW * ratio;
|
||||
if (imgH > pageH - margin * 2) {
|
||||
imgH = pageH - margin * 2;
|
||||
imgW = imgH / ratio;
|
||||
}
|
||||
const x = (pageW - imgW) / 2;
|
||||
const y = (pageH - imgH) / 2;
|
||||
doc.addImage(imgDataUri, 'PNG', x, y, imgW, imgH);
|
||||
doc.save(dashName + '.pdf');
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture the current dashboard layout as-is, then slice into A4 pages.
|
||||
// This matches the on-screen view (instead of forcing "one widget per page").
|
||||
const root = document.getElementById('cbGridStack') || shell;
|
||||
const doc = new jsPDFCtor({ orientation: 'portrait', unit: 'pt', format: 'a4' });
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
const margin = 14;
|
||||
|
||||
const canvas = await html2canvas(root, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
backgroundColor: bg,
|
||||
});
|
||||
|
||||
const imgW = pageW - margin * 2;
|
||||
const scaleFactor = imgW / Math.max(1, canvas.width); // pt per px
|
||||
const availableHpt = pageH - margin * 2;
|
||||
const sliceHpx = Math.max(1, Math.floor(availableHpt / Math.max(0.0001, scaleFactor)));
|
||||
|
||||
let pageIndex = 0;
|
||||
for (let y = 0; y < canvas.height; y += sliceHpx) {
|
||||
const pageCanvas = document.createElement('canvas');
|
||||
const pageHeightPx = Math.min(sliceHpx, canvas.height - y);
|
||||
pageCanvas.width = canvas.width;
|
||||
pageCanvas.height = pageHeightPx;
|
||||
const ctx = pageCanvas.getContext('2d');
|
||||
ctx.drawImage(canvas, 0, y, canvas.width, pageHeightPx, 0, 0, canvas.width, pageHeightPx);
|
||||
|
||||
const imgDataUri = pageCanvas.toDataURL('image/png');
|
||||
const imgH = pageHeightPx * scaleFactor;
|
||||
if (pageIndex > 0) doc.addPage();
|
||||
doc.addImage(imgDataUri, 'PNG', margin, margin, imgW, imgH);
|
||||
pageIndex++;
|
||||
}
|
||||
|
||||
doc.save(dashName + '.pdf');
|
||||
} finally {
|
||||
// Restore export buttons no matter what.
|
||||
exportBtns.forEach((el, idx) => { el.style.display = prevDisplay[idx]; });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Dashboard PDF export failed.');
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
if (btnLabel) btnLabel.textContent = oldLabel || 'Export PDF';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function mountWidget(w) {
|
||||
const mount = document.getElementById('cb-w-' + w.id);
|
||||
if (!mount) return;
|
||||
@ -277,6 +528,14 @@
|
||||
const actions = editLink + rm;
|
||||
|
||||
if (w.widget_type === 'chart') {
|
||||
const exportPng = '<button type="button" class="cb-dash-widget-export cb-dash-export-png" data-widget-id="' + w.id + '" title="Export PNG" aria-label="Export PNG"><span class="material-symbols-outlined" style="font-size:18px;">image</span></button>';
|
||||
const exportPdf = '<button type="button" class="cb-dash-widget-export cb-dash-export-pdf" data-widget-id="' + w.id + '" title="Export PDF" aria-label="Export PDF"><span class="material-symbols-outlined" style="font-size:18px;">picture_as_pdf</span></button>';
|
||||
const canExcel = !READ_ONLY && boot.urls && boot.urls.chartExportBase && w.chart_id;
|
||||
const excelHref = canExcel ? (boot.urls.chartExportBase + w.chart_id + '/export?format=excel') : '';
|
||||
const exportExcel = canExcel
|
||||
? '<a class="cb-dash-widget-export" href="' + escapeHtml(excelHref) + '" title="Export Excel" aria-label="Export Excel" target="_blank" rel="noopener noreferrer"><span class="material-symbols-outlined" style="font-size:18px;">table_view</span></a>'
|
||||
: '';
|
||||
const chartActions = exportPng + exportPdf + exportExcel + actions;
|
||||
const vars = await loadVariableDefs(w.saved_query_id);
|
||||
let varsHtml = '';
|
||||
if (vars.length) {
|
||||
@ -284,7 +543,7 @@
|
||||
}
|
||||
mount.innerHTML =
|
||||
'<div class="cb-dash-widget-card h-100">' +
|
||||
'<div class="cb-dash-widget-head"><span class="text-truncate">' + escapeHtml(title) + '</span><div class="cb-dash-widget-head-actions">' + actions + '</div></div>' +
|
||||
'<div class="cb-dash-widget-head"><span class="text-truncate">' + escapeHtml(title) + '</span><div class="cb-dash-widget-head-actions">' + chartActions + '</div></div>' +
|
||||
'<div class="cb-dash-widget-body">' +
|
||||
varsHtml +
|
||||
'<div class="cb-dash-chart-inner position-relative">' +
|
||||
@ -316,6 +575,8 @@
|
||||
});
|
||||
|
||||
await loadChartData(w);
|
||||
mount.querySelector('.cb-dash-export-png')?.addEventListener('click', () => exportChartPng(w));
|
||||
mount.querySelector('.cb-dash-export-pdf')?.addEventListener('click', () => exportChartPdf(w));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -465,6 +726,8 @@
|
||||
|
||||
document.getElementById('cbBtnToggleEdit')?.addEventListener('click', () => { if (!READ_ONLY) toggleEdit(); });
|
||||
|
||||
document.getElementById('cbBtnExportDashPdf')?.addEventListener('click', () => { exportDashboardPdf(); });
|
||||
|
||||
document.getElementById('cbBtnSaveLayout')?.addEventListener('click', async () => {
|
||||
if (READ_ONLY || !boot.urls || !boot.urls.saveLayout) return;
|
||||
if (!grid) return;
|
||||
|
||||
@ -38,6 +38,14 @@ $themeClass = match ($theme) {
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnFs" title="Fullscreen">
|
||||
<span class="material-symbols-outlined cb-btn-icon">fullscreen</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnExportDashPdf" title="Export dashboard PDF (A4)">
|
||||
<span class="material-symbols-outlined cb-btn-icon">picture_as_pdf</span>
|
||||
<span>Export PDF</span>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="cbChkDashPdfPageBreaks" checked>
|
||||
<label class="form-check-label small" for="cbChkDashPdfPageBreaks">Page breaks</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#modalShare" data-share-type="dashboard" data-resource-id="<?= $did ?>">Share</button>
|
||||
<a href="<?= base_url('dashboard/settings/' . $did) ?>" class="btn btn-sm btn-outline-secondary">Settings</a>
|
||||
<a href="<?= base_url('dashboard') ?>" class="btn btn-sm btn-light border">All boards</a>
|
||||
@ -150,5 +158,7 @@ $themeClass = match ($theme) {
|
||||
<script src="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack-all.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
||||
<?= view('dashboard/_view_scripts') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@ -20,9 +20,19 @@ $themeClass = match ($theme) {
|
||||
<h1><?= esc($dashboard['name']) ?></h1>
|
||||
<div class="cb-dash-meta"><?= esc($dashboard['description'] ?? '') ?></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="cbBtnFs" title="Fullscreen">
|
||||
<span class="material-symbols-outlined" style="font-size:18px;">fullscreen</span>
|
||||
</button>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="cbBtnExportDashPdf" title="Export dashboard PDF (A4)">
|
||||
<span class="material-symbols-outlined cb-btn-icon">picture_as_pdf</span>
|
||||
<span>Export PDF</span>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="cbChkDashPdfPageBreaks" checked>
|
||||
<label class="form-check-label small" for="cbChkDashPdfPageBreaks">Page breaks</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="cbBtnFs" title="Fullscreen">
|
||||
<span class="material-symbols-outlined" style="font-size:18px;">fullscreen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cb-dash-grid-wrap">
|
||||
@ -60,5 +70,7 @@ $themeClass = match ($theme) {
|
||||
<script src="https://cdn.jsdelivr.net/npm/gridstack@10.3.1/dist/gridstack-all.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
||||
<?= view('dashboard/_view_scripts') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@ -176,8 +176,8 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cb-cb-main-row > .col-lg-5,
|
||||
.cb-cb-main-row > .col-lg-7 {
|
||||
.cb-cb-main-row > .col-lg-4,
|
||||
.cb-cb-main-row > .col-lg-8 {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -360,12 +360,34 @@
|
||||
}
|
||||
|
||||
.cb-cb-color-swatch {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0.15rem;
|
||||
border-radius: 0.35rem;
|
||||
border: 1px solid var(--cb-cb-border);
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
padding: 0.12rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d6deea;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cb-cb-color-swatch::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.cb-cb-color-swatch::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.cb-cb-color-swatch::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.cb-cb-swatch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -388,7 +410,8 @@
|
||||
}
|
||||
|
||||
.cb-cb-main-row > .col-lg-5,
|
||||
.cb-cb-main-row > .col-lg-7 {
|
||||
.cb-cb-main-row > .col-lg-4,
|
||||
.cb-cb-main-row > .col-lg-8 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@ -374,6 +374,24 @@
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.cb-dash-widget-export {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
padding: 2px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cb-dash-widget-export:hover {
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.cb-dash-prose {
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user