diff --git a/config/emailtemplate.conf.php b/config/emailtemplate.conf.php index 93cbaf8..4655572 100644 --- a/config/emailtemplate.conf.php +++ b/config/emailtemplate.conf.php @@ -104,6 +104,15 @@ $tablesDefaults = [ ]; $tables = array_replace($tablesDefaults, $overrides['tables'] ?? []); +$placeholdersDefaults = [ + 'bridge' => [ + 'url' => getenv('EMAILTEMPLATE_PLACEHOLDER_URL') ?: '', + 'token' => getenv('EMAILTEMPLATE_PLACEHOLDER_TOKEN') ?: '', + 'cache_ttl' => (int)(getenv('EMAILTEMPLATE_PLACEHOLDER_CACHE') ?: 300), + ], +]; +$placeholders = array_replace_recursive($placeholdersDefaults, $overrides['placeholders'] ?? []); + $columnsDefaults = [ 'templates' => ['id'=>'id','name'=>'name','desc'=>null,'cat'=>null,'upd'=>'updated_at'], 'sections' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'], @@ -123,4 +132,5 @@ return [ 'multi' => $multi, 'tables' => $tables, 'columns' => $columns, + 'placeholders' => $placeholders, ]; diff --git a/download/emailtemplate_bridge.php b/download/emailtemplate_bridge.php index 0a18b87..97ac755 100644 --- a/download/emailtemplate_bridge.php +++ b/download/emailtemplate_bridge.php @@ -32,6 +32,7 @@ $bridgeConfig = [ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ], ], + 'tables_allow' => [], // optional whitelist: ['customers', 'orders'] ]; $localOverride = __DIR__ . '/emailtemplate.bridge.conf.php'; @@ -115,13 +116,24 @@ try { $dbName = $m[1]; } - $tablesStmt = $pdo->query('SHOW FULL TABLES'); - $tables = []; - while ($row = $tablesStmt->fetch(PDO::FETCH_NUM)) { - $tableName = $row[0]; - if ($tableName === null) { - continue; +$tablesStmt = $pdo->query('SHOW FULL TABLES'); +$tables = []; +$whitelist = []; +if (!empty($bridgeConfig['tables_allow']) && is_array($bridgeConfig['tables_allow'])) { + foreach ($bridgeConfig['tables_allow'] as $tbl) { + if (is_string($tbl) && $tbl !== '') { + $whitelist[strtolower($tbl)] = true; } + } +} +while ($row = $tablesStmt->fetch(PDO::FETCH_NUM)) { + $tableName = $row[0]; + if ($tableName === null) { + continue; + } + if ($whitelist && empty($whitelist[strtolower($tableName)])) { + continue; + } $columnsStmt = $pdo->prepare( 'SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA diff --git a/public/assets/js/bridge/blocks-custom.js b/public/assets/js/bridge/blocks-custom.js index ea39d67..c3a8ba7 100644 --- a/public/assets/js/bridge/blocks-custom.js +++ b/public/assets/js/bridge/blocks-custom.js @@ -44,13 +44,226 @@ const css = o => Object.entries(o).map(([k,v]) => `${k}:${v}`).join(';'); + const PLACEHOLDER_COMPONENT = 'placeholder-block'; + const placeholderSchemaStore = { + promise: null, + tables: [], + }; + + const fetchPlaceholderSchema = () => { + if (placeholderSchemaStore.promise) return placeholderSchemaStore.promise; + const base = B.API_KERNEL_URL || '/api.php'; + const sep = base.includes('?') ? '&' : '?'; + const url = `${base}${sep}action=placeholders.schema`; + placeholderSchemaStore.promise = fetch(url, { credentials: 'include' }) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }) + .then(data => { + placeholderSchemaStore.tables = Array.isArray(data?.tables) ? data.tables : []; + return placeholderSchemaStore.tables; + }) + .catch(err => { + placeholderSchemaStore.tables = []; + log('PLACEHOLDER ERROR', `Schema konnte nicht geladen werden: ${err.message || err}`, 'red', 'error'); + throw err; + }); + return placeholderSchemaStore.promise; + }; + + const getTraitByName = (model, name) => { + if (typeof model.getTrait === 'function') return model.getTrait(name); + const traits = model.get('traits'); + if (!traits) return null; + if (typeof traits.where === 'function') { + const found = traits.where({ name }); + return found && found[0]; + } + if (Array.isArray(traits.models)) { + return traits.models.find(t => t.get && t.get('name') === name) || null; + } + return null; + }; + + const ensurePlaceholderComponent = (editor) => { + const domc = editor.DomComponents; + if (domc.getType(PLACEHOLDER_COMPONENT)) return; + + const defaultType = domc.getType('default'); + const defaultModel = defaultType ? defaultType.model : null; + const defaultView = defaultType ? defaultType.view : null; + + domc.addType(PLACEHOLDER_COMPONENT, { + model: (defaultModel || editor.DomComponents.Component).extend({ + defaults: { + ...(defaultModel && defaultModel.prototype?.defaults ? defaultModel.prototype.defaults : {}), + name: 'Placeholder', + tagName: 'span', + selectable: true, + hoverable: true, + droppable: false, + attributes: { + 'data-placeholder-type': 'custom', + 'data-placeholder-key': 'UEBERSCHRIFT', + }, + traits: [ + { + type: 'select', + name: 'data-placeholder-type', + label: 'Typ', + options: [ + { id: 'custom', label: 'Allgemein' }, + { id: 'database', label: 'Datenbank' }, + ], + changeProp: true, + }, + { + type: 'text', + name: 'data-placeholder-key', + label: 'Bezeichner', + placeholder: 'UEBERSCHRIFT', + changeProp: true, + }, + { + type: 'select', + name: 'data-placeholder-table', + label: 'Tabelle', + options: [], + changeProp: true, + }, + { + type: 'select', + name: 'data-placeholder-column', + label: 'Feld', + options: [], + changeProp: true, + }, + ], + }, + + init() { + this.listenTo(this, 'change:attributes', this.updatePlaceholderState); + this.updatePlaceholderState(); + fetchPlaceholderSchema().then(() => { + this.updateSchemaTraits(); + }).catch(() => { + this.updateSchemaTraits([]); + }); + }, + + updatePlaceholderState() { + this.updateTraitVisibility(); + this.updateSchemaTraits(); + this.updateLabel(); + }, + + updateTraitVisibility() { + const attrs = this.getAttributes(); + const type = attrs['data-placeholder-type'] || 'custom'; + const isDb = type === 'database'; + const tableTrait = getTraitByName(this, 'data-placeholder-table'); + const columnTrait = getTraitByName(this, 'data-placeholder-column'); + const keyTrait = getTraitByName(this, 'data-placeholder-key'); + + if (tableTrait?.view?.el) { + tableTrait.view.el.style.display = isDb ? '' : 'none'; + } + if (columnTrait?.view?.el) { + columnTrait.view.el.style.display = isDb ? '' : 'none'; + } + if (keyTrait?.view?.el) { + keyTrait.view.el.style.display = isDb ? 'none' : ''; + } + }, + + updateSchemaTraits(tablesOverride) { + const tables = Array.isArray(tablesOverride) ? tablesOverride : placeholderSchemaStore.tables; + const tableTrait = getTraitByName(this, 'data-placeholder-table'); + const columnTrait = getTraitByName(this, 'data-placeholder-column'); + + if (tableTrait) { + const opts = tables.map(tbl => ({ id: tbl.name, label: tbl.name })); + tableTrait.set('options', opts); + if (tableTrait.view && tableTrait.view.render) tableTrait.view.render(); + } + + if (columnTrait) { + const attrs = this.getAttributes(); + const tableName = (attrs['data-placeholder-table'] || '').toLowerCase(); + const table = tables.find(tbl => tbl.name.toLowerCase() === tableName); + const colOpts = table + ? table.columns.map(col => ({ + id: col.name, + label: `${col.name} (${col.type})`, + })) + : []; + columnTrait.set('options', colOpts); + if (columnTrait.view && columnTrait.view.render) columnTrait.view.render(); + } + }, + + updateLabel() { + const attrs = this.getAttributes(); + const type = attrs['data-placeholder-type'] || 'custom'; + let label; + if (type === 'database') { + const table = attrs['data-placeholder-table'] || 'TABELLE'; + const column = attrs['data-placeholder-column'] || 'FELD'; + label = `${table}.${column}`.toUpperCase(); + } else { + label = (attrs['data-placeholder-key'] || 'PLATZHALTER').toUpperCase(); + } + const text = `{{${label}}}`; + const comps = this.components(); + if (comps.length === 1 && comps.at(0).is('textnode')) { + comps.at(0).set('content', text); + } else { + comps.reset([{ type: 'textnode', content: text }]); + } + }, + }, { + isComponent(el) { + if (el && el.hasAttribute && el.hasAttribute('data-placeholder-type')) { + return { type: PLACEHOLDER_COMPONENT }; + } + return false; + }, + }), + view: (defaultView || editor.DomComponents.View).extend({ + render() { + defaultView && defaultView.prototype.render + ? defaultView.prototype.render.apply(this, arguments) + : editor.DomComponents.View.prototype.render.apply(this, arguments); + this.el.classList.add('placeholder-block'); + this.el.style.display = 'inline-block'; + this.el.style.padding = '2px 8px'; + this.el.style.border = '1px dashed #94a3b8'; + this.el.style.borderRadius = '6px'; + this.el.style.background = '#f1f5f9'; + this.el.style.fontFamily = 'monospace'; + this.el.style.fontSize = '12px'; + return this; + }, + }), + }); + }; + function register(editor) { log('EXECUTION', `Starte Block-Registrierung für ${TARGET_CAT_ID}.`, '#DAA520'); const bm = editor.BlockManager; + ensurePlaceholderComponent(editor); // --- Custom-Blöcke DEFINIEREN --- + // PLACEHOLDER + addOnce(bm, 'cust-placeholder', { + id: 'cust-placeholder', + label: '🔖 Placeholder', + content: `{{UEBERSCHRIFT}}` + }); + // TEXT addOnce(bm, 'cust-text', { id:'cust-text', label:'📝 Text', content:`