Skip to content

Commit c641cb5

Browse files
committed
- Custom calculated-column formulas can round values with ROUND(value) or ROUND(value, decimals).
Signed-off-by: Rello <github@scherello.de>
1 parent e0201c9 commit c641cb5

4 files changed

Lines changed: 293 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
##
44
### Added
5+
- Custom calculated-column formulas can round values with ROUND(value) or ROUND(value, decimals).
6+
- Nextcloud Dashboard API V2 clients can display favorite reports with PHP-rendered SVG value and trend previews.
57
- Local datasource file paths can use date text variables and formatting through an opt-in filename editor.
68
- Reusable script for generating the CycloneDX SBOM from the bundled dependencies.
79
- GitHub Actions cross-check Analytics on multiple Nextcloud, PHP, and database combinations.
810

911
### Changed
12+
- Allow calculated columns to be positioned in the table layout dialog.
1013
- Update PhpSpreadsheet to the 5.x release line.
1114
- Update bundled jsPDF to v4.2.1.
1215
- Only send scheduled data load error notifications when the complete load fails.

js/filter.js

Lines changed: 120 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ OCA.Analytics.Filter = {
7474
thresholdEl.classList.remove('report-option-active');
7575
}
7676
}
77+
78+
const tableOptionsEl = document.getElementById('optionsMenuTableOptions');
79+
if (tableOptionsEl) {
80+
const calculatedColumns = OCA.Analytics.currentReportData.options.tableoptions?.calculatedColumns;
81+
if (typeof calculatedColumns === 'string' && calculatedColumns.trim() !== '') {
82+
tableOptionsEl.classList.add('report-option-active');
83+
} else {
84+
tableOptionsEl.classList.remove('report-option-active');
85+
}
86+
}
7787
},
7888

7989
/**
@@ -756,6 +766,7 @@ OCA.Analytics.Filter = {
756766
OCA.Analytics.Filter.calculatedColumnsEditorState.draft = null;
757767

758768
OCA.Analytics.Filter.renderCalculatedColumnsList();
769+
OCA.Analytics.Filter.Drag.syncCalculatedColumns();
759770
OCA.Analytics.Filter.closeCalculatedColumnEditor();
760771
},
761772

@@ -1304,6 +1315,7 @@ OCA.Analytics.Filter = {
13041315
state.parseError = false;
13051316
document.getElementById('tableOptionsCalculatedColumns').value = OCA.Analytics.Filter.serializeCalculatedColumnsValue(state.items);
13061317
OCA.Analytics.Filter.renderCalculatedColumnsList();
1318+
OCA.Analytics.Filter.Drag.syncCalculatedColumns();
13071319
OCA.Analytics.Filter.closeCalculatedColumnEditor();
13081320
},
13091321

@@ -1313,10 +1325,12 @@ OCA.Analytics.Filter = {
13131325
return;
13141326
}
13151327

1316-
state.items.splice(state.editingIndex, 1);
1328+
const deletedIndex = state.editingIndex;
1329+
state.items.splice(deletedIndex, 1);
13171330
state.parseError = false;
13181331
document.getElementById('tableOptionsCalculatedColumns').value = OCA.Analytics.Filter.serializeCalculatedColumnsValue(state.items);
13191332
OCA.Analytics.Filter.renderCalculatedColumnsList();
1333+
OCA.Analytics.Filter.Drag.syncCalculatedColumns(deletedIndex);
13201334
OCA.Analytics.Filter.closeCalculatedColumnEditor();
13211335
},
13221336

@@ -1840,6 +1854,41 @@ OCA.Analytics.Filter = {
18401854
};
18411855

18421856
OCA.Analytics.Filter.Drag = {
1857+
createDraggableItem: function (text, index, calculationIndex = null) {
1858+
const div = document.createElement('div');
1859+
div.id = 'column-' + index;
1860+
div.className = 'draggable tableOptionsLayoutItem';
1861+
div.draggable = true;
1862+
div.ondragstart = OCA.Analytics.Filter.Drag.drag;
1863+
if (calculationIndex !== null) {
1864+
div.dataset.calculationIndex = calculationIndex;
1865+
}
1866+
1867+
const iconDiv = document.createElement('div');
1868+
iconDiv.className = 'icon-analytics-gripLines sidebarPointer';
1869+
div.appendChild(iconDiv);
1870+
1871+
const textSpan = document.createElement('span');
1872+
textSpan.textContent = text;
1873+
div.appendChild(textSpan);
1874+
1875+
return div;
1876+
},
1877+
1878+
getCalculatedColumnItems: function () {
1879+
const calculatedColumnsInput = document.getElementById('tableOptionsCalculatedColumns');
1880+
if (calculatedColumnsInput) {
1881+
const parsed = OCA.Analytics.Filter.parseCalculatedColumnsValue(calculatedColumnsInput.value);
1882+
return parsed.items
1883+
.map(calc => OCA.Analytics.Visualization.normalizeCalculatedColumn(calc))
1884+
.filter(calc => calc !== null && OCA.Analytics.Visualization.isCalculatedColumnRenderable(calc));
1885+
}
1886+
1887+
const tableOptions = OCA.Analytics.currentReportData.options.tableoptions || {};
1888+
return OCA.Analytics.Visualization.getCalculatedColumns(tableOptions)
1889+
.filter(calc => OCA.Analytics.Visualization.isCalculatedColumnRenderable(calc));
1890+
},
1891+
18431892
/**
18441893
* Allow dropping an element during drag and drop operations.
18451894
*/
@@ -1892,56 +1941,102 @@ OCA.Analytics.Filter.Drag = {
18921941
*/
18931942
initialize: function () {
18941943
let layoutConfig = OCA.Analytics.currentReportData.options.tableoptions.layout;
1895-
let headerItems = OCA.Analytics.currentReportData.header;
1944+
const sourceHeaders = OCA.Analytics.currentReportData.header || [];
1945+
const calculations = OCA.Analytics.Filter.Drag.getCalculatedColumnItems();
1946+
const headerItems = [
1947+
...sourceHeaders,
1948+
...calculations.map(calc => calc.title || t('analytics', 'Calculated column')),
1949+
];
18961950
let rowsSection = document.getElementById('rows');
18971951

18981952
// Clear existing sections
18991953
document.querySelectorAll('.section').forEach(function (section) {
19001954
section.innerHTML = '<p>' + section.id.charAt(0).toUpperCase() + section.id.slice(1) + '</p>';
19011955
});
19021956

1903-
// Function to create a draggable div
1904-
const createDraggableItem = (text, index) => {
1905-
let div = document.createElement('div');
1906-
div.id = 'column-' + index;
1907-
div.className = 'draggable tableOptionsLayoutItem';
1908-
div.draggable = true;
1909-
div.ondragstart = OCA.Analytics.Filter.Drag.drag;
1910-
1911-
// Create and append the icon div
1912-
let iconDiv = document.createElement('div');
1913-
iconDiv.className = 'icon-analytics-gripLines sidebarPointer';
1914-
div.appendChild(iconDiv);
1915-
1916-
// Create and append the text span
1917-
let textSpan = document.createElement('span');
1918-
textSpan.textContent = text;
1919-
div.appendChild(textSpan);
1920-
1921-
return div;
1922-
};
1923-
19241957
if (!layoutConfig || Object.keys(layoutConfig).length === 0) {
19251958
// Fallback: Add all header items to the "rows" section
19261959
headerItems.forEach((text, index) => {
1927-
let draggableItem = createDraggableItem(text, index);
1960+
const calculationIndex = index >= sourceHeaders.length ? index - sourceHeaders.length : null;
1961+
let draggableItem = OCA.Analytics.Filter.Drag.createDraggableItem(text, index, calculationIndex);
19281962
rowsSection.appendChild(draggableItem);
19291963
});
19301964
} else {
1965+
const placedIndexes = new Set();
19311966
// Append items to their respective sections based on layoutConfig
19321967
Object.keys(layoutConfig).forEach(section => {
19331968
let sectionElement = document.getElementById(section);
19341969
layoutConfig[section].forEach(index => {
1935-
let draggableItem = createDraggableItem(headerItems[index], index);
1970+
if (!sectionElement || headerItems[index] === undefined || placedIndexes.has(index)) {
1971+
return;
1972+
}
1973+
const calculationIndex = index >= sourceHeaders.length ? index - sourceHeaders.length : null;
1974+
let draggableItem = OCA.Analytics.Filter.Drag.createDraggableItem(headerItems[index], index, calculationIndex);
19361975
sectionElement.appendChild(draggableItem);
1976+
placedIndexes.add(index);
19371977
});
19381978
});
1979+
1980+
headerItems.forEach((text, index) => {
1981+
if (placedIndexes.has(index)) {
1982+
return;
1983+
}
1984+
const calculationIndex = index >= sourceHeaders.length ? index - sourceHeaders.length : null;
1985+
rowsSection.appendChild(OCA.Analytics.Filter.Drag.createDraggableItem(text, index, calculationIndex));
1986+
});
19391987
}
19401988

19411989
// Manage placeholders initially
19421990
OCA.Analytics.Filter.Drag.managePlaceholders();
19431991
},
19441992

1993+
syncCalculatedColumns: function (deletedIndex = null) {
1994+
const rowsSection = document.getElementById('rows');
1995+
if (!rowsSection) {
1996+
return;
1997+
}
1998+
1999+
const sourceColumnCount = (OCA.Analytics.currentReportData.header || []).length;
2000+
if (deletedIndex !== null) {
2001+
document.querySelectorAll('.draggable[data-calculation-index]').forEach(item => {
2002+
const calculationIndex = parseInt(item.dataset.calculationIndex, 10);
2003+
if (calculationIndex === deletedIndex) {
2004+
item.remove();
2005+
} else if (calculationIndex > deletedIndex) {
2006+
item.dataset.calculationIndex = calculationIndex - 1;
2007+
item.id = 'column-' + (sourceColumnCount + calculationIndex - 1);
2008+
}
2009+
});
2010+
}
2011+
2012+
const calculations = OCA.Analytics.Filter.Drag.getCalculatedColumnItems();
2013+
calculations.forEach((calc, calculationIndex) => {
2014+
const columnIndex = sourceColumnCount + calculationIndex;
2015+
let item = document.querySelector('.draggable[data-calculation-index="' + calculationIndex + '"]');
2016+
if (!item) {
2017+
item = OCA.Analytics.Filter.Drag.createDraggableItem(
2018+
calc.title || t('analytics', 'Calculated column'),
2019+
columnIndex,
2020+
calculationIndex
2021+
);
2022+
rowsSection.appendChild(item);
2023+
} else {
2024+
item.id = 'column-' + columnIndex;
2025+
const label = item.querySelector('span');
2026+
if (label) {
2027+
label.textContent = calc.title || t('analytics', 'Calculated column');
2028+
}
2029+
}
2030+
});
2031+
2032+
document.querySelectorAll('.draggable[data-calculation-index]').forEach(item => {
2033+
if (parseInt(item.dataset.calculationIndex, 10) >= calculations.length) {
2034+
item.remove();
2035+
}
2036+
});
2037+
OCA.Analytics.Filter.Drag.managePlaceholders();
2038+
},
2039+
19452040
/**
19462041
* Insert or remove drag-and-drop placeholders depending on section
19472042
* content to guide the user during layout editing.

0 commit comments

Comments
 (0)