diff --git a/README.md b/README.md index 69cf8c3..d43ac07 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/VERSION b/VERSION index 6c18eb9..0fd87b1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5.40 +0.5.5.43 diff --git a/app/i18n.py b/app/i18n.py index 4c2be87..dbe3c84 100644 --- a/app/i18n.py +++ b/app/i18n.py @@ -140,6 +140,8 @@ BASE_TRANSLATIONS = { "lists.filters_active": ("{count} filters active", "{count} Filter aktiv"), "lists.filter_status": ("Filtered: {visible} of {total} records", "Gefiltert: {visible} von {total} Datensätzen"), "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_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"), diff --git a/app/static/css/app.css b/app/static/css/app.css index bc4f8c1..2cb069f 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -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"] textarea:disabled, .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; +} diff --git a/app/static/js/asset-columns.js b/app/static/js/asset-columns.js index e878837..dc8349f 100644 --- a/app/static/js/asset-columns.js +++ b/app/static/js/asset-columns.js @@ -224,6 +224,12 @@ } }); + window.addEventListener('asset-table-column-width-changed', event => { + if (!event.detail?.table || event.detail.table === table) { + applyHidden(); + } + }); + applyHidden(); window.dispatchEvent(new CustomEvent('asset-table-columns-ready', { detail: { table } diff --git a/app/static/js/browser-state.js b/app/static/js/browser-state.js index 6d33197..87d8b38 100644 --- a/app/static/js/browser-state.js +++ b/app/static/js/browser-state.js @@ -35,15 +35,35 @@ 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) { if (table.dataset.browserStateReady === '1') return; if (table.dataset.serverFilters === '1') { table.dataset.browserStateReady = '1'; + restoreSort(table, index); return; } const restoreStarted = performance.now(); const controls = filterControls(table); - if (!controls.length) return; + if (!controls.length) { + table.dataset.browserStateReady = '1'; + restoreSort(table, index); + return; + } table.dataset.browserStateReady = '1'; const key = `${tableKey(table, index)}:filters`; @@ -71,6 +91,7 @@ if (typeof table.assetApplyFilters === 'function') { table.assetApplyFilters(); } + restoreSort(table, index); table.dataset.browserStateRestored = '1'; window.dispatchEvent(new CustomEvent('asset-table-state-ready', { detail: { table } })); const duration = performance.now() - restoreStarted; @@ -109,5 +130,25 @@ 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'); })(); diff --git a/app/static/js/table-tools.js b/app/static/js/table-tools.js index 73015bb..872185c 100644 --- a/app/static/js/table-tools.js +++ b/app/static/js/table-tools.js @@ -163,6 +163,104 @@ 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 usesServerFilters = table.dataset.serverFilters === '1'; let serverFilterTimer = null; @@ -173,15 +271,19 @@ // real data-field keys on its movable columns; selection and action // columns must remain without a field key. if (!isAssetTable) { - [...headerRow.cells].forEach((header, index) => { + let syntheticIndex = 0; + [...headerRow.cells].forEach(header => { + if (header.dataset.rowNumberColumn === '1') return; if (!header.dataset.field) { - header.dataset.field = `__col_${index}`; + header.dataset.field = `__col_${syntheticIndex}`; header.dataset.syntheticField = '1'; } + syntheticIndex += 1; }); [...tbody.rows].forEach(row => { [...row.cells].forEach((cell, index) => { + if (cell.dataset.rowNumberColumn === '1') return; const header = headerRow.cells[index]; if (header && !cell.dataset.field) { cell.dataset.field = header.dataset.field; @@ -197,6 +299,160 @@ 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() { const wrapper = table.closest( '#asset-table-scroll, .management-table-scroll, .table-scroll, .table-wrap, .sticky-table' @@ -291,16 +547,22 @@ } function rebuildMissingCellKeys() { - if (isAssetTable) return; + ensureRowNumberCells(); + if (isAssetTable) { + applyStoredColumnWidths(); + return; + } [...tbody.rows].forEach(row => { [...row.cells].forEach((cell, index) => { + if (cell.dataset.rowNumberColumn === '1') return; const header = headerRow.cells[index]; if (header && !cell.dataset.field) { cell.dataset.field = header.dataset.field; } }); }); + applyStoredColumnWidths(); } function numericFilterMatches(rawValue, rawTerm) { @@ -405,6 +667,20 @@ 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() { rebuildMissingCellKeys(); @@ -419,6 +695,7 @@ const serverTotal = Number.parseInt(table.dataset.serverFilterTotal || '', 10); const total = Number.isFinite(serverTotal) ? serverTotal : tbody.rows.length; updateFilterPresentation(active, tbody.rows.length, total); + updateRowNumbers(); window.dispatchEvent(new CustomEvent('asset-table-filter-changed', { detail: { table, @@ -447,6 +724,7 @@ }); updateFilterPresentation(active, visibleRows, tbody.rows.length); + updateRowNumbers(); window.dispatchEvent(new CustomEvent('asset-table-filter-changed', { 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) => { const field = header.dataset.field || ''; const serverName = header.dataset.serverFilterName || ''; @@ -468,37 +783,18 @@ header.addEventListener('click', event => { if (event.target.closest('input,button,a,select,textarea,label')) return; - const direction = header.dataset.sortDirection === 'asc' ? 'desc' : 'asc'; - - [...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(); + applySort(field, direction); }); } const filterCell = document.createElement('th'); if (field) filterCell.dataset.field = field; 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)) { const input = document.createElement('input'); @@ -541,6 +837,8 @@ }); thead.appendChild(filterRow); + installColumnResizers(); + applyStoredColumnWidths(); // Editable cells are always read live, so changed values immediately // participate in filtering without a stale cache. @@ -553,6 +851,8 @@ }); table.assetApplyFilters = applyFilters; + table.assetApplySort = (field, direction, options = {}) => applySort(field, direction, options); + table.assetUpdateRowNumbers = updateRowNumbers; table.assetRebuildFilterCache = rebuildMissingCellKeys; table.dataset.tableToolsReady = '1'; ensureFilterStatus(); @@ -586,7 +886,13 @@ const table = event.detail?.table; if (table?.assetRebuildFilterCache) { table.assetRebuildFilterCache(); - table.assetApplyFilters?.(); + 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.assetUpdateRowNumbers?.(); } }); })(); diff --git a/app/templates/base.html b/app/templates/base.html index 1486e72..c5fdd36 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,7 +14,7 @@ --am-warning:{{ colors.get('warning_color', '#d97706') }}; --am-danger:{{ colors.get('danger_color', '#b42318') }}; } -{{ title or general.get('title', 'AssetManager') }}{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}{% endif %} +{{ title or general.get('title', 'AssetManager') }}{% set favicon_path = general.get('favicon') or general.get('logo') %}{% if favicon_path %}{% endif %}
{% if general.get('logo') %}Logo{% endif %}{{ general.get('title', 'AssetManager') }}{% if general.get('company_name') %}– {{ general.get('company_name') }}{% endif %} diff --git a/app/version.py b/app/version.py index 7a1b3a7..5ebc30f 100644 --- a/app/version.py +++ b/app/version.py @@ -1,2 +1 @@ -APP_VERSION = "0.5.5.40" -__version__ = APP_VERSION +APP_VERSION = "0.5.5.43" diff --git a/docs/version-history/README.md b/docs/version-history/README.md index b730142..0f9832d 100644 --- a/docs/version-history/README.md +++ b/docs/version-history/README.md @@ -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.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. diff --git a/docs/version-history/UPDATE-0.5.5.41.md b/docs/version-history/UPDATE-0.5.5.41.md new file mode 100644 index 0000000..709921a --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.41.md @@ -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. diff --git a/docs/version-history/UPDATE-0.5.5.43.md b/docs/version-history/UPDATE-0.5.5.43.md new file mode 100644 index 0000000..d81bbff --- /dev/null +++ b/docs/version-history/UPDATE-0.5.5.43.md @@ -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.