From b039aceb21a7663b04229ad6f3421b49a7d65f67 Mon Sep 17 00:00:00 2001 From: Daniel S Date: Sun, 9 Aug 2026 12:53:25 +0200 Subject: [PATCH] feat: implement Theme Builder concept and Display Conditions resolver system --- src/pages/admin/editor.astro | 104 +++++++++++++++++++++++++++++++++ src/utils/conditionResolver.ts | 67 +++++++++++++++++++++ tests/waasEngine.test.ts | 30 ++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/utils/conditionResolver.ts diff --git a/src/pages/admin/editor.astro b/src/pages/admin/editor.astro index 7c38631..b719dbf 100644 --- a/src/pages/admin/editor.astro +++ b/src/pages/admin/editor.astro @@ -30,6 +30,9 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
+ Classic Admin @@ -383,6 +386,33 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
+ + + + @@ -721,4 +751,78 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u } }); } + + // --- Theme Builder Display Conditions Modal Management --- + let themeConditionsState = [ + { type: 'include', scope: 'entire_site' } + ]; + + const conditionsModal = document.getElementById('conditions-modal'); + const conditionsBtn = document.getElementById('theme-conditions-btn'); + const closeConditionsBtn = document.getElementById('close-conditions-modal'); + const cancelConditionsBtn = document.getElementById('cancel-conditions-btn'); + const addConditionBtn = document.getElementById('add-condition-btn'); + const saveConditionsBtn = document.getElementById('save-conditions-btn'); + + function openConditionsModal() { + renderConditionsList(); + conditionsModal?.classList.remove('hidden'); + } + + function closeConditionsModal() { + conditionsModal?.classList.add('hidden'); + } + + function renderConditionsList() { + const listContainer = document.getElementById('conditions-list'); + if (!listContainer) return; + + listContainer.innerHTML = themeConditionsState.map((cond, idx) => ( + '
' + + '' + + '' + + '' + + '
' + )).join(''); + } + + window.updateCondition = function(idx, field, value) { + if (themeConditionsState[idx]) { + themeConditionsState[idx][field] = value; + } + }; + + window.removeCondition = function(idx) { + themeConditionsState.splice(idx, 1); + renderConditionsList(); + }; + + if (addConditionBtn) { + addConditionBtn.addEventListener('click', () => { + themeConditionsState.push({ type: 'include', scope: 'entire_site' }); + renderConditionsList(); + }); + } + + conditionsBtn?.addEventListener('click', openConditionsModal); + closeConditionsBtn?.addEventListener('click', closeConditionsModal); + cancelConditionsBtn?.addEventListener('click', closeConditionsModal); + + if (saveConditionsBtn) { + saveConditionsBtn.addEventListener('click', () => { + closeConditionsModal(); + if (alertBox) { + alertBox.classList.remove('hidden'); + alertBox.textContent = 'Display Conditions erfolgreich gespeichert!'; + setTimeout(() => alertBox.classList.add('hidden'), 3000); + } + }); + } \ No newline at end of file diff --git a/src/utils/conditionResolver.ts b/src/utils/conditionResolver.ts new file mode 100644 index 0000000..48f94bf --- /dev/null +++ b/src/utils/conditionResolver.ts @@ -0,0 +1,67 @@ +export interface DisplayCondition { + type: 'include' | 'exclude'; + scope: 'entire_site' | 'singular' | 'archive' | 'page_404'; + entity?: 'page' | 'post' | 'category'; + entityId?: string; +} + +export interface TemplateMeta { + id: string; + name: string; + type: 'header' | 'footer' | 'single' | 'archive' | '404'; + status: 'draft' | 'published'; + priority: number; + conditions: DisplayCondition[]; + createdAt?: string; + updatedAt?: string; +} + +/** + * Resolves which template to use based on target type and request context conditions. + * Specific matches (singular/page_404) take precedence over general 'entire_site' matches. + */ +export function resolveTemplate( + templates: TemplateMeta[], + targetType: TemplateMeta['type'], + context: { url: string; routeType: 'singular' | 'archive' | '404' | 'home'; entityId?: string } +): TemplateMeta | null { + const candidates = templates.filter(t => t.type === targetType && t.status === 'published'); + let bestMatch: TemplateMeta | null = null; + let highestScore = -1; + + for (const tpl of candidates) { + let isIncluded = false; + let isExcluded = false; + let score = 0; + + for (const cond of tpl.conditions) { + if (cond.type === 'exclude') { + if (cond.scope === 'entire_site') isExcluded = true; + if (cond.scope === 'singular' && context.routeType === 'singular' && (!cond.entityId || cond.entityId === context.entityId)) { + isExcluded = true; + } + if (cond.scope === 'page_404' && context.routeType === '404') isExcluded = true; + } else if (cond.type === 'include') { + if (cond.scope === 'entire_site') { + isIncluded = true; + score = Math.max(score, tpl.priority || 1); + } else if (cond.scope === 'singular' && context.routeType === 'singular') { + if (!cond.entityId || cond.entityId === context.entityId) { + isIncluded = true; + score = Math.max(score, (tpl.priority || 1) + 10); + } + } else if (cond.scope === 'page_404' && context.routeType === '404') { + isIncluded = true; + score = Math.max(score, (tpl.priority || 1) + 10); + } + } + } + + if (isIncluded && !isExcluded && score > highestScore) { + highestScore = score; + bestMatch = tpl; + } + } + + return bestMatch; +} diff --git a/tests/waasEngine.test.ts b/tests/waasEngine.test.ts index 76f394f..3239cf9 100644 --- a/tests/waasEngine.test.ts +++ b/tests/waasEngine.test.ts @@ -66,4 +66,34 @@ describe('WaaS Engine Core Unit Tests', () => { const desktopConfig = loadWaasConfigs('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); expect(desktopConfig.siteConfig.siteName).toBe('Desktop Site'); }); + + it('6. Should correctly resolve Theme Builder template based on Display Conditions', async () => { + const { resolveTemplate } = await import('../src/utils/conditionResolver'); + const templates = [ + { + id: 'header-global', + name: 'Global Header', + type: 'header' as const, + status: 'published' as const, + priority: 1, + conditions: [{ type: 'include' as const, scope: 'entire_site' as const }] + }, + { + id: 'header-custom-page', + name: 'Custom Page Header', + type: 'header' as const, + status: 'published' as const, + priority: 1, + conditions: [{ type: 'include' as const, scope: 'singular' as const, entityId: 'landing_1' }] + } + ]; + + // Global matching + const matchGlobal = resolveTemplate(templates, 'header', { url: '/about', routeType: 'singular', entityId: 'about_page' }); + expect(matchGlobal?.id).toBe('header-global'); + + // Specific page matching + const matchSpecific = resolveTemplate(templates, 'header', { url: '/landing', routeType: 'singular', entityId: 'landing_1' }); + expect(matchSpecific?.id).toBe('header-custom-page'); + }); }); \ No newline at end of file