Initialer Import des AssetManagers
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="toolbar">
|
||||
<h1>{{ t('app.categories') }}</h1>
|
||||
<div class="toolbar-actions">
|
||||
<button type="button" class="button button-secondary" id="category-bulk-edit-button"
|
||||
data-base-label="✏ {{ t('categories.bulk.edit', 'Feld ändern')|e }}" disabled>
|
||||
✏ {{ t('categories.bulk.edit', 'Feld ändern') }}
|
||||
</button>
|
||||
<button type="button" class="button button-secondary" id="category-fields-button"
|
||||
data-base-label="⚙ {{ t('categories.fields.manage', 'Felder verwalten')|e }}" disabled>
|
||||
⚙ {{ t('categories.fields.manage', 'Felder verwalten') }}
|
||||
</button>
|
||||
<button type="submit" class="button button-secondary" form="category-bulk-form" id="activate-defaults-button"
|
||||
data-base-label="✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren')|e }}" disabled>
|
||||
✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren') }}
|
||||
</button>
|
||||
<a class="button" href="/categories/new">{{ t('categories.create') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="category-bulk-form" method="post" action="/categories/activate-default-fields"></form>
|
||||
|
||||
<dialog id="category-bulk-edit-dialog">
|
||||
<form id="category-bulk-edit-form" method="post" action="/categories/bulk-edit" enctype="multipart/form-data">
|
||||
<div class="dialog-heading">
|
||||
<h2>{{ t('categories.bulk.dialog_title', 'Markierte Kategorien ändern') }}</h2>
|
||||
<button type="button" class="icon-button" data-dialog-close aria-label="{{ t('common.cancel') }}">×</button>
|
||||
</div>
|
||||
<p id="category-bulk-selected-count" class="muted"></p>
|
||||
<span id="category-bulk-edit-ids"></span>
|
||||
<label>{{ t('categories.bulk.field', 'Feld') }}
|
||||
<select name="field_name" id="category-bulk-field" required>
|
||||
<option value="description">{{ t('common.description') }}</option>
|
||||
<option value="name" id="category-name-option">{{ t('common.name') }}</option>
|
||||
<option value="image">{{ t('categories.bulk.image', 'Kategoriebild') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<p id="category-name-hint" class="muted">{{ t('categories.bulk.name_hint', 'Der Name kann nur geändert werden, wenn genau eine Kategorie markiert ist.') }}</p>
|
||||
<label id="category-text-value-wrap">{{ t('categories.bulk.value', 'Neuer Inhalt') }}
|
||||
<textarea name="field_value" id="category-bulk-value" rows="4"></textarea>
|
||||
</label>
|
||||
<label id="category-image-value-wrap" hidden>{{ t('categories.bulk.image', 'Kategoriebild') }}
|
||||
<input type="file" name="image" accept="image/*">
|
||||
<span class="muted">{{ t('categories.bulk.image_hint', 'Dasselbe Bild wird allen markierten Kategorien zugewiesen.') }}</span>
|
||||
</label>
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="clear_value" id="category-bulk-clear"> {{ t('categories.bulk.clear', 'Ausgewähltes Feld leeren') }}</label>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="button button-secondary" data-dialog-close>{{ t('common.cancel') }}</button>
|
||||
<button type="submit">💾 {{ t('common.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="category-fields-dialog" class="wide-dialog">
|
||||
<form id="category-fields-form" method="post" action="/categories/bulk-fields">
|
||||
<div class="dialog-heading">
|
||||
<h2>{{ t('categories.fields.manage', 'Felder verwalten') }}</h2>
|
||||
<button type="button" class="icon-button" data-fields-dialog-close aria-label="{{ t('common.cancel') }}">×</button>
|
||||
</div>
|
||||
<p id="category-fields-selected-count" class="muted"></p>
|
||||
<span id="category-fields-ids"></span>
|
||||
|
||||
<div class="dialog-tabs" role="tablist" aria-label="{{ t('categories.fields.manage', 'Felder verwalten') }}">
|
||||
<button type="button" class="button button-secondary category-fields-tab is-active" data-operation="add">{{ t('categories.fields.add', 'Hinzufügen') }}</button>
|
||||
<button type="button" class="button button-secondary category-fields-tab" data-operation="remove">{{ t('categories.fields.remove', 'Entfernen') }}</button>
|
||||
<button type="button" class="button button-secondary category-fields-tab" data-operation="settings">{{ t('categories.fields.settings', 'Einstellungen ändern') }}</button>
|
||||
<button type="button" class="button button-secondary category-fields-tab" data-operation="copy">{{ t('categories.fields.copy', 'Übernehmen') }}</button>
|
||||
</div>
|
||||
<input type="hidden" name="operation" id="category-fields-operation" value="add">
|
||||
|
||||
<section class="category-fields-panel" data-panel="add">
|
||||
<p class="muted">{{ t('categories.fields.add_help', 'Die ausgewählten Felder werden in allen markierten Kategorien aktiviert. Bereits vorhandene Zuordnungen bleiben erhalten.') }}</p>
|
||||
<div class="field-choice-grid">
|
||||
{% for definition in definitions %}
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="field_ids" value="{{ definition.id }}"> {{ t(definition.translation_key, definition.label) }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="category-fields-panel" data-panel="remove" hidden>
|
||||
<p class="muted">{{ t('categories.fields.remove_help', 'Die ausgewählten Felder werden in allen markierten Kategorien deaktiviert. Vorhandene Assetdaten bleiben erhalten. Das Namensfeld kann nicht entfernt werden.') }}</p>
|
||||
<div class="field-choice-grid">
|
||||
{% for definition in definitions if definition.field_name != 'name' %}
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="field_ids" value="{{ definition.id }}" disabled> {{ t(definition.translation_key, definition.label) }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="category-fields-panel" data-panel="settings" hidden>
|
||||
<p class="muted">{{ t('categories.fields.settings_help', 'Die Einstellungen werden nur für Kategorien geändert, in denen das Feld aktiv ist.') }}</p>
|
||||
<label>{{ t('categories.bulk.field', 'Feld') }}
|
||||
<select name="field_definition_id">
|
||||
<option value="">{{ t('common.select', 'Auswählen') }}</option>
|
||||
{% for definition in definitions %}<option value="{{ definition.id }}">{{ t(definition.translation_key, definition.label) }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="settings-bulk-grid">
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="apply_required"> {{ t('categories.fields.change_required', 'Pflichtfeld-Einstellung ändern') }}</label>
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="required_value"> {{ t('categories.required') }}</label>
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="apply_show_in_list"> {{ t('categories.fields.change_list', 'Listenanzeige ändern') }}</label>
|
||||
<label class="checkbox-label"><input class="checkbox" type="checkbox" name="show_in_list_value"> {{ t('categories.in_list') }}</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="category-fields-panel" data-panel="copy" hidden>
|
||||
<p class="muted">{{ t('categories.fields.copy_help', 'Die komplette Feldzuordnung und deren Einstellungen werden aus einer vorhandenen Kategorie auf alle markierten Kategorien übernommen. Nicht in der Vorlage aktive Felder werden in den Zielkategorien deaktiviert; Assetdaten bleiben erhalten.') }}</p>
|
||||
<label>{{ t('categories.fields.source', 'Vorlagenkategorie') }}
|
||||
<select name="source_category_id">
|
||||
<option value="">{{ t('common.select', 'Auswählen') }}</option>
|
||||
{% for c in categories %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div class="notice warning">{{ t('categories.fields.copy_warning', 'Die Feldkonfiguration der markierten Kategorien wird an die Vorlage angeglichen. Kategorienamen, Beschreibungen, Bilder und Assets werden nicht verändert.') }}</div>
|
||||
</section>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="button button-secondary" data-fields-dialog-close>{{ t('common.cancel') }}</button>
|
||||
<button type="submit">💾 {{ t('common.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<div class="table-scroll management-table-scroll">
|
||||
<table class="data-table categories-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="selection-column" data-no-sort="1" data-no-filter="1">
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in categories %}
|
||||
{% set count = asset_counts.get(c.id, 0) %}
|
||||
{% set active_fields = c.visible_fields|selectattr('active')|list %}
|
||||
{% set list_fields = active_fields|selectattr('show_in_list')|list %}
|
||||
<tr>
|
||||
<td class="selection-column">
|
||||
<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">
|
||||
{% 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">
|
||||
<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>
|
||||
<button type="submit" class="button button-danger" form="delete-category-{{ c.id }}"
|
||||
{% if count > 0 %}disabled title="{{ t('categories.in_use', 'Kategorie wird noch verwendet') }}"{% else %}title="{{ t('common.delete') }}"{% endif %}>
|
||||
{{ t('common.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<form id="duplicate-category-{{ c.id }}" method="post" action="/categories/{{ c.id }}/duplicate"></form>
|
||||
<form id="delete-category-{{ c.id }}" method="post" action="/categories/{{ c.id }}/delete"
|
||||
onsubmit="return confirm('{{ t('categories.confirm_delete', 'Kategorie wirklich löschen? Das ist nur möglich, wenn sie von keinem Asset verwendet wird.')|e }}');"></form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const boxes = [...document.querySelectorAll('.category-select')];
|
||||
const all = document.getElementById('select-all-categories');
|
||||
const defaultsButton = document.getElementById('activate-defaults-button');
|
||||
const editButton = document.getElementById('category-bulk-edit-button');
|
||||
const fieldsButton = document.getElementById('category-fields-button');
|
||||
const fieldsDialog = document.getElementById('category-fields-dialog');
|
||||
const fieldsIds = document.getElementById('category-fields-ids');
|
||||
const fieldsSelectedCount = document.getElementById('category-fields-selected-count');
|
||||
const fieldsOperation = document.getElementById('category-fields-operation');
|
||||
const dialog = document.getElementById('category-bulk-edit-dialog');
|
||||
const editIds = document.getElementById('category-bulk-edit-ids');
|
||||
const selectedCount = document.getElementById('category-bulk-selected-count');
|
||||
const fieldSelect = document.getElementById('category-bulk-field');
|
||||
const nameOption = document.getElementById('category-name-option');
|
||||
const nameHint = document.getElementById('category-name-hint');
|
||||
const textWrap = document.getElementById('category-text-value-wrap');
|
||||
const imageWrap = document.getElementById('category-image-value-wrap');
|
||||
const clearBox = document.getElementById('category-bulk-clear');
|
||||
|
||||
const visibleBoxes = () => boxes.filter(box => !box.closest('tr')?.hidden && getComputedStyle(box.closest('tr')).display !== 'none');
|
||||
const selectedBoxes = () => boxes.filter(box => box.checked);
|
||||
|
||||
function refresh() {
|
||||
const selected = selectedBoxes();
|
||||
const count = selected.length;
|
||||
const visible = visibleBoxes();
|
||||
const visibleSelected = visible.filter(box => box.checked).length;
|
||||
const defaultsLabel = defaultsButton.dataset.baseLabel || '✓ {{ t('categories.activate_default_fields', 'Standardfelder aktivieren')|e }}';
|
||||
const fieldsLabel = fieldsButton.dataset.baseLabel || '⚙ {{ t('categories.fields.manage', 'Felder verwalten')|e }}';
|
||||
const editLabel = editButton.dataset.baseLabel || '✏ {{ t('categories.bulk.edit', 'Feld ändern')|e }}';
|
||||
defaultsButton.disabled = count === 0;
|
||||
defaultsButton.textContent = count ? `${defaultsLabel} (${count})` : defaultsLabel;
|
||||
fieldsButton.disabled = count === 0;
|
||||
fieldsButton.textContent = count ? `${fieldsLabel} (${count})` : fieldsLabel;
|
||||
editButton.disabled = count === 0;
|
||||
editButton.textContent = count ? `${editLabel} (${count})` : editLabel;
|
||||
all.checked = visible.length > 0 && visibleSelected === visible.length;
|
||||
all.indeterminate = visibleSelected > 0 && visibleSelected < visible.length;
|
||||
}
|
||||
|
||||
function updateFieldUi() {
|
||||
const isImage = fieldSelect.value === 'image';
|
||||
imageWrap.hidden = !isImage;
|
||||
textWrap.hidden = isImage;
|
||||
clearBox.checked = false;
|
||||
}
|
||||
|
||||
boxes.forEach(box => box.addEventListener('change', refresh));
|
||||
all?.addEventListener('change', () => {
|
||||
visibleBoxes().forEach(box => box.checked = all.checked);
|
||||
refresh();
|
||||
});
|
||||
|
||||
editButton?.addEventListener('click', () => {
|
||||
const selected = selectedBoxes();
|
||||
if (!selected.length) return;
|
||||
editIds.replaceChildren(...selected.map(box => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'category_ids';
|
||||
input.value = box.value;
|
||||
return input;
|
||||
}));
|
||||
const countTemplate = {{ t('categories.bulk.selected_count', '{count} Kategorien ausgewählt')|tojson }};
|
||||
selectedCount.textContent = countTemplate.replace('{count}', selected.length);
|
||||
nameOption.disabled = selected.length !== 1;
|
||||
nameHint.hidden = selected.length === 1;
|
||||
if (nameOption.disabled && fieldSelect.value === 'name') fieldSelect.value = 'description';
|
||||
updateFieldUi();
|
||||
dialog.showModal();
|
||||
});
|
||||
|
||||
fieldsButton?.addEventListener('click', () => {
|
||||
const selected = selectedBoxes();
|
||||
if (!selected.length) return;
|
||||
fieldsIds.replaceChildren(...selected.map(box => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden'; input.name = 'category_ids'; input.value = box.value;
|
||||
return input;
|
||||
}));
|
||||
const countTemplate = {{ t('categories.bulk.selected_count', '{count} Kategorien ausgewählt')|tojson }};
|
||||
fieldsSelectedCount.textContent = countTemplate.replace('{count}', selected.length);
|
||||
fieldsDialog.showModal();
|
||||
});
|
||||
|
||||
function selectFieldsOperation(operation) {
|
||||
fieldsOperation.value = operation;
|
||||
fieldsDialog.querySelectorAll('.category-fields-tab').forEach(button => button.classList.toggle('is-active', button.dataset.operation === operation));
|
||||
fieldsDialog.querySelectorAll('.category-fields-panel').forEach(panel => panel.hidden = panel.dataset.panel !== operation);
|
||||
// Only controls in the active panel are submitted. This prevents duplicate
|
||||
// field_ids from the add/remove panels.
|
||||
fieldsDialog.querySelectorAll('.category-fields-panel').forEach(panel => {
|
||||
panel.querySelectorAll('input, select, textarea').forEach(control => control.disabled = panel.dataset.panel !== operation);
|
||||
});
|
||||
}
|
||||
fieldsDialog?.querySelectorAll('.category-fields-tab').forEach(button => button.addEventListener('click', () => selectFieldsOperation(button.dataset.operation)));
|
||||
fieldsDialog?.querySelectorAll('[data-fields-dialog-close]').forEach(button => button.addEventListener('click', () => fieldsDialog.close()));
|
||||
selectFieldsOperation('add');
|
||||
|
||||
fieldSelect?.addEventListener('change', updateFieldUi);
|
||||
dialog?.querySelectorAll('[data-dialog-close]').forEach(button => button.addEventListener('click', () => dialog.close()));
|
||||
|
||||
// Table filters can change row visibility without changing checkboxes. Keep the
|
||||
// selection state consistent whenever the shared table tools signal activity.
|
||||
document.addEventListener('input', event => {
|
||||
if (event.target.closest('.categories-table')) requestAnimationFrame(refresh);
|
||||
});
|
||||
document.addEventListener('change', event => {
|
||||
if (event.target.closest('.categories-table')) requestAnimationFrame(refresh);
|
||||
});
|
||||
refresh();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user