Privacy Policy DSGVO with sheets for categories, export-, check- and first erase-functions
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# AssetManager 0.5.5.40
|
# AssetManager 0.5.5.43
|
||||||
|
|
||||||
AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. The project is released under the **Apache License 2.0** and may be used, modified, and redistributed for private and commercial purposes.
|
AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. The project is released under the **Apache License 2.0** and may be used, modified, and redistributed for private and commercial purposes.
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ BASE_TRANSLATIONS = {
|
|||||||
"lists.filters_active": ("{count} filters active", "{count} Filter aktiv"),
|
"lists.filters_active": ("{count} filters active", "{count} Filter aktiv"),
|
||||||
"lists.filter_status": ("Filtered: {visible} of {total} records", "Gefiltert: {visible} von {total} Datensätzen"),
|
"lists.filter_status": ("Filtered: {visible} of {total} records", "Gefiltert: {visible} von {total} Datensätzen"),
|
||||||
"lists.clear_filters": ("Clear filters", "Filter zurücksetzen"),
|
"lists.clear_filters": ("Clear filters", "Filter zurücksetzen"),
|
||||||
|
"lists.row_number": ("Row number", "Zeilennummer"),
|
||||||
|
"lists.row_number_resize": ("Drag to change the row-number column width", "Ziehen, um die Breite der Nummernspalte zu ändern"),
|
||||||
"appearance.colors_title": ("System colors", "Systemfarben"),
|
"appearance.colors_title": ("System colors", "Systemfarben"),
|
||||||
"appearance.colors_help": ("Configure the central colors used throughout the application.", "Konfigurieren Sie die zentral im gesamten System verwendeten Farben."),
|
"appearance.colors_help": ("Configure the central colors used throughout the application.", "Konfigurieren Sie die zentral im gesamten System verwendeten Farben."),
|
||||||
"appearance.primary_color": ("Primary color", "Primärfarbe"),
|
"appearance.primary_color": ("Primary color", "Primärfarbe"),
|
||||||
|
|||||||
@@ -2851,3 +2851,110 @@ table.server-filter-loading tbody{opacity:.55;transition:opacity .12s ease}
|
|||||||
.privacy-settings-form[data-privacy-readonly="true"] input:disabled,
|
.privacy-settings-form[data-privacy-readonly="true"] input:disabled,
|
||||||
.privacy-settings-form[data-privacy-readonly="true"] textarea:disabled,
|
.privacy-settings-form[data-privacy-readonly="true"] textarea:disabled,
|
||||||
.privacy-settings-form[data-privacy-readonly="true"] select:disabled { opacity: 1; color: inherit; -webkit-text-fill-color: currentColor; cursor: default; }
|
.privacy-settings-form[data-privacy-readonly="true"] select:disabled { opacity: 1; color: inherit; -webkit-text-fill-color: currentColor; cursor: default; }
|
||||||
|
|
||||||
|
|
||||||
|
/* v0.5.5.41: readable chart status/category filters in dark mode. */
|
||||||
|
html[data-theme="dark"] .checkbox-option{
|
||||||
|
background:#f8fafc !important;
|
||||||
|
color:#111827 !important;
|
||||||
|
border-color:#cbd5e1 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .checkbox-option label,
|
||||||
|
html[data-theme="dark"] .checkbox-option span,
|
||||||
|
html[data-theme="dark"] .checkbox-option strong{
|
||||||
|
color:#111827 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v0.5.5.41: generated visual row number column for data tables. */
|
||||||
|
.data-table .table-row-number-header,
|
||||||
|
.data-table .table-row-number-cell{
|
||||||
|
width:1%;
|
||||||
|
min-width:3.25rem;
|
||||||
|
white-space:nowrap;
|
||||||
|
text-align:right;
|
||||||
|
font-variant-numeric:tabular-nums;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-header{
|
||||||
|
cursor:default;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-cell{
|
||||||
|
color:var(--am-muted,#64748b);
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Visual row-number column: readable by default and user-resizable per table. */
|
||||||
|
.data-table .table-row-number-header,
|
||||||
|
.data-table .table-row-number-filter,
|
||||||
|
.data-table .table-row-number-cell {
|
||||||
|
width: var(--table-row-number-width, 64px);
|
||||||
|
min-width: var(--table-row-number-width, 64px);
|
||||||
|
max-width: var(--table-row-number-width, 64px);
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-header {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 14px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-cell {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-resizer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -4px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 9px;
|
||||||
|
cursor: col-resize;
|
||||||
|
z-index: 3;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-resizer::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
bottom: 20%;
|
||||||
|
left: 4px;
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.data-table .table-row-number-resizer:hover::after,
|
||||||
|
.data-table .table-row-number-resizer:focus-visible::after {
|
||||||
|
border-left-color: currentColor;
|
||||||
|
}
|
||||||
|
html.table-column-resizing,
|
||||||
|
html.table-column-resizing * {
|
||||||
|
cursor: col-resize !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v0.5.5.43: user-resizable widths for every standard table column. */
|
||||||
|
.data-table thead tr:first-child > th:not(.table-row-number-header) {
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.data-table .table-column-resizer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -4px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 9px;
|
||||||
|
z-index: 4;
|
||||||
|
cursor: col-resize;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.data-table .table-column-resizer::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 18%;
|
||||||
|
bottom: 18%;
|
||||||
|
left: 4px;
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.data-table .table-column-resizer:hover::after,
|
||||||
|
.data-table .table-column-resizer:focus-visible::after {
|
||||||
|
border-left-color: currentColor;
|
||||||
|
}
|
||||||
|
|||||||
@@ -224,6 +224,12 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('asset-table-column-width-changed', event => {
|
||||||
|
if (!event.detail?.table || event.detail.table === table) {
|
||||||
|
applyHidden();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
applyHidden();
|
applyHidden();
|
||||||
window.dispatchEvent(new CustomEvent('asset-table-columns-ready', {
|
window.dispatchEvent(new CustomEvent('asset-table-columns-ready', {
|
||||||
detail: { table }
|
detail: { table }
|
||||||
|
|||||||
@@ -35,15 +35,35 @@
|
|||||||
return control.name || control.dataset.field || cell?.dataset.field || `filter-${index}`;
|
return control.name || control.dataset.field || cell?.dataset.field || `filter-${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreSort(table, index, force = false) {
|
||||||
|
if (!force && table.dataset.browserSortRestored === '1') return;
|
||||||
|
if (typeof table.assetApplySort !== 'function') return;
|
||||||
|
|
||||||
|
const key = `${tableKey(table, index)}:sort`;
|
||||||
|
let state = null;
|
||||||
|
try { state = JSON.parse(api.get(key) || 'null'); } catch (_) {}
|
||||||
|
if (state?.field && (state.direction === 'asc' || state.direction === 'desc')) {
|
||||||
|
table.assetApplySort(state.field, state.direction, {silent: true});
|
||||||
|
} else {
|
||||||
|
table.assetUpdateRowNumbers?.();
|
||||||
|
}
|
||||||
|
table.dataset.browserSortRestored = '1';
|
||||||
|
}
|
||||||
|
|
||||||
function restoreTable(table, index) {
|
function restoreTable(table, index) {
|
||||||
if (table.dataset.browserStateReady === '1') return;
|
if (table.dataset.browserStateReady === '1') return;
|
||||||
if (table.dataset.serverFilters === '1') {
|
if (table.dataset.serverFilters === '1') {
|
||||||
table.dataset.browserStateReady = '1';
|
table.dataset.browserStateReady = '1';
|
||||||
|
restoreSort(table, index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const restoreStarted = performance.now();
|
const restoreStarted = performance.now();
|
||||||
const controls = filterControls(table);
|
const controls = filterControls(table);
|
||||||
if (!controls.length) return;
|
if (!controls.length) {
|
||||||
|
table.dataset.browserStateReady = '1';
|
||||||
|
restoreSort(table, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
table.dataset.browserStateReady = '1';
|
table.dataset.browserStateReady = '1';
|
||||||
const key = `${tableKey(table, index)}:filters`;
|
const key = `${tableKey(table, index)}:filters`;
|
||||||
@@ -71,6 +91,7 @@
|
|||||||
if (typeof table.assetApplyFilters === 'function') {
|
if (typeof table.assetApplyFilters === 'function') {
|
||||||
table.assetApplyFilters();
|
table.assetApplyFilters();
|
||||||
}
|
}
|
||||||
|
restoreSort(table, index);
|
||||||
table.dataset.browserStateRestored = '1';
|
table.dataset.browserStateRestored = '1';
|
||||||
window.dispatchEvent(new CustomEvent('asset-table-state-ready', { detail: { table } }));
|
window.dispatchEvent(new CustomEvent('asset-table-state-ready', { detail: { table } }));
|
||||||
const duration = performance.now() - restoreStarted;
|
const duration = performance.now() - restoreStarted;
|
||||||
@@ -109,5 +130,25 @@
|
|||||||
api.remove(key);
|
api.remove(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('asset-table-sort-changed', event => {
|
||||||
|
const table = event.detail?.table;
|
||||||
|
if (!(table instanceof HTMLTableElement)) return;
|
||||||
|
const tables = [...document.querySelectorAll('table')];
|
||||||
|
const index = tables.indexOf(table);
|
||||||
|
const key = `${tableKey(table, index >= 0 ? index : 0)}:sort`;
|
||||||
|
api.set(key, JSON.stringify({
|
||||||
|
field: event.detail?.field || '',
|
||||||
|
direction: event.detail?.direction || 'asc'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('asset-table-content-reloaded', event => {
|
||||||
|
const table = event.detail?.table;
|
||||||
|
if (!(table instanceof HTMLTableElement)) return;
|
||||||
|
const tables = [...document.querySelectorAll('table')];
|
||||||
|
const index = tables.indexOf(table);
|
||||||
|
restoreSort(table, index >= 0 ? index : 0, true);
|
||||||
|
});
|
||||||
|
|
||||||
perf?.end('browser-state:gesamtes Modul');
|
perf?.end('browser-state:gesamtes Modul');
|
||||||
})();
|
})();
|
||||||
|
|||||||
+333
-27
@@ -163,6 +163,104 @@
|
|||||||
|
|
||||||
const headerRow = thead.rows[0];
|
const headerRow = thead.rows[0];
|
||||||
|
|
||||||
|
// Add a purely visual row-number column before every real table column.
|
||||||
|
// It is recalculated after filtering, sorting and partial server reloads and
|
||||||
|
// therefore never represents a database identifier.
|
||||||
|
if (!headerRow.querySelector('th[data-row-number-column="1"]')) {
|
||||||
|
const numberHeader = document.createElement('th');
|
||||||
|
numberHeader.className = 'table-row-number-header';
|
||||||
|
numberHeader.dataset.rowNumberColumn = '1';
|
||||||
|
numberHeader.dataset.noSort = '1';
|
||||||
|
numberHeader.dataset.noFilter = '1';
|
||||||
|
numberHeader.textContent = '#';
|
||||||
|
numberHeader.title = document.body.dataset.rowNumberLabel || 'Row number';
|
||||||
|
headerRow.insertBefore(numberHeader, headerRow.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberHeader = headerRow.querySelector('th[data-row-number-column="1"]');
|
||||||
|
const rowNumberStoragePart = table.dataset.storageKey || table.id || window.location.pathname;
|
||||||
|
const rowNumberWidthKey = `assetmanager:table:${rowNumberStoragePart}:row-number-width`;
|
||||||
|
const defaultRowNumberWidth = 64;
|
||||||
|
const minimumRowNumberWidth = 48;
|
||||||
|
const maximumRowNumberWidth = 180;
|
||||||
|
|
||||||
|
function readStoredRowNumberWidth() {
|
||||||
|
try {
|
||||||
|
const stored = Number.parseInt(window.localStorage.getItem(rowNumberWidthKey) || '', 10);
|
||||||
|
if (Number.isFinite(stored)) {
|
||||||
|
return Math.min(maximumRowNumberWidth, Math.max(minimumRowNumberWidth, stored));
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return defaultRowNumberWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyRowNumberWidth(width) {
|
||||||
|
const normalized = Math.min(maximumRowNumberWidth, Math.max(minimumRowNumberWidth, Math.round(width)));
|
||||||
|
table.style.setProperty('--table-row-number-width', `${normalized}px`);
|
||||||
|
table.dataset.rowNumberWidth = String(normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installRowNumberResizer() {
|
||||||
|
if (!numberHeader || numberHeader.querySelector('.table-row-number-resizer')) return;
|
||||||
|
const handle = document.createElement('span');
|
||||||
|
handle.className = 'table-row-number-resizer';
|
||||||
|
handle.setAttribute('role', 'separator');
|
||||||
|
handle.setAttribute('aria-orientation', 'vertical');
|
||||||
|
handle.tabIndex = 0;
|
||||||
|
handle.title = document.body.dataset.rowNumberResizeLabel || 'Drag to change column width';
|
||||||
|
numberHeader.appendChild(handle);
|
||||||
|
|
||||||
|
const saveWidth = width => {
|
||||||
|
const normalized = applyRowNumberWidth(width);
|
||||||
|
try { window.localStorage.setItem(rowNumberWidthKey, String(normalized)); } catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
handle.addEventListener('pointerdown', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startWidth = Number.parseInt(table.dataset.rowNumberWidth || '', 10) || defaultRowNumberWidth;
|
||||||
|
handle.setPointerCapture?.(event.pointerId);
|
||||||
|
document.documentElement.classList.add('table-column-resizing');
|
||||||
|
|
||||||
|
const move = moveEvent => applyRowNumberWidth(startWidth + moveEvent.clientX - startX);
|
||||||
|
const finish = finishEvent => {
|
||||||
|
saveWidth(Number.parseInt(table.dataset.rowNumberWidth || '', 10) || startWidth);
|
||||||
|
handle.releasePointerCapture?.(finishEvent.pointerId);
|
||||||
|
document.documentElement.classList.remove('table-column-resizing');
|
||||||
|
handle.removeEventListener('pointermove', move);
|
||||||
|
handle.removeEventListener('pointerup', finish);
|
||||||
|
handle.removeEventListener('pointercancel', finish);
|
||||||
|
};
|
||||||
|
handle.addEventListener('pointermove', move);
|
||||||
|
handle.addEventListener('pointerup', finish);
|
||||||
|
handle.addEventListener('pointercancel', finish);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('keydown', event => {
|
||||||
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||||
|
event.preventDefault();
|
||||||
|
const current = Number.parseInt(table.dataset.rowNumberWidth || '', 10) || defaultRowNumberWidth;
|
||||||
|
saveWidth(current + (event.key === 'ArrowRight' ? 8 : -8));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRowNumberWidth(readStoredRowNumberWidth());
|
||||||
|
installRowNumberResizer();
|
||||||
|
|
||||||
|
function ensureRowNumberCells() {
|
||||||
|
[...tbody.rows].forEach(row => {
|
||||||
|
if (row.querySelector('td[data-row-number-column="1"]')) return;
|
||||||
|
const cell = document.createElement('td');
|
||||||
|
cell.className = 'table-row-number-cell';
|
||||||
|
cell.dataset.rowNumberColumn = '1';
|
||||||
|
cell.setAttribute('aria-hidden', 'true');
|
||||||
|
row.insertBefore(cell, row.firstChild);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ensureRowNumberCells();
|
||||||
|
|
||||||
const isAssetTable = table.id === 'asset-table';
|
const isAssetTable = table.id === 'asset-table';
|
||||||
const usesServerFilters = table.dataset.serverFilters === '1';
|
const usesServerFilters = table.dataset.serverFilters === '1';
|
||||||
let serverFilterTimer = null;
|
let serverFilterTimer = null;
|
||||||
@@ -173,15 +271,19 @@
|
|||||||
// real data-field keys on its movable columns; selection and action
|
// real data-field keys on its movable columns; selection and action
|
||||||
// columns must remain without a field key.
|
// columns must remain without a field key.
|
||||||
if (!isAssetTable) {
|
if (!isAssetTable) {
|
||||||
[...headerRow.cells].forEach((header, index) => {
|
let syntheticIndex = 0;
|
||||||
|
[...headerRow.cells].forEach(header => {
|
||||||
|
if (header.dataset.rowNumberColumn === '1') return;
|
||||||
if (!header.dataset.field) {
|
if (!header.dataset.field) {
|
||||||
header.dataset.field = `__col_${index}`;
|
header.dataset.field = `__col_${syntheticIndex}`;
|
||||||
header.dataset.syntheticField = '1';
|
header.dataset.syntheticField = '1';
|
||||||
}
|
}
|
||||||
|
syntheticIndex += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
[...tbody.rows].forEach(row => {
|
[...tbody.rows].forEach(row => {
|
||||||
[...row.cells].forEach((cell, index) => {
|
[...row.cells].forEach((cell, index) => {
|
||||||
|
if (cell.dataset.rowNumberColumn === '1') return;
|
||||||
const header = headerRow.cells[index];
|
const header = headerRow.cells[index];
|
||||||
if (header && !cell.dataset.field) {
|
if (header && !cell.dataset.field) {
|
||||||
cell.dataset.field = header.dataset.field;
|
cell.dataset.field = header.dataset.field;
|
||||||
@@ -197,6 +299,160 @@
|
|||||||
|
|
||||||
const filterDefinitions = [];
|
const filterDefinitions = [];
|
||||||
|
|
||||||
|
// User-resizable widths for all real table columns. Widths are stored per
|
||||||
|
// table and column key. The visual row-number column keeps its dedicated
|
||||||
|
// implementation above because it is generated dynamically.
|
||||||
|
const columnWidthsStorageKey = `assetmanager:table:${rowNumberStoragePart}:column-widths`;
|
||||||
|
const minimumColumnWidth = 56;
|
||||||
|
const maximumColumnWidth = 900;
|
||||||
|
|
||||||
|
function readStoredColumnWidths() {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(window.localStorage.getItem(columnWidthsStorageKey) || '{}');
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||||
|
} catch (_) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let storedColumnWidths = readStoredColumnWidths();
|
||||||
|
|
||||||
|
function columnStorageKey(header, columnIndex) {
|
||||||
|
return header?.dataset.field || header?.dataset.serverFilterName || `__column_${columnIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedColumnWidth(width) {
|
||||||
|
return Math.min(maximumColumnWidth, Math.max(minimumColumnWidth, Math.round(width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyColumnWidth(columnIndex, width, options = {}) {
|
||||||
|
const header = headerRow.cells[columnIndex];
|
||||||
|
if (!header || header.dataset.rowNumberColumn === '1') return null;
|
||||||
|
const normalized = normalizedColumnWidth(width);
|
||||||
|
const cells = [header, filterRow.cells[columnIndex], ...[...tbody.rows].map(row => row.cells[columnIndex])];
|
||||||
|
cells.filter(Boolean).forEach(cell => {
|
||||||
|
cell.style.width = `${normalized}px`;
|
||||||
|
cell.style.minWidth = `${normalized}px`;
|
||||||
|
cell.style.maxWidth = `${normalized}px`;
|
||||||
|
});
|
||||||
|
header.dataset.userColumnWidth = String(normalized);
|
||||||
|
|
||||||
|
// The asset list calculates its scroll width from data-column-width.
|
||||||
|
// Keep that existing mechanism in sync instead of introducing a second
|
||||||
|
// competing table-width calculation.
|
||||||
|
if (isAssetTable && header.dataset.field) {
|
||||||
|
header.dataset.columnWidth = String(normalized);
|
||||||
|
window.dispatchEvent(new CustomEvent('asset-table-column-width-changed', {
|
||||||
|
detail: { table, field: header.dataset.field, width: normalized }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.save) {
|
||||||
|
storedColumnWidths[columnStorageKey(header, columnIndex)] = normalized;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(columnWidthsStorageKey, JSON.stringify(storedColumnWidths));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStoredColumnWidths() {
|
||||||
|
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||||
|
if (header.dataset.rowNumberColumn === '1') return;
|
||||||
|
const stored = Number.parseInt(storedColumnWidths[columnStorageKey(header, columnIndex)], 10);
|
||||||
|
if (Number.isFinite(stored)) applyColumnWidth(columnIndex, stored);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetColumnWidth(columnIndex) {
|
||||||
|
const header = headerRow.cells[columnIndex];
|
||||||
|
if (!header || header.dataset.rowNumberColumn === '1') return;
|
||||||
|
delete storedColumnWidths[columnStorageKey(header, columnIndex)];
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(columnWidthsStorageKey, JSON.stringify(storedColumnWidths));
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
[header, filterRow.cells[columnIndex], ...[...tbody.rows].map(row => row.cells[columnIndex])]
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(cell => {
|
||||||
|
cell.style.removeProperty('width');
|
||||||
|
cell.style.removeProperty('min-width');
|
||||||
|
cell.style.removeProperty('max-width');
|
||||||
|
});
|
||||||
|
delete header.dataset.userColumnWidth;
|
||||||
|
|
||||||
|
if (isAssetTable && header.dataset.field) {
|
||||||
|
const configured = Number.parseInt(header.dataset.defaultColumnWidth || '', 10);
|
||||||
|
const fallback = Number.isFinite(configured) ? configured : 180;
|
||||||
|
header.dataset.columnWidth = String(fallback);
|
||||||
|
window.dispatchEvent(new CustomEvent('asset-table-column-width-changed', {
|
||||||
|
detail: { table, field: header.dataset.field, width: fallback }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function installColumnResizers() {
|
||||||
|
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||||
|
if (header.dataset.rowNumberColumn === '1' || header.colSpan > 1) return;
|
||||||
|
if (header.querySelector(':scope > .table-column-resizer')) return;
|
||||||
|
|
||||||
|
if (isAssetTable && !header.dataset.defaultColumnWidth) {
|
||||||
|
header.dataset.defaultColumnWidth = header.dataset.columnWidth || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = document.createElement('span');
|
||||||
|
handle.className = 'table-column-resizer';
|
||||||
|
handle.setAttribute('role', 'separator');
|
||||||
|
handle.setAttribute('aria-orientation', 'vertical');
|
||||||
|
handle.tabIndex = 0;
|
||||||
|
handle.title = document.body.dataset.rowNumberResizeLabel || 'Drag to change column width';
|
||||||
|
header.appendChild(handle);
|
||||||
|
|
||||||
|
handle.addEventListener('click', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('dblclick', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
resetColumnWidth(columnIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('pointerdown', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startWidth = Math.round(header.getBoundingClientRect().width);
|
||||||
|
handle.setPointerCapture?.(event.pointerId);
|
||||||
|
document.documentElement.classList.add('table-column-resizing');
|
||||||
|
|
||||||
|
const move = moveEvent => applyColumnWidth(columnIndex, startWidth + moveEvent.clientX - startX);
|
||||||
|
const finish = finishEvent => {
|
||||||
|
const current = Number.parseInt(header.dataset.userColumnWidth || '', 10) || startWidth;
|
||||||
|
applyColumnWidth(columnIndex, current, { save: true });
|
||||||
|
handle.releasePointerCapture?.(finishEvent.pointerId);
|
||||||
|
document.documentElement.classList.remove('table-column-resizing');
|
||||||
|
handle.removeEventListener('pointermove', move);
|
||||||
|
handle.removeEventListener('pointerup', finish);
|
||||||
|
handle.removeEventListener('pointercancel', finish);
|
||||||
|
};
|
||||||
|
handle.addEventListener('pointermove', move);
|
||||||
|
handle.addEventListener('pointerup', finish);
|
||||||
|
handle.addEventListener('pointercancel', finish);
|
||||||
|
});
|
||||||
|
|
||||||
|
handle.addEventListener('keydown', event => {
|
||||||
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const current = Number.parseInt(header.dataset.userColumnWidth || '', 10)
|
||||||
|
|| Math.round(header.getBoundingClientRect().width);
|
||||||
|
applyColumnWidth(columnIndex, current + (event.key === 'ArrowRight' ? 12 : -12), { save: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function filterStatusTarget() {
|
function filterStatusTarget() {
|
||||||
const wrapper = table.closest(
|
const wrapper = table.closest(
|
||||||
'#asset-table-scroll, .management-table-scroll, .table-scroll, .table-wrap, .sticky-table'
|
'#asset-table-scroll, .management-table-scroll, .table-scroll, .table-wrap, .sticky-table'
|
||||||
@@ -291,16 +547,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rebuildMissingCellKeys() {
|
function rebuildMissingCellKeys() {
|
||||||
if (isAssetTable) return;
|
ensureRowNumberCells();
|
||||||
|
if (isAssetTable) {
|
||||||
|
applyStoredColumnWidths();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[...tbody.rows].forEach(row => {
|
[...tbody.rows].forEach(row => {
|
||||||
[...row.cells].forEach((cell, index) => {
|
[...row.cells].forEach((cell, index) => {
|
||||||
|
if (cell.dataset.rowNumberColumn === '1') return;
|
||||||
const header = headerRow.cells[index];
|
const header = headerRow.cells[index];
|
||||||
if (header && !cell.dataset.field) {
|
if (header && !cell.dataset.field) {
|
||||||
cell.dataset.field = header.dataset.field;
|
cell.dataset.field = header.dataset.field;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
applyStoredColumnWidths();
|
||||||
}
|
}
|
||||||
|
|
||||||
function numericFilterMatches(rawValue, rawTerm) {
|
function numericFilterMatches(rawValue, rawTerm) {
|
||||||
@@ -405,6 +667,20 @@
|
|||||||
else serverFilterTimer = window.setTimeout(navigate, 700);
|
else serverFilterTimer = window.setTimeout(navigate, 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateRowNumbers() {
|
||||||
|
let visibleIndex = 0;
|
||||||
|
[...tbody.rows].forEach(row => {
|
||||||
|
const cell = row.querySelector('td[data-row-number-column="1"]');
|
||||||
|
if (!cell) return;
|
||||||
|
if (row.hidden) {
|
||||||
|
cell.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visibleIndex += 1;
|
||||||
|
cell.textContent = String(visibleIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
rebuildMissingCellKeys();
|
rebuildMissingCellKeys();
|
||||||
|
|
||||||
@@ -419,6 +695,7 @@
|
|||||||
const serverTotal = Number.parseInt(table.dataset.serverFilterTotal || '', 10);
|
const serverTotal = Number.parseInt(table.dataset.serverFilterTotal || '', 10);
|
||||||
const total = Number.isFinite(serverTotal) ? serverTotal : tbody.rows.length;
|
const total = Number.isFinite(serverTotal) ? serverTotal : tbody.rows.length;
|
||||||
updateFilterPresentation(active, tbody.rows.length, total);
|
updateFilterPresentation(active, tbody.rows.length, total);
|
||||||
|
updateRowNumbers();
|
||||||
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
||||||
detail: {
|
detail: {
|
||||||
table,
|
table,
|
||||||
@@ -447,6 +724,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateFilterPresentation(active, visibleRows, tbody.rows.length);
|
updateFilterPresentation(active, visibleRows, tbody.rows.length);
|
||||||
|
updateRowNumbers();
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
|
||||||
detail: {
|
detail: {
|
||||||
@@ -458,6 +736,43 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySort(field, direction, options = {}) {
|
||||||
|
const header = [...headerRow.cells].find(item => item.dataset.field === field);
|
||||||
|
if (!header || header.dataset.noSort) return false;
|
||||||
|
|
||||||
|
const columnIndex = header.cellIndex;
|
||||||
|
[...headerRow.cells].forEach(item => {
|
||||||
|
delete item.dataset.sortDirection;
|
||||||
|
item.classList.remove('sort-asc', 'sort-desc');
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedDirection = direction === 'desc' ? 'desc' : 'asc';
|
||||||
|
header.dataset.sortDirection = normalizedDirection;
|
||||||
|
header.classList.add(normalizedDirection === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||||
|
|
||||||
|
const rows = [...tbody.rows];
|
||||||
|
rows.sort((left, right) => {
|
||||||
|
const definition = { field, columnIndex };
|
||||||
|
const result = compareValues(
|
||||||
|
textValue(cellForDefinition(left, definition)),
|
||||||
|
textValue(cellForDefinition(right, definition))
|
||||||
|
);
|
||||||
|
return normalizedDirection === 'asc' ? result : -result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
rows.forEach(row => fragment.appendChild(row));
|
||||||
|
tbody.appendChild(fragment);
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
window.dispatchEvent(new CustomEvent('asset-table-sort-changed', {
|
||||||
|
detail: { table, field, direction: normalizedDirection }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[...headerRow.cells].forEach((header, columnIndex) => {
|
[...headerRow.cells].forEach((header, columnIndex) => {
|
||||||
const field = header.dataset.field || '';
|
const field = header.dataset.field || '';
|
||||||
const serverName = header.dataset.serverFilterName || '';
|
const serverName = header.dataset.serverFilterName || '';
|
||||||
@@ -468,37 +783,18 @@
|
|||||||
|
|
||||||
header.addEventListener('click', event => {
|
header.addEventListener('click', event => {
|
||||||
if (event.target.closest('input,button,a,select,textarea,label')) return;
|
if (event.target.closest('input,button,a,select,textarea,label')) return;
|
||||||
|
|
||||||
const direction = header.dataset.sortDirection === 'asc' ? 'desc' : 'asc';
|
const direction = header.dataset.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
applySort(field, direction);
|
||||||
[...headerRow.cells].forEach(item => {
|
|
||||||
delete item.dataset.sortDirection;
|
|
||||||
item.classList.remove('sort-asc', 'sort-desc');
|
|
||||||
});
|
|
||||||
|
|
||||||
header.dataset.sortDirection = direction;
|
|
||||||
header.classList.add(direction === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
||||||
|
|
||||||
const rows = [...tbody.rows];
|
|
||||||
rows.sort((left, right) => {
|
|
||||||
const definition = { field, columnIndex };
|
|
||||||
const result = compareValues(
|
|
||||||
textValue(cellForDefinition(left, definition)),
|
|
||||||
textValue(cellForDefinition(right, definition))
|
|
||||||
);
|
|
||||||
return direction === 'asc' ? result : -result;
|
|
||||||
});
|
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
rows.forEach(row => fragment.appendChild(row));
|
|
||||||
tbody.appendChild(fragment);
|
|
||||||
applyFilters();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterCell = document.createElement('th');
|
const filterCell = document.createElement('th');
|
||||||
if (field) filterCell.dataset.field = field;
|
if (field) filterCell.dataset.field = field;
|
||||||
filterCell.dataset.columnIndex = String(columnIndex);
|
filterCell.dataset.columnIndex = String(columnIndex);
|
||||||
|
if (header.dataset.rowNumberColumn === '1') {
|
||||||
|
filterCell.dataset.rowNumberColumn = '1';
|
||||||
|
filterCell.className = 'table-row-number-filter';
|
||||||
|
}
|
||||||
|
|
||||||
if (!header.dataset.noFilter && (!usesServerFilters || serverName)) {
|
if (!header.dataset.noFilter && (!usesServerFilters || serverName)) {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
@@ -541,6 +837,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
thead.appendChild(filterRow);
|
thead.appendChild(filterRow);
|
||||||
|
installColumnResizers();
|
||||||
|
applyStoredColumnWidths();
|
||||||
|
|
||||||
// Editable cells are always read live, so changed values immediately
|
// Editable cells are always read live, so changed values immediately
|
||||||
// participate in filtering without a stale cache.
|
// participate in filtering without a stale cache.
|
||||||
@@ -553,6 +851,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
table.assetApplyFilters = applyFilters;
|
table.assetApplyFilters = applyFilters;
|
||||||
|
table.assetApplySort = (field, direction, options = {}) => applySort(field, direction, options);
|
||||||
|
table.assetUpdateRowNumbers = updateRowNumbers;
|
||||||
table.assetRebuildFilterCache = rebuildMissingCellKeys;
|
table.assetRebuildFilterCache = rebuildMissingCellKeys;
|
||||||
table.dataset.tableToolsReady = '1';
|
table.dataset.tableToolsReady = '1';
|
||||||
ensureFilterStatus();
|
ensureFilterStatus();
|
||||||
@@ -586,7 +886,13 @@
|
|||||||
const table = event.detail?.table;
|
const table = event.detail?.table;
|
||||||
if (table?.assetRebuildFilterCache) {
|
if (table?.assetRebuildFilterCache) {
|
||||||
table.assetRebuildFilterCache();
|
table.assetRebuildFilterCache();
|
||||||
|
const sortedHeader = table.tHead?.rows?.[0]?.querySelector('[data-sort-direction]');
|
||||||
|
if (sortedHeader?.dataset.field && table.assetApplySort) {
|
||||||
|
table.assetApplySort(sortedHeader.dataset.field, sortedHeader.dataset.sortDirection, {silent: true});
|
||||||
|
} else {
|
||||||
table.assetApplyFilters?.();
|
table.assetApplyFilters?.();
|
||||||
}
|
}
|
||||||
|
table.assetUpdateRowNumbers?.();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
--am-warning:{{ colors.get('warning_color', '#d97706') }};
|
--am-warning:{{ colors.get('warning_color', '#d97706') }};
|
||||||
--am-danger:{{ colors.get('danger_color', '#b42318') }};
|
--am-danger:{{ colors.get('danger_color', '#b42318') }};
|
||||||
}
|
}
|
||||||
</style><script>(()=>{try{if(localStorage.getItem('assetmanager.sidebar.collapsed')==='true')document.documentElement.classList.add('sidebar-collapsed-preset')}catch(_){}})();</script><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ title or general.get('title', 'AssetManager') }}</title>{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}<link rel="icon" href="{{ favicon_path }}">{% endif %}<link rel="stylesheet" href="/static/css/app.css?v={{ application_version() }}"><link rel="stylesheet" href="/custom.css"></head><body{% if request and request.url.path == '/assets' %} class="asset-list-scroll-page"{% endif %} data-record-count-template="{{ t('lists.record_count', 'Angezeigt: {visible} von {total} Datensätzen')|e }}" data-record-count-title="{{ t('lists.record_count_hint', 'Die Anzeige berücksichtigt die aktuell gesetzten Tabellenfilter.')|e }}" data-filter-active-label="{{ t('lists.filter_active', 'Filter aktiv')|e }}" data-filters-active-template="{{ t('lists.filters_active', '{count} Filter aktiv')|e }}" data-filter-status-template="{{ t('lists.filter_status', 'Gefiltert: {visible} von {total} Datensätzen')|e }}" data-clear-filters-label="{{ t('lists.clear_filters', 'Filter zurücksetzen')|e }}" data-persist-browser-state="{{ 'true' if session_user and session_user.persist_browser_state else 'false' }}" data-toast-position="{{ session_user.toast_position if session_user else 'top-right' }}" data-toast-duration="{{ session_user.toast_duration_seconds if session_user else 6 }}">
|
</style><script>(()=>{try{if(localStorage.getItem('assetmanager.sidebar.collapsed')==='true')document.documentElement.classList.add('sidebar-collapsed-preset')}catch(_){}})();</script><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ title or general.get('title', 'AssetManager') }}</title>{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}<link rel="icon" href="{{ favicon_path }}">{% endif %}<link rel="stylesheet" href="/static/css/app.css?v={{ application_version() }}"><link rel="stylesheet" href="/custom.css"></head><body{% if request and request.url.path == '/assets' %} class="asset-list-scroll-page"{% endif %} data-record-count-template="{{ t('lists.record_count', 'Angezeigt: {visible} von {total} Datensätzen')|e }}" data-record-count-title="{{ t('lists.record_count_hint', 'Die Anzeige berücksichtigt die aktuell gesetzten Tabellenfilter.')|e }}" data-filter-active-label="{{ t('lists.filter_active', 'Filter aktiv')|e }}" data-filters-active-template="{{ t('lists.filters_active', '{count} Filter aktiv')|e }}" data-filter-status-template="{{ t('lists.filter_status', 'Gefiltert: {visible} von {total} Datensätzen')|e }}" data-clear-filters-label="{{ t('lists.clear_filters', 'Filter zurücksetzen')|e }}" data-row-number-label="{{ t('lists.row_number', 'Zeilennummer')|e }}" data-row-number-resize-label="{{ t('lists.row_number_resize', 'Ziehen, um die Breite der Nummernspalte zu ändern')|e }}" data-persist-browser-state="{{ 'true' if session_user and session_user.persist_browser_state else 'false' }}" data-toast-position="{{ session_user.toast_position if session_user else 'top-right' }}" data-toast-duration="{{ session_user.toast_duration_seconds if session_user else 6 }}">
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<a class="brand" href="/">{% if general.get('logo') %}<img src="{{ general.get('logo') }}" alt="Logo">{% endif %}<span>{{ general.get('title', 'AssetManager') }}</span>{% if general.get('company_name') %}<span class="brand-company">– {{ general.get('company_name') }}</span>{% endif %}</a>
|
<a class="brand" href="/">{% if general.get('logo') %}<img src="{{ general.get('logo') }}" alt="Logo">{% endif %}<span>{{ general.get('title', 'AssetManager') }}</span>{% if general.get('company_name') %}<span class="brand-company">– {{ general.get('company_name') }}</span>{% endif %}</a>
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
APP_VERSION = "0.5.5.40"
|
APP_VERSION = "0.5.5.43"
|
||||||
__version__ = APP_VERSION
|
|
||||||
|
|||||||
@@ -162,3 +162,4 @@ See [UPDATE-NOTE.md](UPDATE-NOTE.md).
|
|||||||
|
|
||||||
- [0.5.5.39](UPDATE-0.5.5.39.md) – Privacy read-only tabs and permission-aware dashboard counts.
|
- [0.5.5.39](UPDATE-0.5.5.39.md) – Privacy read-only tabs and permission-aware dashboard counts.
|
||||||
- [0.5.5.40](UPDATE-0.5.5.40.md) – Privacy exports available to all authenticated users.
|
- [0.5.5.40](UPDATE-0.5.5.40.md) – Privacy exports available to all authenticated users.
|
||||||
|
- [0.5.5.41](UPDATE-0.5.5.41.md) – Dark-mode filter readability, persisted table sorting, and generated row numbers.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Version 0.5.5.41
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improved readability of chart status and category filter options in dark mode.
|
||||||
|
- Table sorting is now stored per table in browser state and restored when the user returns.
|
||||||
|
- Every standard data table now receives a generated first column with visual row numbers.
|
||||||
|
- Row numbers are recalculated after filtering, sorting, and partial server-side table refreshes and are not database identifiers.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Version 0.5.5.43
|
||||||
|
|
||||||
|
- Added drag handles to resize every standard table column.
|
||||||
|
- Stored column widths independently for each table and column.
|
||||||
|
- Kept the asset list's existing horizontal-scroll width calculation in sync.
|
||||||
|
- Added keyboard resizing with the left and right arrow keys.
|
||||||
|
- Added double-click on a separator to reset a column to its default width.
|
||||||
Reference in New Issue
Block a user