snapshot: preserve scalable app registry and landing visibility rules
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{% extends 'workflows/base_shell.html' %}
|
||||
{% load static i18n %}
|
||||
{% trans "Ungespeicherte Änderungen" as dirty_state_label %}
|
||||
|
||||
{% block title %}{% trans "App Registry" %}{% endblock %}
|
||||
|
||||
@@ -21,63 +22,220 @@
|
||||
</div>
|
||||
<form method="post" action="{% url 'save_portal_app_registry' %}" class="stack-form">
|
||||
{% csrf_token %}
|
||||
<div class="table-wrap app-registry-wrap">
|
||||
<table class="app-registry-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Key" %}</th>
|
||||
<th>{% trans "Aktiv" %}</th>
|
||||
<th>{% trans "Bereich" %}</th>
|
||||
<th>{% trans "Reihenfolge" %}</th>
|
||||
<th>{% trans "Titel DE" %}</th>
|
||||
<th>{% trans "Titel EN" %}</th>
|
||||
<th>{% trans "Aktion DE" %}</th>
|
||||
<th>{% trans "Aktion EN" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<div><strong>{{ row.config.key }}</strong></div>
|
||||
<div class="mini">{{ row.definition.title }}</div>
|
||||
</td>
|
||||
<td class="select-col">
|
||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
||||
</td>
|
||||
<td>
|
||||
<select name="section__{{ row.config.key }}">
|
||||
{% for value, label in section_choices %}
|
||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
||||
<textarea name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
||||
<textarea name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<section class="app-registry-filters">
|
||||
<div class="field">
|
||||
<label for="app-registry-search">{% trans "Suche" %}</label>
|
||||
<input id="app-registry-search" type="text" placeholder="{% trans 'Nach App-Name oder Key filtern' %}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="app-registry-state">{% trans "Status" %}</label>
|
||||
<select id="app-registry-state">
|
||||
<option value="all">{% trans "Alle" %}</option>
|
||||
<option value="enabled">{% trans "Aktiv" %}</option>
|
||||
<option value="disabled">{% trans "Deaktiviert" %}</option>
|
||||
<option value="platform_only">{% trans "Platform only" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="app-registry-section">{% trans "Bereich" %}</label>
|
||||
<select id="app-registry-section">
|
||||
<option value="all">{% trans "Alle" %}</option>
|
||||
<option value="apps">{% trans "Apps" %}</option>
|
||||
<option value="platform_apps">{% trans "Platform Apps" %}</option>
|
||||
<option value="admin_apps">{% trans "Admin Apps" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<div class="app-registry-cards">
|
||||
{% for row in rows %}
|
||||
<details class="app-registry-card{% if not row.config.is_enabled %} is-disabled{% endif %}" data-app-card
|
||||
data-key="{{ row.config.key|lower }}"
|
||||
data-title="{{ row.definition.title|lower }}"
|
||||
data-enabled="{% if row.config.is_enabled %}1{% else %}0{% endif %}"
|
||||
data-platform-only="{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}1{% else %}0{% endif %}"
|
||||
data-section="{{ row.config.section }}">
|
||||
<summary class="app-registry-summary">
|
||||
<div class="app-registry-summary-main">
|
||||
<div class="app-registry-card-title-row">
|
||||
<h2>{{ row.definition.title }}</h2>
|
||||
{% if row.config.is_enabled %}
|
||||
<span class="badge sent">{% trans "Aktiv" %}</span>
|
||||
{% else %}
|
||||
<span class="badge cancelled">{% trans "Deaktiviert" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mini">{{ row.config.key }}</div>
|
||||
<p class="app-registry-card-copy">{{ row.definition.description }}</p>
|
||||
<p class="mini">{% trans "Empfohlener Standardzugriff:" %} {{ row.default_visibility_summary }}</p>
|
||||
</div>
|
||||
<div class="app-registry-summary-meta">
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.section == 'platform_apps' %}
|
||||
{% trans "Platform Apps" %}
|
||||
{% elif row.config.section == 'admin_apps' %}
|
||||
{% trans "Admin Apps" %}
|
||||
{% else %}
|
||||
{% trans "Apps" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="badge">{% trans "Sortierung" %}: {{ row.config.sort_order }}</span>
|
||||
{% if not row.config.visible_to_super_admin and not row.config.visible_to_admin and not row.config.visible_to_it_staff and not row.config.visible_to_staff %}
|
||||
<span class="badge paused">{% trans "Platform only" %}</span>
|
||||
{% elif row.config.visible_to_super_admin and row.config.visible_to_admin and row.config.visible_to_it_staff and row.config.visible_to_staff %}
|
||||
<span class="badge sent">{% trans "Alle Firmenrollen" %}</span>
|
||||
{% else %}
|
||||
<span class="badge scheduled">
|
||||
{% if row.config.visible_to_super_admin %}{% trans "Super Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_admin %}{% if row.config.visible_to_super_admin %} + {% endif %}{% trans "Admin" %}{% endif %}
|
||||
{% if row.config.visible_to_it_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin %} + {% endif %}{% trans "IT Staff" %}{% endif %}
|
||||
{% if row.config.visible_to_staff %}{% if row.config.visible_to_super_admin or row.config.visible_to_admin or row.config.visible_to_it_staff %} + {% endif %}{% trans "Staff" %}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
<div class="app-registry-card-grid">
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Verfügbarkeit" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="is_enabled__{{ row.config.key }}" {% if row.config.is_enabled %}checked{% endif %} />
|
||||
<span>{% trans "App aktiviert" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Deaktivierte Apps erscheinen nicht auf der Landing Page, selbst wenn Rollen sie sehen dürften." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Sichtbarkeit nach Rolle" %}</h3>
|
||||
<div class="check-row app-registry-checks">
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_super_admin__{{ row.config.key }}" {% if row.config.visible_to_super_admin %}checked{% endif %} />
|
||||
<span>{% trans "Super Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_admin__{{ row.config.key }}" {% if row.config.visible_to_admin %}checked{% endif %} />
|
||||
<span>{% trans "Admin" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_it_staff__{{ row.config.key }}" {% if row.config.visible_to_it_staff %}checked{% endif %} />
|
||||
<span>{% trans "IT Staff" %}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="visible_to_staff__{{ row.config.key }}" {% if row.config.visible_to_staff %}checked{% endif %} />
|
||||
<span>{% trans "Staff" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">{% trans "Wenn keine Firmenrolle aktiv ist, bleibt die App nur für die Platform sichtbar." %}</p>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel">
|
||||
<h3>{% trans "Platzierung" %}</h3>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="section__{{ row.config.key }}">{% trans "Bereich" %}</label>
|
||||
<select id="section__{{ row.config.key }}" name="section__{{ row.config.key }}">
|
||||
{% for value, label in section_choices %}
|
||||
<option value="{{ value }}"{% if row.config.section == value %} selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="sort_order__{{ row.config.key }}">{% trans "Reihenfolge" %}</label>
|
||||
<input id="sort_order__{{ row.config.key }}" type="number" name="sort_order__{{ row.config.key }}" value="{{ row.config.sort_order }}" min="0" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="app-registry-panel app-registry-copy-panel">
|
||||
<h3>{% trans "Bezeichnungen & Texte" %}</h3>
|
||||
<div class="grid lang-pairs">
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "Deutsch" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override__{{ row.config.key }}">{% trans "Titel" %}</label>
|
||||
<input id="title_override__{{ row.config.key }}" type="text" name="title_override__{{ row.config.key }}" value="{{ row.config.title_override }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override__{{ row.config.key }}">{% trans "Beschreibung" %}</label>
|
||||
<textarea id="description_override__{{ row.config.key }}" name="description_override__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override__{{ row.config.key }}">{% trans "Aktionslabel" %}</label>
|
||||
<input id="action_label_override__{{ row.config.key }}" type="text" name="action_label_override__{{ row.config.key }}" value="{{ row.config.action_label_override }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="lang-block">
|
||||
<h4>{% trans "English" %}</h4>
|
||||
<div class="field">
|
||||
<label for="title_override_en__{{ row.config.key }}">{% trans "Title" %}</label>
|
||||
<input id="title_override_en__{{ row.config.key }}" type="text" name="title_override_en__{{ row.config.key }}" value="{{ row.config.title_override_en }}" placeholder="{{ row.definition.title }}" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="description_override_en__{{ row.config.key }}">{% trans "Description" %}</label>
|
||||
<textarea id="description_override_en__{{ row.config.key }}" name="description_override_en__{{ row.config.key }}" rows="3" placeholder="{{ row.definition.description }}">{{ row.config.description_override_en }}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="action_label_override_en__{{ row.config.key }}">{% trans "Action label" %}</label>
|
||||
<input id="action_label_override_en__{{ row.config.key }}" type="text" name="action_label_override_en__{{ row.config.key }}" value="{{ row.config.action_label_override_en }}" placeholder="{{ row.definition.action_label }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="toolbar" style="margin-top:1rem;">
|
||||
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
|
||||
<div class="app-registry-savebar">
|
||||
<div>
|
||||
<div class="hint">{% trans "Empfehlung: Produktweite Apps sparsam halten, kundenbezogene Prozesse unter Apps oder Admin Apps einordnen." %}</div>
|
||||
<div class="mini" id="app-registry-dirty-state">{% trans "Keine ungespeicherten Änderungen" %}</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "App Registry speichern" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
(function () {
|
||||
const searchInput = document.getElementById('app-registry-search');
|
||||
const stateSelect = document.getElementById('app-registry-state');
|
||||
const sectionSelect = document.getElementById('app-registry-section');
|
||||
const cards = Array.from(document.querySelectorAll('[data-app-card]'));
|
||||
const form = document.querySelector('form[action$="app-registry/save/"], form[action*="save_portal_app_registry"]') || document.querySelector('.stack-form');
|
||||
const dirtyState = document.getElementById('app-registry-dirty-state');
|
||||
|
||||
function applyFilters() {
|
||||
const query = (searchInput?.value || '').trim().toLowerCase();
|
||||
const state = stateSelect?.value || 'all';
|
||||
const section = sectionSelect?.value || 'all';
|
||||
|
||||
cards.forEach((card) => {
|
||||
const matchesQuery = !query || card.dataset.key.includes(query) || card.dataset.title.includes(query);
|
||||
const matchesState =
|
||||
state === 'all' ||
|
||||
(state === 'enabled' && card.dataset.enabled === '1') ||
|
||||
(state === 'disabled' && card.dataset.enabled === '0') ||
|
||||
(state === 'platform_only' && card.dataset.platformOnly === '1');
|
||||
const matchesSection = section === 'all' || card.dataset.section === section;
|
||||
card.hidden = !(matchesQuery && matchesState && matchesSection);
|
||||
});
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
if (!dirtyState) return;
|
||||
dirtyState.textContent = "{{ dirty_state_label|escapejs }}";
|
||||
}
|
||||
|
||||
searchInput?.addEventListener('input', applyFilters);
|
||||
stateSelect?.addEventListener('change', applyFilters);
|
||||
sectionSelect?.addEventListener('change', applyFilters);
|
||||
form?.addEventListener('input', markDirty);
|
||||
form?.addEventListener('change', markDirty);
|
||||
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<span class="eyebrow">{% trans "Operations Console" %}</span>
|
||||
<h1>{{ portal_title }}</h1>
|
||||
<p>{% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}</p>
|
||||
{% if can_manage_integrations %}
|
||||
<div class="status-row">
|
||||
<span class="status-pill status-pill-neutral">{% trans "Rolle:" %} {{ role_label }}</span>
|
||||
<span class="status-pill {% if nextcloud_enabled %}ok{% else %}warn{% endif %}">
|
||||
@@ -44,6 +45,7 @@
|
||||
</span>
|
||||
<span class="status-pill status-pill-neutral">{% trans "PDF + E-Mail Workflow bereit" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user