diff --git a/TEST_PLAN.md b/TEST_PLAN.md new file mode 100644 index 0000000..cef8896 --- /dev/null +++ b/TEST_PLAN.md @@ -0,0 +1,17 @@ +# Test Plan + +1. **Build and restore dependencies** + - Run `./setup.sh` to restore NuGet and client libraries. + - Ensure the command completes without errors. + +2. **Run unit tests** + - Execute `dotnet test website/MyWebApp.sln`. + - All tests should pass. + +3. **Manual UI smoke test** + - Start the application with `dotnet run --project website/MyWebApp`. + - Verify admin pages load correctly: + - Create and edit pages and sections using the unified section editor. + - Insert blocks via the updated API endpoints. + - Confirm media uploads still work and page layouts render as before. + diff --git a/extension/html/drive-auth.html b/extension/html/drive-auth.html index 77ee7c5..8bf3aa7 100644 --- a/extension/html/drive-auth.html +++ b/extension/html/drive-auth.html @@ -1,8 +1,8 @@ - +
-hi
", Type = PageSectionType.Html }); ++ context.PageSections.Add(new PageSection { PageId = page.Id, Zone = "header", Html = "hi
", Type = PageSectionType.Html }); + + context.SaveChanges(); + } +@@ -30,7 +30,7 @@ public class PageSectionTests + using (var context = new ApplicationDbContext(options)) + { + var section = context.PageSections.Include(s => s.Page) +- .Single(s => s.Area == "header" && s.Page!.Slug == "test"); ++ .Single(s => s.Zone == "header" && s.Page!.Slug == "test"); + Assert.Equal("hi
", section.Html); + Assert.Equal("test", section.Page!.Slug); + } +diff --git a/website/MyWebApp.Tests/SanitizationTests.cs b/website/MyWebApp.Tests/SanitizationTests.cs +index 44e1adc..3246fa6 100644 +--- a/website/MyWebApp.Tests/SanitizationTests.cs ++++ b/website/MyWebApp.Tests/SanitizationTests.cs +@@ -40,7 +40,7 @@ public class SanitizationTests + Layout = "single-column", + Sections = new Listb
" } ++ new PageSection { Zone = "main", Html = "b
" } + } + }; + var result = await controller.Create(model); +@@ -55,7 +55,7 @@ public class SanitizationTests + var (ctx, layout, sanitizer) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, sanitizer); + +- var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "test", Html = "a
" } } ++ Sections = new Lista
" } } + }; + await controller.Create(createModel); + var page = ctx.Pages.Single(p => p.Slug == "edit"); +@@ -83,7 +83,7 @@ public class SanitizationTests + Slug = page.Slug, + Title = page.Title, + Layout = page.Layout, +- Sections = new Listb
" } } ++ Sections = new Listb
" } } + }; + var result = await controller.Edit(model); + var section = ctx.PageSections.Single(s => s.PageId == page.Id); +@@ -96,10 +96,10 @@ public class SanitizationTests + { + var (ctx, layout, sanitizer) = CreateServices(); + var controller = new AdminPageSectionController(ctx, layout, sanitizer); +- var model = new PageSection { PageId = ctx.Pages.First().Id, Area = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; ++ var model = new PageSection { PageId = ctx.Pages.First().Id, Zone = "md", Html = "# Hello\n", Type = PageSectionType.Markdown }; + var result = await controller.Create(model, null); + Assert.IsType| Page | Area | Type | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Page | Zone | Type | ||||||||||||
| @s.Page?.Slug | +-@s.Area | ++@s.Zone | + +@s.Type | + +diff --git a/website/MyWebApp/wwwroot/css/admin.css b/website/MyWebApp/wwwroot/css/admin.css +index ac0c040..e891877 100644 +--- a/website/MyWebApp/wwwroot/css/admin.css ++++ b/website/MyWebApp/wwwroot/css/admin.css +@@ -1278,15 +1278,15 @@ form.mb-3 { + background: #0ea5e9; + color: #fff; + } +-.area-group { ++.zone-group { + border: 1px solid #e2e8f0; + padding: 0.5rem; + margin-bottom: 1rem; + } +-.area-group h3 { ++.zone-group h3 { + margin: 0 0 0.5rem 0; + text-transform: capitalize; + } +-.area-sections { ++.zone-sections { + min-height: 10px; + } +diff --git a/website/MyWebApp/wwwroot/js/page-editor.js b/website/MyWebApp/wwwroot/js/page-editor.js +index 9dec3f2..533e07c 100644 +--- a/website/MyWebApp/wwwroot/js/page-editor.js ++++ b/website/MyWebApp/wwwroot/js/page-editor.js +@@ -10,32 +10,32 @@ window.addEventListener('load', () => { + + function buildGroups() { + container.innerHTML = ''; +- (layoutZones[currentLayout] || []).forEach(a => { ++ (layoutZones[currentLayout] || []).forEach(z => { + const group = document.createElement('div'); +- group.className = 'area-group'; +- group.dataset.area = a; ++ group.className = 'zone-group'; ++ group.dataset.zone = z; + const h = document.createElement('h3'); +- h.textContent = a; ++ h.textContent = z; + const div = document.createElement('div'); +- div.className = 'area-sections'; ++ div.className = 'zone-sections'; + group.appendChild(h); + group.appendChild(div); + container.appendChild(group); + }); + } + +- function populateAreas(select) { ++ function populateZones(select) { + if (!select) return; + const current = select.dataset.selected || select.value; +- select.innerHTML = (layoutZones[currentLayout] || []).map(a => ``).join(''); ++ select.innerHTML = (layoutZones[currentLayout] || []).map(z => ``).join(''); + if (current) select.value = current; + select.dataset.selected = ''; + } + + function placeSection(section) { +- const select = section.querySelector('.area-select'); +- const area = select ? select.value : 'main'; +- const group = container.querySelector(`.area-group[data-area='${area}'] .area-sections`); ++ const select = section.querySelector('.zone-select'); ++ const zone = select ? select.value : 'main'; ++ const group = container.querySelector(`.zone-group[data-zone='${zone}'] .zone-sections`); + if (group) group.appendChild(section); + } + +@@ -43,11 +43,11 @@ window.addEventListener('load', () => { + const preview = document.getElementById('layout-preview'); + if (!preview) return; + preview.innerHTML = ''; +- (layoutZones[currentLayout] || []).forEach(a => { ++ (layoutZones[currentLayout] || []).forEach(z => { + const div = document.createElement('div'); + div.className = 'preview-zone'; +- div.dataset.area = a; +- div.textContent = a; ++ div.dataset.zone = z; ++ div.textContent = z; + preview.appendChild(div); + }); + } +@@ -56,9 +56,9 @@ window.addEventListener('load', () => { + const zone = e.target.closest('.preview-zone'); + if (!zone) return; + if (activeIndex !== null) { +- const select = document.querySelector(`.area-select[data-index='${activeIndex}']`); ++ const select = document.querySelector(`.zone-select[data-index='${activeIndex}']`); + if (select) { +- select.value = zone.dataset.area; ++ select.value = zone.dataset.zone; + placeSection(select.closest('.section-editor')); + updateIndexes(); + } +@@ -91,7 +91,7 @@ window.addEventListener('load', () => { + buildGroups(); + existing.forEach(el => { + const idx = el.dataset.index; +- populateAreas(el.querySelector('.area-select')); ++ populateZones(el.querySelector('.zone-select')); + placeSection(el); + initSectionEditor(idx); + }); +@@ -105,7 +105,7 @@ window.addEventListener('load', () => { + currentLayout = layoutSelect.value; + buildGroups(); + document.querySelectorAll('.section-editor').forEach(sec => { +- populateAreas(sec.querySelector('.area-select')); ++ populateZones(sec.querySelector('.zone-select')); + placeSection(sec); + }); + updateIndexes(); +@@ -123,7 +123,7 @@ window.addEventListener('load', () => { + }); + + container.addEventListener('change', e => { +- if (e.target.classList.contains('area-select')) { ++ if (e.target.classList.contains('zone-select')) { + const section = e.target.closest('.section-editor'); + placeSection(section); + updateIndexes(); +@@ -137,7 +137,7 @@ window.addEventListener('load', () => { + temp.innerHTML = html; + const section = temp.firstElementChild; + section.dataset.index = index; +- populateAreas(section.querySelector('.area-select')); ++ populateZones(section.querySelector('.zone-select')); + placeSection(section); + initSectionEditor(index); + updateIndexes(); +@@ -163,7 +163,7 @@ window.addEventListener('load', () => { + dest.value = src.value; + } + }); +- populateAreas(clone.querySelector('.area-select')); ++ populateZones(clone.querySelector('.zone-select')); + placeSection(clone); + initSectionEditor(index); + if (editors[original.dataset.index]) { +diff --git a/website/MyWebApp/wwwroot/js/page-section-area.js b/website/MyWebApp/wwwroot/js/page-section-area.js +deleted file mode 100644 +index c105b77..0000000 +--- a/website/MyWebApp/wwwroot/js/page-section-area.js ++++ /dev/null +@@ -1,19 +0,0 @@ +-window.addEventListener('load', () => { +- const pageSelect = document.querySelector('select[name="PageId"]'); +- const areaSelect = document.getElementById('area-select'); +- if (!pageSelect || !areaSelect) return; +- +- function loadAreas() { +- const id = pageSelect.value; +- if (!id) { areaSelect.innerHTML = ''; return; } +- fetch(`/AdminPageSection/GetAreasForPage/${id}`) +- .then(r => r.json()) +- .then(list => { +- areaSelect.innerHTML = list.map(a => ``).join(''); +- if (areaSelect.dataset.selected) +- areaSelect.value = areaSelect.dataset.selected; +- }); +- } +- loadAreas(); +- pageSelect.addEventListener('change', loadAreas); +-}); +diff --git a/website/MyWebApp/wwwroot/js/page-section-zone.js b/website/MyWebApp/wwwroot/js/page-section-zone.js +new file mode 100644 +index 0000000..9b2d0d3 +--- /dev/null ++++ b/website/MyWebApp/wwwroot/js/page-section-zone.js +@@ -0,0 +1,19 @@ ++window.addEventListener('load', () => { ++ const pageSelect = document.querySelector('select[name="PageId"]'); ++ const zoneSelect = document.getElementById('zone-select'); ++ if (!pageSelect || !zoneSelect) return; ++ ++ function loadZones() { ++ const id = pageSelect.value; ++ if (!id) { zoneSelect.innerHTML = ''; return; } ++ fetch(`/AdminPageSection/GetZonesForPage/${id}`) ++ .then(r => r.json()) ++ .then(list => { ++ zoneSelect.innerHTML = list.map(a => ``).join(''); ++ if (zoneSelect.dataset.selected) ++ zoneSelect.value = zoneSelect.dataset.selected; ++ }); ++ } ++ loadZones(); ++ pageSelect.addEventListener('change', loadZones); ++}); diff --git a/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs new file mode 100644 index 0000000..d4632be --- /dev/null +++ b/website/MyWebApp.Tests/AdminBlockTemplateControllerTests.cs @@ -0,0 +1,68 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using MyWebApp.Controllers; +using MyWebApp.Data; +using MyWebApp.Models; +using MyWebApp.Services; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +public class AdminBlockTemplateControllerTests +{ + private static (AdminBlockTemplateController controller, ApplicationDbContext ctx, SqliteConnection conn) Create() + { + var conn = new SqliteConnection("DataSource=:memory:"); + conn.Open(); + var options = new DbContextOptionsBuilder|||||||||||
| Name | |||
|---|---|---|---|
| Name | @t.Name | Edit | Delete | -Add to Page | } diff --git a/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml new file mode 100644 index 0000000..e2a40f5 --- /dev/null +++ b/website/MyWebApp/Views/AdminBlockTemplate/_PageAssignment.cshtml @@ -0,0 +1,51 @@ +@using MyWebApp.Models +@{ + var selectedPages = ViewBag.SelectedPageIds as List