fix table with handling, icons implemented

This commit is contained in:
root
2026-08-09 08:50:13 +02:00
parent 5e32be7d0c
commit 3ff0794cfc
43 changed files with 1027 additions and 95 deletions
+16 -1
View File
@@ -73,7 +73,22 @@
</div>
<div class="detail">
<div>{% if asset.image_path %}<img class="asset-image" src="{{ asset.image_path }}">{% elif asset.category.image_path %}<img class="asset-image" src="{{ asset.category.image_path }}">{% endif %}</div>
<div>
{% set visual_source = asset.visual_source or 'auto' %}
{% if visual_source == 'asset_image' %}
{% if asset.image_path %}<img class="asset-image" src="{{ asset.image_path }}">{% endif %}
{% elif visual_source == 'category_image' %}
{% if asset.category.image_path %}<img class="asset-image" src="{{ asset.category.image_path }}">{% endif %}
{% elif visual_source == 'mesh_icon' %}
{% if asset.mesh_icon and asset.mesh_icon >= 1 and asset.mesh_icon <= 8 %}<img class="asset-image meshcentral-device-icon" src="/static/img/meshcentral-device-icons/icons256-{{ asset.mesh_icon }}-1.png" alt="">{% endif %}
{% elif asset.image_path %}
<img class="asset-image" src="{{ asset.image_path }}">
{% elif asset.category.image_path %}
<img class="asset-image" src="{{ asset.category.image_path }}">
{% elif asset.mesh_icon and asset.mesh_icon >= 1 and asset.mesh_icon <= 8 %}
<img class="asset-image meshcentral-device-icon" src="/static/img/meshcentral-device-icons/icons256-{{ asset.mesh_icon }}-1.png" alt="">
{% endif %}
</div>
<dl>{% for f in active_fields %}{% if f.field_name not in ['room_number', 'parent_asset_id'] %}{% set d=f.definition %}{% set v=custom_values.get(f.field_name) if d and not d.is_system else asset|attr(f.field_name) %}{% if v %}<dt>{{ t(d.translation_key, d.label) if d else f.label }}</dt><dd>{{ v }}</dd>{% endif %}{% endif %}{% endfor %}{% if asset.room_number %}<dt>{{ t('assets.room') }}</dt><dd>{{ asset.room_number }}</dd>{% endif %}
{% if parent_asset %}<dt>{{ t('assets.parent') }}</dt><dd><a href="/assets/{{ parent_asset.id }}">{{ parent_asset.name }}</a></dd>{% endif %}
{% if child_assets %}<dt>{{ t('assets.child_assets') }}</dt><dd>{% for child in child_assets %}<a href="/assets/{{ child.id }}">{{ child.name }}</a>{% if not loop.last %}, {% endif %}{% endfor %}</dd>{% endif %}
+50 -7
View File
@@ -23,18 +23,61 @@
{% elif d and d.data_type in ['integer','decimal'] %}<input type="number" {% if d.data_type=='decimal' %}step="any"{% endif %} name="{{f.field_name}}" value="{{value or ''}}" {% if f.required %}required{% endif %}>
{% elif d and d.data_type == 'json' %}<textarea name="{{f.field_name}}">{{value|tojson if value else ''}}</textarea>
{% else %}<input name="{{f.field_name}}" value="{{value or ''}}" {% if f.required %}required{% endif %}>{% endif %}</label>{% endfor %}{% endif %}
<label>{{t('assets.image', 'Image')}}<input type="file" name="image" accept="image/*"></label><button class="button button-save">💾 {{t('common.save')}}</button></form>
<div class="form-field asset-image-editor" data-image-editor>
<label for="asset-image-input">{{t('assets.image', 'Image')}}</label>
<div class="asset-edit-image-preview" id="asset-current-image-preview" data-image-preview {% if not (asset and not duplicate_mode and asset.image_path) %}hidden style="display:none"{% endif %}>
<img src="{{ asset.image_path if asset and not duplicate_mode and asset.image_path else '' }}" alt="" class="asset-edit-preview-image">
<div class="asset-edit-image-preview-actions">
<span class="muted">{{ t('assets.current_image', 'Aktuelles Assetbild') }}</span>
<button type="button" class="button button-danger button-small" id="remove-asset-image-button" data-image-remove aria-pressed="false" {% if not (asset and not duplicate_mode and asset.image_path) %}hidden disabled{% endif %}>{{ t('assets.remove_image', 'Assetbild entfernen') }}</button>
</div>
</div>
<input type="hidden" name="remove_image" id="remove-asset-image" value="0" data-image-remove-input>
<div class="image-remove-pending" data-image-remove-pending hidden>{{ t('assets.image_remove_pending', 'Assetbild wird beim Speichern entfernt.') }}</div>
<div class="standard-image-library">
<strong>{{ t('images.library', 'Standardbild-Bibliothek') }}</strong>
<div class="muted image-library-help">{{ t('images.library_help', 'Wähle ein bereits vorhandenes Standardbild oder füge ein Bild einmalig zur Bibliothek hinzu.') }}</div>
{% if standard_images %}
<div class="standard-image-grid">
{% for item in standard_images %}
<label class="standard-image-choice" title="{{ item.name }}">
<input type="radio" name="library_image" value="{{ item.path }}" data-image-library-choice data-image-src="{{ item.path }}" {% if asset and not duplicate_mode and asset.image_path == item.path %}checked{% endif %}>
<img src="{{ item.path }}" alt="">
<span>{{ item.name }}</span>
</label>
{% endfor %}
</div>
{% else %}
<div class="muted image-library-empty">{{ t('images.library_empty', 'Noch keine Standardbilder vorhanden.') }}</div>
{% endif %}
<label class="image-upload-label">{{ t('images.library_upload', 'Neues Standardbild hinzufügen und verwenden') }}
<input type="file" name="library_upload" accept="image/*" data-image-library-upload>
</label>
<small class="muted">{{ t('images.library_upload_help', 'Das Bild wird einmal in der gemeinsamen Bibliothek gespeichert und kann danach bei weiteren Assets und Kategorien wiederverwendet werden.') }}</small>
</div>
</div>
<label>{{ t('assets.visual_source', 'Anzeigequelle') }}
<select name="visual_source">
{% set current_visual_source = asset.visual_source if asset and asset.visual_source else 'auto' %}
<option value="auto" {% if current_visual_source == 'auto' %}selected{% endif %}>{{ t('assets.visual_source.auto', 'Automatisch: Assetbild → Kategoriebild → MeshCentral-Icon') }}</option>
<option value="asset_image" {% if current_visual_source == 'asset_image' %}selected{% endif %}>{{ t('assets.visual_source.asset_image', 'Nur Assetbild') }}</option>
<option value="category_image" {% if current_visual_source == 'category_image' %}selected{% endif %}>{{ t('assets.visual_source.category_image', 'Kategoriebild') }}</option>
<option value="mesh_icon" {% if current_visual_source == 'mesh_icon' %}selected{% endif %}>{{ t('assets.visual_source.mesh_icon', 'MeshCentral-Icon') }}</option>
</select>
<small class="muted">{{ t('assets.visual_source.help', 'Legt fest, welches Bild oder Icon für dieses Asset in Listen und Details bevorzugt angezeigt wird.') }}</small>
</label><button class="button button-save">💾 {{t('common.save')}}</button></form>
{% if asset and not duplicate_mode %}
<script>
(() => {
const input = document.getElementById('mesh-node-id-input');
const clearButton = document.getElementById('mesh-node-id-clear');
if (!input || !clearButton) return;
clearButton.addEventListener('click', () => {
input.value = '';
input.focus();
input.dispatchEvent(new Event('input', { bubbles: true }));
});
if (input && clearButton) {
clearButton.addEventListener('click', () => {
input.value = '';
input.focus();
input.dispatchEvent(new Event('input', { bubbles: true }));
});
}
})();
</script>
{% endif %}
+21
View File
@@ -104,6 +104,7 @@
<div class="table-scroll sticky-table" id="asset-table-scroll">
<table class="data-table asset-table-fixed" id="asset-table" data-storage-key="asset-columns-{{ selected.id if selected else 'all' }}" data-fixed-column-width="{% if is_admin %}224{% else %}180{% endif %}">
<thead><tr>
<th class="asset-visual-column" data-no-sort="1" data-no-filter="1" data-field="asset_visual" data-label="{{ t('assets.device_visual', 'Gerät')|e }}" title="{{ t('assets.device_visual', 'Gerät')|e }}">{{ t('assets.device_visual', 'Gerät') }}</th>
{% if is_admin %}<th class="selection-column" data-no-sort="1" data-no-filter="1"><input type="checkbox" id="select-all-assets" aria-label="{{ t('assets.select_all') }}"></th>{% endif %}
{% set has_name = fields|selectattr('field_name', 'equalto', 'name')|list|length > 0 %}
{% if not has_name %}<th draggable="true" data-field="name" data-column-width="220" data-label="{{ t('field.name', 'Bezeichnung')|e }}" style="width:220px;min-width:220px;max-width:220px">{{ t('field.name', 'Bezeichnung') }}</th>{% endif %}
@@ -120,6 +121,26 @@
</tr></thead>
<tbody>
{% for a in assets %}<tr data-platform="{{ asset_platforms.get(a.id, 'unknown') }}">
<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' %}
{% if visual_source == 'asset_image' %}
{% if a.image_path %}<img src="{{ a.image_path }}" alt="" class="asset-list-device-image">{% else %}<span class="asset-list-device-placeholder" aria-hidden="true"></span>{% endif %}
{% elif visual_source == 'category_image' %}
{% if a.category and a.category.image_path %}<img src="{{ a.category.image_path }}" alt="" class="asset-list-device-image category-device-image" title="{{ a.category.name|e }}">{% else %}<span class="asset-list-device-placeholder" aria-hidden="true"></span>{% endif %}
{% elif visual_source == 'mesh_icon' %}
{% if a.mesh_icon and a.mesh_icon >= 1 and a.mesh_icon <= 8 %}<img src="/static/img/meshcentral-device-icons/icons256-{{ a.mesh_icon }}-1.png" alt="" class="asset-list-device-image meshcentral-device-icon" title="{{ t('assets.mesh_icon_title', 'MeshCentral-Geräte-Icon {icon}').format(icon=a.mesh_icon)|e }}">{% else %}<span class="asset-list-device-placeholder" aria-hidden="true"></span>{% endif %}
{% elif a.image_path %}
<img src="{{ a.image_path }}" alt="" class="asset-list-device-image">
{% elif a.category and a.category.image_path %}
<img src="{{ a.category.image_path }}" alt="" class="asset-list-device-image category-device-image" title="{{ a.category.name|e }}">
{% elif a.mesh_icon and a.mesh_icon >= 1 and a.mesh_icon <= 8 %}
<img src="/static/img/meshcentral-device-icons/icons256-{{ a.mesh_icon }}-1.png" alt="" class="asset-list-device-image meshcentral-device-icon" title="{{ t('assets.mesh_icon_title', 'MeshCentral-Geräte-Icon {icon}').format(icon=a.mesh_icon)|e }}">
{% else %}
<span class="asset-list-device-placeholder" aria-hidden="true"></span>
{% endif %}
</a>
</td>
{% if is_admin %}<td class="selection-column"><input type="checkbox" class="asset-select" value="{{ a.id }}" aria-label="{{ t('assets.select_named', 'Asset {name} markieren').format(name=a.name)|e }}"></td>{% endif %}
{% if not has_name %}<td data-field="name"><a href="/assets/{{ a.id }}">{{ a.name }}</a></td>{% endif %}
{% if not selected %}<td data-field="category">{{ a.category.name }}</td>{% endif %}
+1 -1
View File
@@ -105,7 +105,7 @@
{% endif %}
</aside>
<main class="app-content">{% block content %}{% endblock %}</main>
</div><div id="toast-container" class="toast-container" aria-live="polite"></div><script src="/static/js/local-time.js"></script><script src="/static/js/browser-state.js?v={{ app_version }}"></script><script src="/static/js/table-tools.js?v={{ app_version }}"></script><script src="/static/js/list-tools.js?v={{ app_version }}"></script><script src="/static/js/asset-columns.js?v={{ app_version }}"></script><script src="/static/js/asset-duplicates.js?v={{ app_version }}"></script><script src="/static/js/toasts.js"></script><script>
</div><div id="toast-container" class="toast-container" aria-live="polite"></div><script src="/static/js/local-time.js"></script><script src="/static/js/browser-state.js?v={{ app_version }}"></script><script src="/static/js/table-tools.js?v={{ app_version }}"></script><script src="/static/js/list-tools.js?v={{ app_version }}"></script><script src="/static/js/asset-columns.js?v={{ app_version }}"></script><script src="/static/js/asset-duplicates.js?v={{ app_version }}"></script><script src="/static/js/toasts.js"></script><script src="/static/js/image-editor.js?v={{ app_version }}"></script><script>
(() => {
document.addEventListener('keydown', event => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return;
+21 -17
View File
@@ -122,19 +122,20 @@
</dialog>
<div class="table-scroll management-table-scroll">
<table class="data-table categories-table">
<table class="data-table categories-table" data-storage-key="categories">
<thead>
<tr>
<th class="selection-column" data-no-sort="1" data-no-filter="1">
<th class="selection-column" data-no-sort="1" data-no-filter="1" data-field="selection">
<input type="checkbox" id="select-all-categories" aria-label="{{ t('categories.select_all', 'Alle Kategorien markieren') }}">
</th>
<th>{{ t('common.name') }}</th>
<th>{{ t('common.assets') }}</th>
<th>{{ t('categories.fields_total', 'Felder gesamt') }}</th>
<th>{{ t('categories.active_fields') }}</th>
<th>{{ t('categories.list_fields', 'In Liste') }}</th>
<th>{{ t('categories.attributes', 'Aktive Attribute') }}</th>
<th data-no-filter="1" data-no-sort="1">{{ t('common.actions') }}</th>
<th data-no-sort="1" data-no-filter="1" data-field="category_image">{{ t('categories.image', 'Bild') }}</th>
<th data-field="name">{{ t('common.name') }}</th>
<th data-field="assets_count">{{ t('common.assets') }}</th>
<th data-field="fields_total">{{ t('categories.fields_total', 'Felder gesamt') }}</th>
<th data-field="active_fields">{{ t('categories.active_fields') }}</th>
<th data-field="list_fields">{{ t('categories.list_fields', 'In Liste') }}</th>
<th data-field="attributes">{{ t('categories.attributes', 'Aktive Attribute') }}</th>
<th data-no-filter="1" data-no-sort="1" data-field="actions">{{ t('common.actions') }}</th>
</tr>
</thead>
<tbody>
@@ -143,23 +144,26 @@
{% set active_fields = c.visible_fields|selectattr('active')|list %}
{% set list_fields = active_fields|selectattr('show_in_list')|list %}
<tr>
<td class="selection-column">
<td class="selection-column" data-field="selection">
<input class="category-select" type="checkbox" form="category-bulk-form" name="category_ids" value="{{ c.id }}"
aria-label="{{ c.name|e }} {{ t('common.select', 'markieren') }}">
</td>
<td>{{ c.name }}</td>
<td>{{ count }}</td>
<td>{{ c.visible_fields|length }}</td>
<td>{{ active_fields|length }}</td>
<td>{{ list_fields|length }}</td>
<td class="category-attributes">
<td class="category-image-cell" data-field="category_image">
{% if c.image_path %}<img src="{{ c.image_path }}" alt="" class="category-table-image">{% else %}<span class="asset-list-device-placeholder" aria-hidden="true"></span>{% endif %}
</td>
<td data-field="name">{{ c.name }}</td>
<td data-field="assets_count">{{ count }}</td>
<td data-field="fields_total">{{ c.visible_fields|length }}</td>
<td data-field="active_fields">{{ active_fields|length }}</td>
<td data-field="list_fields">{{ list_fields|length }}</td>
<td class="category-attributes" data-field="attributes">
{% for field in active_fields %}
<span class="attribute-chip{% if field.show_in_list %} attribute-chip-list{% endif %}" title="{{ t('categories.shown_in_list', 'In Liste sichtbar') if field.show_in_list else t('categories.active_only', 'Aktiv, aber nicht in Liste sichtbar') }}">{{ field_label(field) }}</span>
{% else %}
<span class="muted">{{ t('categories.no_active_fields', 'Keine aktiven Felder') }}</span>
{% endfor %}
</td>
<td class="category-actions">
<td class="category-actions" data-field="actions">
<div class="action-buttons">
<a class="button button-secondary" href="/categories/{{ c.id }}/edit" title="{{ t('common.edit') }}">{{ t('common.edit') }}</a>
<button type="submit" class="button button-secondary" form="duplicate-category-{{ c.id }}" title="{{ t('common.duplicate') }}">{{ t('common.duplicate') }}</button>
+35 -2
View File
@@ -3,7 +3,39 @@
<form method="post" enctype="multipart/form-data">
<label>{{ t('common.name') }}<input name="name" value="{{category.name if category else ''}}" required></label>
<label>{{ t('common.description') }}<textarea name="description">{{category.description if category and category.description else ''}}</textarea></label>
<label>{{ t('categories.image') }}<input type="file" name="image" accept="image/*"></label>
<div class="form-field category-image-editor" data-image-editor>
<label for="category-image-input">{{ t('categories.image') }}</label>
<div class="asset-edit-image-preview" id="category-current-image-preview" data-image-preview {% if not (category and category.image_path) %}hidden style="display:none"{% endif %}>
<img src="{{ category.image_path if category and category.image_path else '' }}" class="asset-edit-preview-image" alt="">
<div class="asset-edit-image-preview-actions">
<span class="muted">{{ t('categories.current_image', 'Aktuelles Kategoriebild') }}</span>
<button type="button" class="button button-danger button-small" id="remove-category-image-button" data-image-remove aria-pressed="false" {% if not (category and category.image_path) %}hidden disabled{% endif %}>{{ t('categories.remove_image', 'Kategoriebild entfernen') }}</button>
</div>
</div>
<input type="hidden" name="remove_image" id="remove-category-image" value="0" data-image-remove-input>
<div class="image-remove-pending" data-image-remove-pending hidden>{{ t('categories.image_remove_pending', 'Kategoriebild wird beim Speichern entfernt.') }}</div>
<div class="standard-image-library">
<strong>{{ t('images.library', 'Standardbild-Bibliothek') }}</strong>
<div class="muted image-library-help">{{ t('images.library_help', 'Wähle ein bereits vorhandenes Standardbild oder füge ein Bild einmalig zur Bibliothek hinzu.') }}</div>
{% if standard_images %}
<div class="standard-image-grid">
{% for item in standard_images %}
<label class="standard-image-choice" title="{{ item.name }}">
<input type="radio" name="library_image" value="{{ item.path }}" data-image-library-choice data-image-src="{{ item.path }}" {% if category and category.image_path == item.path %}checked{% endif %}>
<img src="{{ item.path }}" alt="">
<span>{{ item.name }}</span>
</label>
{% endfor %}
</div>
{% else %}
<div class="muted image-library-empty">{{ t('images.library_empty', 'Noch keine Standardbilder vorhanden.') }}</div>
{% endif %}
<label class="image-upload-label">{{ t('images.library_upload', 'Neues Standardbild hinzufügen und verwenden') }}
<input type="file" name="library_upload" accept="image/*" data-image-library-upload>
</label>
<small class="muted">{{ t('images.library_upload_help', 'Das Bild wird einmal in der gemeinsamen Bibliothek gespeichert und kann danach bei weiteren Assets und Kategorien wiederverwendet werden.') }}</small>
</div>
</div>
<h2>{{ t('categories.fields_title') }}</h2>
<p class="muted">Labels, descriptions, data types and read-only settings are managed centrally under Settings → Fields.</p>
<table class="data-table"><thead><tr><th>{{ t('common.active') }}</th><th>{{ t('categories.database_field') }}</th><th>{{ t('categories.display_name') }}</th><th>{{ t('categories.required') }}</th><th>{{ t('categories.in_list') }}</th><th>{{ t('categories.readonly') }}</th></tr></thead><tbody>
@@ -13,4 +45,5 @@
<td><input class="checkbox" type="checkbox" name="required_{{definition.id}}" {% if (cfg and cfg.required) or definition.field_name == 'name' %}checked{% endif %}{% if definition.readonly or definition.field_name == 'name' %} disabled{% endif %}></td>
<td><input class="checkbox" type="checkbox" name="list_{{definition.id}}" {% if (cfg and cfg.show_in_list) or definition.field_name == 'name' %}checked{% endif %}></td>
<td>{{ t('common.yes') if definition.readonly else t('common.no') }}</td></tr>{% endfor %}</tbody></table>
<button>💾 {{ t('common.save') }}</button></form>{% endblock %}
<button>💾 {{ t('common.save') }}</button></form>
{% endblock %}
+2 -2
View File
@@ -52,7 +52,7 @@
<h2>2. {{ t('settings.mesh_mapping') }}</h2>
{% set mesh = config.meshcentral %}
{% set category_mapping = mesh.get('category_mapping', {}) %}
<p class="muted">{{ t('settings.mesh_mapping_help') }}</p>
<p class="muted">{{ t('settings.mesh_icon_mapping_help') }}</p>
<form method="post" action="/sync/meshcentral/categories/save" class="compact-form">
<label>{{ t('settings.fallback_category') }}
<select name="mesh_fallback_category_id" required>
@@ -73,7 +73,7 @@
('8', 'settings.mesh_type_linux_other')
] %}
{% for type_id, type_label in mesh_types %}
<label>{{ t(type_label) }} ({{ t('settings.meshcentral_type') }} {{ type_id }})
<label class="mesh-icon-category-label"><span class="mesh-icon-category-title"><img src="/static/img/meshcentral-device-icons/icons256-{{ type_id }}-1.png" alt="" class="mesh-icon-preview">{{ t(type_label) }} ({{ t('settings.meshcentral_type') }} {{ type_id }})</span>
<select name="mesh_category_{{ type_id }}">
<option value="">{{ t('settings.use_fallback') }}</option>
{% for category in categories %}