fmeshsync fix with synology,vm's and lxc

This commit is contained in:
root
2026-08-08 19:44:27 +02:00
parent 2786f12ab6
commit 5e32be7d0c
41 changed files with 1335 additions and 1674 deletions
+48 -5
View File
@@ -9,6 +9,11 @@
const ignoreEmpty = document.getElementById('duplicate-ignore-empty');
const caseInsensitive = document.getElementById('duplicate-case-insensitive');
const summary = document.getElementById('duplicate-search-summary');
const contextStatus = document.getElementById('asset-context-filter-status');
const duplicateChip = document.getElementById('asset-duplicate-filter-chip');
const duplicateLabel = document.getElementById('asset-duplicate-filter-label');
const duplicateRemove = document.getElementById('asset-duplicate-filter-remove');
const categoryRemove = document.getElementById('asset-category-filter-remove');
if (!table || !openButton || !dialog || table.dataset.duplicateSearchReady === '1') return;
table.dataset.duplicateSearchReady = '1';
@@ -26,6 +31,17 @@
label: th.dataset.label || th.textContent.trim()
}));
function refreshContextStatus() {
const categoryChip = document.getElementById('asset-category-filter-chip');
const duplicateActive = table.dataset.duplicateFilterActive === '1';
if (duplicateChip) duplicateChip.hidden = !duplicateActive;
if (contextStatus) {
const hasActive = Boolean(categoryChip) || duplicateActive;
contextStatus.hidden = !hasActive;
contextStatus.classList.toggle('is-active', hasActive);
}
}
function fillSelect() {
const previous = select.value;
select.innerHTML = '';
@@ -42,8 +58,13 @@
function clearDuplicateFilter() {
dataRows().forEach(row => row.classList.remove('duplicate-hidden', 'duplicate-match'));
summary.hidden = true;
window.dispatchEvent(new Event('asset-table-filter-changed'));
delete table.dataset.duplicateFilterActive;
delete table.dataset.duplicateFilterTotal;
delete table.dataset.duplicateFilterField;
if (summary) summary.hidden = true;
if (duplicateLabel) duplicateLabel.textContent = '';
refreshContextStatus();
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {detail: {table}}));
}
function normalize(value) {
@@ -93,9 +114,24 @@
}
});
summary.textContent = `${duplicateRows.size} Datensätze in ${duplicateGroups} Dublettengruppen gefunden.`;
summary.hidden = false;
window.dispatchEvent(new Event('asset-table-filter-changed'));
table.dataset.duplicateFilterActive = '1';
table.dataset.duplicateFilterTotal = String(duplicateRows.size);
table.dataset.duplicateFilterField = field;
const selectedOption = select.options[select.selectedIndex];
const fieldLabel = selectedOption?.textContent?.trim() || field;
const duplicateTemplate = contextStatus?.dataset.duplicateTemplate || 'Duplikate: {field}';
if (duplicateLabel) duplicateLabel.textContent = duplicateTemplate.replace('{field}', fieldLabel);
refreshContextStatus();
const summaryTemplate = dialog.dataset.resultSummaryTemplate || '{records} Datensätze in {groups} Dublettengruppen gefunden.';
if (summary) {
summary.textContent = summaryTemplate
.replace('{records}', String(duplicateRows.size))
.replace('{groups}', String(duplicateGroups));
summary.hidden = false;
}
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {detail: {table}}));
dialog.close();
}
@@ -108,6 +144,13 @@
});
applyButton.addEventListener('click', applyDuplicateFilter);
clearButton.addEventListener('click', clearDuplicateFilter);
duplicateRemove?.addEventListener('click', clearDuplicateFilter);
categoryRemove?.addEventListener('click', () => {
const url = new URL(window.location.href);
url.searchParams.delete('category_id');
window.location.assign(url.toString());
});
refreshContextStatus();
}
if (document.readyState === 'loading') {
+42 -1
View File
@@ -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');
})();
+8 -4
View File
@@ -11,7 +11,7 @@
}
function isVisible(row) {
if (row.hidden || row.classList.contains('hidden')) return false;
if (row.hidden || row.classList.contains('hidden') || row.classList.contains('duplicate-hidden')) return false;
return true;
}
@@ -40,23 +40,27 @@
const updateStarted = performance.now();
const rows = [...table.querySelectorAll('tbody tr')].filter(isDataRow);
const visible = rows.filter(isVisible).length;
const duplicateTotal = Number.parseInt(table.dataset.duplicateFilterTotal || '', 10);
const total = table.dataset.duplicateFilterActive === '1' && Number.isFinite(duplicateTotal)
? duplicateTotal
: rows.length;
const target = countTarget(table, index);
if (target) {
const template = document.body.dataset.recordCountTemplate || 'Angezeigt: {visible} von {total} Datensätzen';
target.textContent = template
.replace('{visible}', String(visible))
.replace('{total}', String(rows.length));
.replace('{total}', String(total));
target.title = document.body.dataset.recordCountTitle || '';
}
window.dispatchEvent(new CustomEvent('asset-list-count-updated', {
detail: { table, visible, total: rows.length }
detail: { table, visible, total }
}));
const duration = performance.now() - updateStarted;
const logger = duration >= 50 ? console.warn : console.debug;
logger('[AssetManager][Performance]', 'list-tools:update', {
dauer_ms: Math.round(duration * 10) / 10,
sichtbar: visible,
gesamt: rows.length
gesamt: total
});
}
+294 -27
View File
@@ -163,6 +163,221 @@
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();
// Every visible data column can be resized independently. Widths are
// stored per table and per field/index so they survive navigation,
// reloads and container updates. The visual row-number column keeps its
// dedicated implementation above.
const columnWidthStoragePrefix = `assetmanager:table:${rowNumberStoragePart}:column-width:`;
const minimumColumnWidth = 56;
const maximumColumnWidth = 1200;
function columnStorageId(header, columnIndex) {
const field = header?.dataset?.field;
if (field) return field;
return `index-${columnIndex}`;
}
function normalizeColumnWidth(width) {
return Math.min(maximumColumnWidth, Math.max(minimumColumnWidth, Math.round(width)));
}
function applyColumnWidth(columnIndex, width) {
const normalized = normalizeColumnWidth(width);
const rows = [
...thead.rows,
...tbody.rows
];
rows.forEach(row => {
const cell = row.cells?.[columnIndex];
if (!cell || cell.dataset.rowNumberColumn === '1') return;
cell.style.width = `${normalized}px`;
cell.style.minWidth = `${normalized}px`;
cell.style.maxWidth = `${normalized}px`;
});
return normalized;
}
function readStoredColumnWidth(header, columnIndex) {
try {
const key = `${columnWidthStoragePrefix}${columnStorageId(header, columnIndex)}`;
const stored = Number.parseInt(window.localStorage.getItem(key) || '', 10);
if (Number.isFinite(stored)) return normalizeColumnWidth(stored);
} catch (_) {}
return null;
}
function saveColumnWidth(header, columnIndex, width) {
const normalized = applyColumnWidth(columnIndex, width);
try {
const key = `${columnWidthStoragePrefix}${columnStorageId(header, columnIndex)}`;
window.localStorage.setItem(key, String(normalized));
} catch (_) {}
return normalized;
}
function applyStoredColumnWidths() {
[...headerRow.cells].forEach((header, columnIndex) => {
if (header.dataset.rowNumberColumn === '1') return;
const stored = readStoredColumnWidth(header, columnIndex);
if (stored !== null) applyColumnWidth(columnIndex, stored);
});
}
function installColumnResizers() {
[...headerRow.cells].forEach((header, columnIndex) => {
if (header.dataset.rowNumberColumn === '1') return;
if (header.dataset.noResize === '1') return;
if (header.querySelector(':scope > .table-column-resizer')) return;
header.classList.add('table-resizable-column-header');
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.columnResizeLabel || 'Drag to change the column width';
header.appendChild(handle);
handle.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
});
handle.addEventListener('pointerdown', event => {
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const measured = header.getBoundingClientRect().width;
const startWidth = measured > 0 ? measured : minimumColumnWidth;
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 = header.getBoundingClientRect().width || startWidth;
saveColumnWidth(header, columnIndex, current);
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 = header.getBoundingClientRect().width || minimumColumnWidth;
saveColumnWidth(header, columnIndex, current + (event.key === 'ArrowRight' ? 12 : -12));
});
});
}
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 +388,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;
@@ -291,10 +510,12 @@
}
function rebuildMissingCellKeys() {
ensureRowNumberCells();
if (isAssetTable) 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;
@@ -405,6 +626,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 +654,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 +683,7 @@
});
updateFilterPresentation(active, visibleRows, tbody.rows.length);
updateRowNumbers();
window.dispatchEvent(new CustomEvent('asset-table-filter-changed', {
detail: {
@@ -458,6 +695,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 +742,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 +796,8 @@
});
thead.appendChild(filterRow);
installColumnResizers();
applyStoredColumnWidths();
// Editable cells are always read live, so changed values immediately
// participate in filtering without a stale cache.
@@ -553,7 +810,10 @@
});
table.assetApplyFilters = applyFilters;
table.assetApplySort = (field, direction, options = {}) => applySort(field, direction, options);
table.assetUpdateRowNumbers = updateRowNumbers;
table.assetRebuildFilterCache = rebuildMissingCellKeys;
table.assetApplyStoredColumnWidths = applyStoredColumnWidths;
table.dataset.tableToolsReady = '1';
ensureFilterStatus();
@@ -586,7 +846,14 @@
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?.();
table.assetApplyStoredColumnWidths?.();
}
});
})();