Files
emailtemplate.it/public/assets/js/bridge/library-api.js
2026-02-09 01:38:39 +01:00

283 lines
13 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* /assets/js/bridge/library-api.js (FINAL & KORRIGIERT FÜR FLEXIBLE API-BASES) */
(function(B){
    
    // 🛑 WICHTIG: Globalen Cache-Speicher initialisieren (wird von blocks-api.js gelesen)
    B.ApiItemCache = B.ApiItemCache || {};
    if (!B || typeof grapesjs === 'undefined') return;
    const PluginName = 'bridge-library-api';
// NEU: Standard-API-Basis für Abwärtskompatibilität, falls nichts konfiguriert
const API_BASE_FALLBACK = (B.API_BASE || '/api/editor');
    // Konstanten
    const TARGET_CAT_ID = 'custom';
const PLACEHOLDER_ID = 'api-placeholder-loading';
const REFERENCE_COMPONENT_TYPE = 'library-reference';
    if (B.LOG_CONFIG && B.LOG_CONFIG.PLUGINS) {
        B.LOG_CONFIG.PLUGINS[PluginName] = true; 
    }
    const log = (type, message, color = '#6A5ACD', logType = 'info', force = false) => {
        if (typeof B.log === 'function') {
            B.log(PluginName, `[${type}] ${message}`, color, logType, force);
        } else {
            if (logType === 'error') {
                 console.error(`%c[${PluginName} - ${type}] %c${message}`, 'color:red; font-weight:bold;', 'color:inherit;');
            }
        }
    };
    const logApiData = (data) => B.logData(PluginName, data);
    log('INIT', 'API-Schicht initialisiert.');
    
    // --- HILFSFUNKTIONEN ---
/**
* Gibt die korrekte API-Basis-URL für einen Ressourcentyp (kind) zurück.
* Nutzt die zentrale Map B.RESOURCE_API_BASES, die in category-config.js gefüllt wurde.
*/
const getApiBase = (resource) => {
// Fallback auf die konfigurierte Standard-Basis, falls die Map noch nicht existiert oder der Eintrag fehlt.
return (B.RESOURCE_API_BASES && B.RESOURCE_API_BASES[resource]) || API_BASE_FALLBACK;
};
    const buildApiUrl = (resource, action='list', params = {}) => {
// KORREKTUR: Nutzt jetzt die dynamisch ermittelte API-Basis
const apiBase = getApiBase(resource);
        const url = new URL(apiBase, window.location.origin);
        
        url.searchParams.set('resource', resource);
        url.searchParams.set('action', action);
        
        Object.entries(params).forEach(([key, value]) => {
            if (value !== null && value !== undefined) url.searchParams.set(key, value);
        });
        return url.toString();
    };
    const shouldLoad = (resource) => {
        if (Array.isArray(B.ALLOWED_SECTION_SLUGS) && B.ALLOWED_SECTION_SLUGS.length) {
            return B.ALLOWED_SECTION_SLUGS.includes(resource);
        }
        const mode = (B.EDITOR_MODE || 'TEMPLATES').toUpperCase();
        switch (mode) {
            case 'TEMPLATES':
                return true;
            default:
                return true;
        }
    };
    const fetchSectionsConfig = async () => {
        const rows = await fetchData('sections_config', 'list');
        return Array.isArray(rows) ? rows : [];
    };
    const resolveAllowedSections = async () => {
        const sections = await fetchSectionsConfig();
        if (!sections.length) return [];
        const modeSlug = String(B.EDITOR_MODE || '').toLowerCase();
        const current = sections.find(s => String(s.slug || '').toLowerCase() === modeSlug);
        if (!current) return sections;
        return sections.filter(s => Number(s.position) > Number(current.position));
    };
    const fetchData = (resource, action='list', params = {}) => {
// ... (Rest der fetchData-Funktion bleibt unverändert, nutzt aber die korrigierte buildApiUrl)
        const url = buildApiUrl(resource, action, params); 
        const cacheKey = action === 'get' ? `${resource}-${params.id}` : null;
        // Cache-Check verwendet B.ApiItemCache
        if (cacheKey && B.ApiItemCache.hasOwnProperty(cacheKey)) {
            log('CACHE HIT', `Cache Hit für /${resource}-${cacheKey}.`, '#708090', 'info');
            return Promise.resolve(B.ApiItemCache[cacheKey]);
        }
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
            }, 
        })
            .then(response => {
                if (!response.ok) {
                    log('API ERROR', `API-Aufruf fehlgeschlagen für /${resource}/${action}: ${response.status} (${response.statusText})`, 'red', 'error');
                    // 💡 KORREKTUR: Bei HTTP-Fehler immer ein leeres Array für LIST und leeres Objekt für GET zurückgeben.
                    return action === 'get' ? {} : { items: [] }; 
                }
                return response.json();
            })
            .then(data => {
                if (data.ok === false) {
                    log('API ERROR', `API-Fehler für /${resource}: ${data.error || 'Unbekannt'}`, 'red', 'error');
                    // 💡 KORREKTUR: Bei API-Fehler ('ok: false') immer leeres Array/Objekt zurückgeben.
                    return action === 'get' ? {} : { items: [] };
                }
                
const result = data.items || data.data || data.item;
let finalResult = result ? (Array.isArray(result) ? result : (action === 'list' ? (result.items || []) : result)) : (action === 'list' ? [] : {});
const resultIsArray = Array.isArray(finalResult);
const resultLength = resultIsArray ? finalResult.length : (Object.keys(finalResult).length > 0 ? 1 : 0);
                log('EXTRACT SUCCESS', `Extrahiert ${resultLength} Elemente (Typ: ${action}) für /${resource}.`);
                
// Bei GET-Antworten stellt der Server häufig html/content separat bereit.
if (action === 'get' && finalResult && !resultIsArray) {
if (data && typeof data.html !== 'undefined' && !('html' in finalResult)) {
finalResult.html = data.html;
}
if (data && typeof data.content !== 'undefined' && !('content' in finalResult)) {
finalResult.content = data.content;
}
}
// Cache-Speicherung verwendet B.ApiItemCache
if (cacheKey && resultLength > 0) {
B.ApiItemCache[cacheKey] = finalResult;
}
                
                // 💡 KORREKTUR: Bei LIST (action='list') geben wir immer ein Array zurück, sonst das Objekt
                return finalResult;
            })
            .catch(error => {
                log('FETCH ERROR', `FEHLER beim Fetchen oder Parsen von /${resource}: ${error.message}`, 'red', 'error', true);
                return action === 'get' ? {} : [];
            });
    };
// --- Exportierte Core-Funktionen (jetzt generisch) ---
// NEU: Generische Fetch-Funktion für jeden Ressourcentyp ('kind')
    B.fetchResource = (kind) => {
        if (B.USE_DYNAMIC_SECTIONS && ['templates','sections','blocks','snippets'].includes(String(kind || '').toLowerCase())) {
            return Promise.resolve([]);
        }
        if (!shouldLoad(kind)) {
            log('BLOCKED', `Blockiert: ${kind} (Modus: ${B.EDITOR_MODE})`, '#708090', 'info');
            return Promise.resolve([]);
        }
        return fetchData(kind).then(items => Array.isArray(items) ? items : []);
    };
// Die alten hardcodierten Funktionen verwenden jetzt die neue generische Funktion
B.fetchTemplates = () => B.fetchResource('templates');
    B.fetchSnippets = () => B.fetchResource('snippets');
    B.fetchSections = () => B.fetchResource('sections');
    B.fetchBlocks = () => B.fetchResource('blocks');
    B.getApiItem = (kind, id) => {
        const normalized = String(kind || '').toLowerCase();
        const legacyKinds = ['templates', 'sections', 'blocks', 'snippets', 'content', 'sections_config', 'content_versions'];
        if (B.USE_DYNAMIC_SECTIONS && !legacyKinds.includes(normalized)) {
            return fetchData('content', 'get', { id: id, section_slug: normalized, active_only: 1 });
       }
        return fetchData(normalized || kind, 'get', { id: id });
    };
    B.clearApiCache = () => {
        B.ApiItemCache = {}; // Cache leeren
        log('CACHE CLEAR', `API-Cache geleert.`, 'orange', 'warn');
    };
    // 🚀 Zentrale Funktion zum Laden und Registrieren der Blöcke
    B.loadAndRegisterApiBlocks = (editor) => {
const bm = editor.BlockManager;
const loadDynamic = async () => {
const sections = await resolveAllowedSections();
B.ALLOWED_SECTION_SLUGS = sections.map(s => String(s.slug || '').toLowerCase());
if (!sections.length) {
bm.remove(PLACEHOLDER_ID);
return [];
}
const promises = sections.map(section =>
fetchData('content', 'list', { section_id: section.id, active_only: 1 })
.then(items => (Array.isArray(items) ? items : []).map(i => ({
...i,
kind: String(section.slug || '').toLowerCase(),
section_name: section.name || '',
})))
);
const results = await Promise.all(promises);
return results.flat();
};
const startLoad = (B.USE_DYNAMIC_SECTIONS ? loadDynamic() : Promise.resolve([]));
log('API START', 'Starte API-Abruf für dynamische Sections...', '#1E90FF');
startLoad
.then(apiItems => {
const filtered = Array.isArray(apiItems) ? apiItems.filter(item => item && item.id) : [];
 
log(`API SUCCESS`, `${filtered.length} Elemente gefunden.`, '#9400D3');
logApiData(filtered); 
 
if (filtered.length === 0) {
log('NO DATA', 'Keine API-Daten gefunden.', 'orange', 'warn', true);
bm.remove(PLACEHOLDER_ID);
} else {
filtered.forEach(item => {
const blockId = `lib-${item.kind}-${item.id}`;
const label = item.name || item.label || 'Unbenannter Block';
const itemKindUpper = item.kind.toUpperCase();
 
// Hier wird der Block-Manager-Block registriert
// ... (Der Rest der Logik bleibt unverändert) ...
const blockDefinition = {
label: label,
category: TARGET_CAT_ID,
                           // 💡 KORREKTUR: Immer die library-reference-Komponente verwenden, um die Referenz-Logik
                           // (mit editable: false) aus blocks-api.js zu erzwingen.
content: {
type: REFERENCE_COMPONENT_TYPE,
'lib-kind': item.kind,
'lib-id': item.id,
attributes: {
'data-lib-kind': item.kind,
'data-lib-id': item.id,
'data-lib-ref': '1',
},
                                 // NEU: startContent wird nur als reines HTML übergeben.
                                 // Die Logik in blocks-api.js (init/reloadComponentContent) kümmert sich um die Anzeige.
startContent: item.html || item.content || '<div style="padding: 10px; color: #dc3545; background-color: #fce7f3; border: 1px solid #fbcfe8; text-align: center;">🛑 Fehler: Inhalt fehlte beim Laden.</div>',
                                 content: '', // Wichtig: Beim Drop keinen GrapesJS-Content setzen
},
attributes: { 'title': itemKindUpper },
media: item.preview_url ? `<img src="${item.preview_url}">` : '',
};
bm.add(blockId, blockDefinition);
});
 
bm.remove(PLACEHOLDER_ID);
log(`REGISTRATION`, `${apiItems.length} API-Blöcke registriert. Platzhalter entfernt.`, '#008000');
const reloadExistingComponents = () => {
const allComponents = editor.DomComponents.getWrapper().find(`[data-gjs-type="${REFERENCE_COMPONENT_TYPE}"]`);
allComponents.forEach(component => {
if (component.get('lib-id') && component.components().length === 0 && typeof component.reloadComponentContent === 'function') {
log(`RELOAD START`, `Lade ${component.get('lib-kind')}/${component.get('lib-id')} nach Cache-Füllung (Sicherheitsnetz).`, '#FF4500');
component.reloadComponentContent({ forced: true, reason: 'EXISTING_CONTENT_RELOAD' }); 
}
});
};
setTimeout(reloadExistingComponents, 100);
}
})
.catch(error => {
// Hier wird der Fehler von fetchData oder map abgefangen
log('FETCH ERROR', `FEHLER beim Laden der API-Blöcke: ${error.message}`, '#dc3545', 'error', true);
bm.remove(PLACEHOLDER_ID);
});
};
})(window.BridgeParts || (window.BridgeParts = {}));