multi value filtering added and font color fix for duplicates

This commit is contained in:
2026-08-11 23:28:35 +02:00
parent 5594431749
commit e0175ab48a
340 changed files with 739 additions and 160 deletions
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
Regular → Executable
View File
Regular → Executable
+120 -6
View File
@@ -99,6 +99,33 @@
{% if selected %}<input type="hidden" name="category_id" value="{{ selected.id }}">{% endif %}
</form>
{% endif %}
<dialog id="software-compare-select-dialog" class="software-compare-select-dialog">
<form method="dialog">
<div class="dialog-heading">
<h2>{{ t('software.compare.select_second_title', 'Zweites Asset auswählen') }}</h2>
<button type="button" class="button button-secondary button-small" id="software-compare-dialog-close" aria-label="{{ t('common.close', 'Schließen')|e }}">×</button>
</div>
<p class="muted">{{ t('software.compare.select_second_help', 'Wähle das zweite Asset für den Vergleich. Das Auswahlfeld zeigt nur vorhandene Werte des gewählten Feldes.') }}</p>
<label>{{ t('software.compare.first_asset', 'Erstes Asset') }}
<input type="text" id="software-compare-first-name" readonly>
</label>
<label>{{ t('software.compare.select_by', 'Auswahl nach') }}
<select id="software-compare-field-select">
{% for option in compare_field_options %}<option value="{{ option.field_name }}"{% if option.field_name == 'name' %} selected{% endif %}>{{ option.label }}</option>{% endfor %}
</select>
</label>
<label>{{ t('software.compare.second_asset', 'Zweites Asset') }}
<select id="software-compare-second-select" disabled>
<option value="">{{ t('common.loading', 'Lade…') }}</option>
</select>
</label>
<p class="muted software-compare-selection-status" id="software-compare-selection-status" role="status" aria-live="polite"></p>
<div class="form-actions">
<button type="button" class="button button-secondary" id="software-compare-dialog-cancel">{{ t('common.cancel', 'Abbrechen') }}</button>
<button type="button" class="button" id="software-compare-dialog-start" disabled>⇄ {{ t('software.compare.button', 'Software vergleichen') }}</button>
</div>
</form>
</dialog>
{% endif %}
<div class="table-scrollbar-top" id="asset-table-scroll-top" aria-hidden="true"><div></div></div>
<div class="table-scroll sticky-table" id="asset-table-scroll">
@@ -120,7 +147,7 @@
<th data-no-sort="1" data-no-filter="1" class="actions-column">{{ t('common.actions', 'Aktionen') }}</th>
</tr></thead>
<tbody>
{% for a in assets %}<tr data-platform="{{ asset_platforms.get(a.id, 'unknown') }}">
{% for a in assets %}<tr data-platform="{{ asset_platforms.get(a.id, 'unknown') }}" data-asset-id="{{ a.id }}" data-asset-name="{{ a.name|e }}">
<td class="asset-visual-column" data-field="asset_visual">
<a href="/assets/{{ a.id }}" class="asset-visual-link" aria-label="{{ a.name|e }}">
{% set visual_source = a.visual_source or 'auto' %}
@@ -387,6 +414,14 @@
const deleteButton = document.getElementById('bulk-delete-button');
const mergeButton = document.getElementById('merge-duplicates-button');
const softwareCompareButton = document.getElementById('software-compare-button');
const softwareCompareDialog = document.getElementById('software-compare-select-dialog');
const softwareCompareField = document.getElementById('software-compare-field-select');
const softwareCompareSecond = document.getElementById('software-compare-second-select');
const softwareCompareFirstName = document.getElementById('software-compare-first-name');
const softwareCompareStatus = document.getElementById('software-compare-selection-status');
const softwareCompareStart = document.getElementById('software-compare-dialog-start');
const softwareCompareClose = document.getElementById('software-compare-dialog-close');
const softwareCompareCancel = document.getElementById('software-compare-dialog-cancel');
const jobTypeSelect = document.getElementById('bulk-job-type');
const jobStartButton = document.getElementById('bulk-job-start-button');
const isVisible = box => {
@@ -424,8 +459,10 @@
mergeButton.textContent = count >= 2 ? `⧉ {{ t('duplicates.merge') }} (${count})` : '⧉ {{ t('duplicates.merge') }}';
}
if (softwareCompareButton) {
softwareCompareButton.disabled = count !== 2;
softwareCompareButton.textContent = count === 2 ? `⇄ {{ t('software.compare.button', 'Software vergleichen') }} (2)` : '⇄ {{ t('software.compare.button', 'Software vergleichen') }}';
softwareCompareButton.disabled = count < 1 || count > 2;
softwareCompareButton.textContent = (count === 1 || count === 2)
? `⇄ {{ t('software.compare.button', 'Software vergleichen') }} (${count})`
: '⇄ {{ t('software.compare.button', 'Software vergleichen') }}';
}
if (jobTypeSelect) { jobTypeSelect.disabled = count === 0; filterJobDefinitions(); }
if (jobStartButton) {
@@ -436,13 +473,70 @@
}
if (all) { all.checked = visible.length > 0 && count === visible.length; all.indeterminate = count > 0 && count < visible.length; }
}
function submit(formId, containerId) {
function submitIds(formId, containerId, ids) {
const form = document.getElementById(formId);
const container = document.getElementById(containerId);
if (!form || !container) return;
container.innerHTML = '';
selected().forEach(id => { const input = document.createElement('input'); input.type='hidden'; input.name='asset_ids'; input.value=id; container.appendChild(input); });
ids.forEach(id => { const input = document.createElement('input'); input.type='hidden'; input.name='asset_ids'; input.value=id; container.appendChild(input); });
form.requestSubmit();
}
function submit(formId, containerId) {
submitIds(formId, containerId, selected());
}
function selectedAssetName(assetId) {
const box = boxes().find(item => item.value === String(assetId));
return box?.closest('tr')?.dataset.assetName || '';
}
async function loadSoftwareCompareOptions() {
const ids = selected();
if (ids.length !== 1 || !softwareCompareField || !softwareCompareSecond) return;
const firstId = ids[0];
softwareCompareSecond.disabled = true;
softwareCompareSecond.innerHTML = `<option value="">{{ t('common.loading', 'Lade…')|e }}</option>`;
if (softwareCompareStatus) softwareCompareStatus.textContent = '';
if (softwareCompareStart) softwareCompareStart.disabled = true;
const target = new URL('/api/assets/software-compare/options', window.location.origin);
target.searchParams.set('field_name', softwareCompareField.value || 'name');
target.searchParams.set('exclude_asset_id', firstId);
try {
const response = await fetch(target.toString(), {headers: {'Accept': 'application/json'}, cache: 'no-store'});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const payload = await response.json();
const options = Array.isArray(payload.options) ? payload.options : [];
softwareCompareSecond.innerHTML = '';
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = options.length
? {{ t('software.compare.choose_value', 'Wert auswählen …')|tojson }}
: {{ t('software.compare.no_selectable_values', 'Keine passenden Werte vorhanden')|tojson }};
softwareCompareSecond.appendChild(placeholder);
options.forEach(item => {
const option = document.createElement('option');
option.value = String(item.asset_id);
option.textContent = item.label || item.value || item.asset_name || String(item.asset_id);
option.dataset.assetName = item.asset_name || '';
softwareCompareSecond.appendChild(option);
});
softwareCompareSecond.disabled = options.length === 0;
if (softwareCompareStatus) {
softwareCompareStatus.textContent = options.length
? {{ t('software.compare.available_values', '{count} Werte verfügbar')|tojson }}.replace('{count}', String(options.length))
: {{ t('software.compare.no_selectable_values', 'Keine passenden Werte vorhanden')|tojson }};
}
} catch (error) {
softwareCompareSecond.innerHTML = `<option value="">{{ t('software.compare.load_error', 'Auswahl konnte nicht geladen werden')|e }}</option>`;
if (softwareCompareStatus) softwareCompareStatus.textContent = {{ t('software.compare.load_error', 'Auswahl konnte nicht geladen werden')|tojson }};
}
}
async function openSoftwareCompareDialog() {
const ids = selected();
if (ids.length !== 1 || !softwareCompareDialog) return;
if (softwareCompareFirstName) softwareCompareFirstName.value = selectedAssetName(ids[0]);
if (softwareCompareField) softwareCompareField.value = 'name';
softwareCompareDialog.showModal();
await loadSoftwareCompareOptions();
}
table.addEventListener('change', event => {
if (event.target.matches('.asset-select')) refresh();
});
@@ -450,7 +544,27 @@
editButton?.addEventListener('click', () => submit('bulk-edit-form', 'bulk-edit-ids'));
deleteButton?.addEventListener('click', () => submit('bulk-delete-form', 'bulk-delete-ids'));
mergeButton?.addEventListener('click', () => submit('merge-duplicates-form', 'merge-duplicate-ids'));
softwareCompareButton?.addEventListener('click', () => submit('software-compare-form', 'software-compare-ids'));
softwareCompareButton?.addEventListener('click', () => {
const ids = selected();
if (ids.length === 2) {
submitIds('software-compare-form', 'software-compare-ids', ids);
return;
}
if (ids.length === 1) openSoftwareCompareDialog();
});
softwareCompareField?.addEventListener('change', loadSoftwareCompareOptions);
softwareCompareSecond?.addEventListener('change', () => {
if (softwareCompareStart) softwareCompareStart.disabled = !softwareCompareSecond.value;
});
softwareCompareStart?.addEventListener('click', () => {
const ids = selected();
const secondId = softwareCompareSecond?.value || '';
if (ids.length !== 1 || !secondId) return;
softwareCompareDialog?.close();
submitIds('software-compare-form', 'software-compare-ids', [ids[0], secondId]);
});
softwareCompareClose?.addEventListener('click', () => softwareCompareDialog?.close());
softwareCompareCancel?.addEventListener('click', () => softwareCompareDialog?.close());
jobTypeSelect?.addEventListener('change', refresh);
jobStartButton?.addEventListener('click', () => {
const count = selected().length;
View File
Regular → Executable
View File
View File
View File
Regular → Executable
+1 -1
View File
@@ -14,7 +14,7 @@
--am-warning:{{ colors.get('warning_color', '#d97706') }};
--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-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-column-resize-label="{{ t('lists.column_resize', 'Ziehen, um die Spaltenbreite 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 }}">
</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-column-resize-label="{{ t('lists.column_resize', 'Ziehen, um die Spaltenbreite zu ändern')|e }}" data-multi-value-filter-hint="{{ t('lists.multi_value_filter_hint', 'Mehrere Suchwerte können zeilenweise eingefügt werden. Innerhalb einer Spalte gilt ODER.')|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">
<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>
Regular → Executable
+1 -1
View File
@@ -122,7 +122,7 @@
</dialog>
<div class="table-scroll management-table-scroll">
<table class="data-table categories-table" data-storage-key="categories">
<table class="data-table categories-table" data-storage-key="categories" data-default-sort-field="name" data-default-sort-direction="asc">
<thead>
<tr>
<th class="selection-column" data-no-sort="1" data-no-filter="1" data-field="selection">
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
Regular → Executable
View File
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File
View File
View File
View File
View File
View File
Regular → Executable
View File
View File
View File
Regular → Executable
View File
View File
View File
View File
Regular → Executable
View File
Regular → Executable
View File