Files
emailtemplate.it/public/assets/js/ui-list.js
2026-01-19 01:20:10 +01:00

354 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { apiList, apiGet, apiDelete, apiUpdate, apiAction, toast } from './api.js';
function formatUsage(usage){
if (!usage || !usage.total) return '';
const parts=[];
if (usage.templates) parts.push(`${usage.templates} Template${usage.templates!==1?'s':''}`);
if (usage.sections) parts.push(`${usage.sections} Section${usage.sections!==1?'s':''}`);
if (usage.blocks) parts.push(`${usage.blocks} Block${usage.blocks!==1?'s':''}`);
if (usage.snippets) parts.push(`${usage.snippets} Snippet${usage.snippets!==1?'s':''}`);
if (!parts.length) return '';
return `<div class="mt-3 text-sm text-rose-600">
Dieses Element wird aktuell verwendet in: <strong>${parts.join(', ')}</strong>.<br>
Das Löschen entfernt diese Referenzen.
</div>`;
}
function esc(s=''){
return String(s)
.replace(/&/g,'&amp;')
.replace(/</g,'&lt;')
.replace(/>/g,'&gt;')
.replace(/"/g,'&quot;')
.replace(/'/g,'&#39;');
}
function normalizeApiName(v=''){
return String(v)
.trim()
.toLowerCase()
.replace(/\s+/g,'-')
.replace(/[^a-z0-9_-]+/g,'-')
.replace(/-+/g,'-')
.replace(/^-|-$/g,'');
}
async function openSnippetEditor(id){
let resp = {};
try { resp = await apiGet('snippets', id) || {}; } catch(e){}
const row = resp?.item || resp?.data || resp || {};
const name = row?.name || '';
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
window.EditorUI.open({ id: Number(id), name }, 'snippets');
return;
}
if (window.__openEditor) {
window.__openEditor({ resource: 'snippets', id: Number(id), name });
return;
}
console.warn('Kein Editor-Entry-Point gefunden (EditorUI.open / __openEditor).');
toast('Editor ist nicht initialisiert.', false);
}
async function openTemplateEditor(id){
const dlg = document.getElementById('editTemplateDialog');
const form = document.getElementById('editTemplateForm');
const inpName = document.getElementById('edit_tpl_name');
const inpApiName = document.getElementById('edit_tpl_api_name');
const apiWarn = document.getElementById('edit_tpl_api_warn');
const btnCancel = document.getElementById('editTemplateCancel');
let resp = {};
try { resp = await apiGet('templates', id) || {}; } catch(e){}
const row = resp?.item || resp?.data || resp || {};
const initialApi = row.api_name || '';
if (inpName) inpName.value = row.name || '';
if (inpApiName) inpApiName.value = initialApi;
if (apiWarn) apiWarn.classList.add('hidden');
const onApiInput = () => {
if (!inpApiName) return;
const next = normalizeApiName(inpApiName.value);
if (next !== inpApiName.value) inpApiName.value = next;
if (apiWarn) {
apiWarn.classList.toggle('hidden', inpApiName.value.trim() === initialApi);
}
};
function cleanup(){
form && form.removeEventListener('submit', onSubmit);
btnCancel && (btnCancel.onclick = null);
inpApiName && inpApiName.removeEventListener('input', onApiInput);
}
async function onSubmit(ev){
ev.preventDefault();
try{
const res = await apiUpdate('templates', id, {
name: inpName ? inpName.value : '',
api_name: inpApiName ? inpApiName.value : ''
});
toast(res && res.ok ? 'Template gespeichert' : 'Speichern fehlgeschlagen', !!(res && res.ok));
dlg && dlg.close();
cleanup();
loadList('templates');
}catch(e){
toast('Speichern fehlgeschlagen', false);
}
}
inpApiName && inpApiName.addEventListener('input', onApiInput);
form && form.addEventListener('submit', onSubmit, { once:false });
btnCancel && (btnCancel.onclick = () => { dlg && dlg.close(); cleanup(); });
dlg && dlg.showModal();
}
export async function loadList(resource){
const el=document.getElementById(`view-${resource}`); if(!el) return;
const label = resource.charAt(0).toUpperCase()+resource.slice(1);
el.innerHTML=`<div class='rounded-2xl border bg-white overflow-hidden'>
<div class='px-4 py-2 border-b bg-gray-50 text-sm font-medium flex items-center gap-3'>
<span>${label}</span>
<div class='ms-auto flex items-center gap-2'>
<div class='flex items-center gap-1'>
<input id='filter-${resource}' class='input text-sm' placeholder='Suche Name/API' />
<button id='filter-${resource}-reset' class='btn' type='button' title='Suche zurücksetzen'>×</button>
</div>
<select id='sort-${resource}' class='input text-sm'>
<option value='created_asc'>Erstelldatum (aufsteigend)</option>
<option value='name_asc'>Name AZ</option>
<option value='name_desc'>Name ZA</option>
<option value='updated_desc'>Zuletzt bearbeitet</option>
</select>
</div>
</div>
<div id='list-${resource}' class='divide-y'>Lade …</div></div>`;
const data=await apiList(resource);
const list=el.querySelector(`#list-${resource}`);
const filterInput=el.querySelector(`#filter-${resource}`);
const filterReset=el.querySelector(`#filter-${resource}-reset`);
const sortSelect=el.querySelector(`#sort-${resource}`);
if(!Array.isArray(data)||data.length===0){
list.innerHTML=`<div class='p-4 text-sm text-gray-500'>Keine Einträge</div>`;
return;
}
const sortDefault = (window.__listSortDefault || 'created_asc');
if (sortSelect) sortSelect.value = sortDefault;
function compareByName(a, b) {
const av = String(a?.name || '');
const bv = String(b?.name || '');
return av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
}
function parseDate(value) {
const t = Date.parse(value || '');
return Number.isFinite(t) ? t : 0;
}
function sortItems(items, key) {
const listCopy = items.slice();
if (key === 'name_asc') {
return listCopy.sort(compareByName);
}
if (key === 'name_desc') {
return listCopy.sort((a,b)=>compareByName(b,a));
}
if (key === 'updated_desc') {
return listCopy.sort((a,b)=>parseDate(b.updated_at) - parseDate(a.updated_at));
}
return listCopy.sort((a,b)=>parseDate(a.created_at) - parseDate(b.created_at));
}
function matchesQuery(item, query) {
if (!query) return true;
const q = query.toLowerCase();
const name = String(item?.name || '').toLowerCase();
const api = resource === 'templates' ? String(item?.api_name || '').toLowerCase() : '';
return name.includes(q) || (api && api.includes(q));
}
function parentBadge(r,it){
if(r==='sections'&&it.template_id) return `<span class="chip"><span class="dot"></span> Template&nbsp;#${it.template_id}${it.template_name ? ' · '+esc(it.template_name) : ''}</span>`;
if(r==='blocks'&&it.section_id) return `<span class="chip"><span class="dot"></span> Section&nbsp;#${it.section_id}${it.section_name ? ' · '+esc(it.section_name) : ''}</span>`;
if(r==='snippets'&&it.block_id) return `<span class="chip"><span class="dot"></span> Block&nbsp;#${it.block_id}${it.block_name ? ' · '+esc(it.block_name) : ''}</span>`;
return '<span class="chip"><span class="dot"></span> frei</span>';
}
function render(items){
list.innerHTML=items.map(item=>{
const name = esc(item.name||'');
const apiName = resource==='templates' ? esc(item.api_name||'') : '';
const apiLine = (resource==='templates' && apiName) ? `<div class='text-xs text-slate-500'>API: ${apiName}</div>` : '';
const nameCell = `<div class='min-w-48'>
<div class='font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
${apiLine}
</div>`;
const openBtn = (['templates','sections','blocks'].includes(resource))
? `<button class='btn' data-open='${resource}:${item.id}'>Im E-Mail-Editor öffnen</button>` : '';
const editBtn = (resource==='snippets')
? `<button class='btn' data-edit='snippets:${item.id}'>Im Editor</button>` : '';
const editTplBtn = (resource==='templates')
? `<button class='btn' data-edit='templates:${item.id}'>Bearbeiten</button>` : '';
const testBtn = resource==='templates'
? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
const prevBtn = `<button class='btn' data-preview='${resource}:${item.id}'>Vorschau</button>`;
const delBtn = `<button class='btn btn-danger' data-del='${resource}:${item.id}' data-name='${name}'>Löschen</button>`;
return `<div class='p-3 flex items-center gap-3'>
${nameCell}
<div class='text-xs text-gray-500'>#${item.id}</div>
<div class='text-xs'>${parentBadge(resource,item)}</div>
<div class='ms-auto flex gap-2'>${[openBtn, editBtn, editTplBtn, testBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
</div>`;
}).join('');
bindListHandlers(list, resource);
}
function applyFilter(){
const query = (filterInput?.value || '').trim();
const sortKey = sortSelect?.value || sortDefault;
const filtered = data.filter(item => matchesQuery(item, query));
const sorted = sortItems(filtered, sortKey);
render(sorted);
}
async function persistSort(nextValue){
window.__listSortDefault = nextValue;
try {
await apiAction('account.settings.update', { method: 'POST', data: { list_sort: nextValue } });
} catch {}
}
if (filterInput) {
filterInput.addEventListener('input', applyFilter);
}
if (filterReset) {
filterReset.addEventListener('click', () => {
if (filterInput) {
filterInput.value = '';
filterInput.focus();
}
applyFilter();
});
}
if (sortSelect) {
sortSelect.addEventListener('change', () => {
const next = sortSelect.value || sortDefault;
persistSort(next);
applyFilter();
});
}
applyFilter();
const delDlg=document.getElementById('deleteDialog'),
delText=document.getElementById('deleteText'),
delForm=document.getElementById('deleteForm'),
delCancel=document.getElementById('deleteCancel');
let pending=null;
delCancel && (delCancel.onclick=()=>{pending=null;delDlg.close();});
// --- Editor öffnen (ANPASSUNG) -----------------------------------------
function bindListHandlers(scope, resName){
scope.querySelectorAll('[data-open]').forEach(b=>b.addEventListener('click', async ()=>{
const [res,id]=b.dataset.open.split(':');
// Detail laden, um Name + aktuellen HTML/Content zu haben
const obj = await apiGet(res,id);
const name = obj?.name || '';
const html = obj ? (obj.html ?? obj.content ?? '') : '';
// Globale Kontexte (werden von Editor/anderen Modulen genutzt)
window.__currentItemId = Number(id);
window.__currentEditorCtx = { id:Number(id), mode:res };
// Bevorzugt EditorUI.open nutzen; Fallback: __openEditor (Bestand)
if (window.EditorUI && typeof window.EditorUI.open === 'function') {
window.EditorUI.open({ id:Number(id), name, html }, res);
} else if (window.__openEditor) {
window.__openEditor({ resource:res, id:Number(id), name, html });
} else {
console.warn('Kein Editor-Entry-Point gefunden (EditorUI.open / __openEditor).');
toast('Editor ist nicht initialisiert.', false);
}
}));
// -----------------------------------------------------------------------
// edit snippet
scope.querySelectorAll('[data-edit]').forEach(b=>b.addEventListener('click', async ()=>{
const [res, id] = b.dataset.edit.split(':');
if (res === 'snippets') await openSnippetEditor(id);
if (res === 'templates') await openTemplateEditor(id);
}));
// preview
const prevDlg=document.getElementById('previewDialog'), prevFrame=document.getElementById('previewFrame');
scope.querySelectorAll('[data-preview]').forEach(b=>b.addEventListener('click', async ()=>{
const [res,id]=b.dataset.preview.split(':');
const obj=await apiGet(res,id);
const html=(obj?.html||obj?.content||'<em>(leer)</em)');
prevFrame.srcdoc='<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>'+html+'</body></html>';
prevDlg.showModal();
}));
// test send (templates only)
scope.querySelectorAll('[data-test]').forEach(b=>b.addEventListener('click', ()=>{
const id = Number(b.dataset.test || '0');
const nm = b.dataset.name || '';
if (!id) {
toast('Testversand: Ungültige ID', false);
return;
}
if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') {
window.AdminTestSend.open({ id, name: nm });
} else {
toast('Testversand ist aktuell nicht verfügbar.', false);
}
}));
// delete
scope.querySelectorAll('[data-del]').forEach(b=>b.addEventListener('click', async ()=>{
const [res,id]=b.dataset.del.split(':'); const nm=b.dataset.name||'';
let usage = null;
try {
const detail = await apiGet(res, id);
usage = detail?.usage || null;
} catch {}
pending={res,id,nm,usage};
const usageWarn = formatUsage(usage);
delText && (delText.innerHTML=`Soll <strong>${nm || '(ohne Name)'} #${id}</strong> aus <strong>${res}</strong> wirklich gelöscht werden?<br><span class="text-rose-600">Achtung:</span> Kinder-Elemente werden <em>nicht</em> automatisch mit gelöscht.${usageWarn}`);
delDlg.showModal();
}));
}
delForm && (delForm.onsubmit=async(e)=>{
e.preventDefault();
if(!pending) return delDlg.close();
const r=await apiDelete(pending.res,pending.id);
delDlg.close();
toast(r&&r.ok?'Gelöscht':'Löschen fehlgeschlagen', !!(r&&r.ok), {duration:3000});
loadList(resource);
});
}
export function initLists(){
loadList('templates');
// Public reload helper (wird vom Snippet-Editor genutzt)
window.__reloadList = loadList;
// Backwards compat (falls woanders genutzt)
window.loadList = loadList;
}