VPR-59 [2/6] CMS migration: content blocks + left navigation#246
VPR-59 [2/6] CMS migration: content blocks + left navigation#246rlorenzo wants to merge 2 commits into
Conversation
- Extend /api/CMS/content: status/system/section/search filters, get-by-id, version history list and retrieval, restore, admin-gated permanent delete, content-only PATCH, file attach/detach, and ModifiedOn-based conflict detection (409 on stale saves) - Fix history semantics to match legacy: store the previous version with its original author/date (was writing the new content, and create wrote a history row with block id 0) - List endpoint projects without the Content column to avoid loading every block body; attached file GUIDs are validated on save - UI: content block list page and editor (QEditor, settings sidebar, permissions, file attachment via search, version history with restore-by-save, conflict dialog)
- New /api/cms/left-navs (ManageNavigation): menu CRUD with cascade delete, and a batch item save that adds/updates/deletes/reorders in one call, replacing the legacy DataTables editor flow - Fix nav display filter: items with no permissions are now visible to any logged-in user (legacy behavior); previously they were hidden from rendered menus on Layout/Home/Students pages - Validate left nav item URLs with SafeUrlAttribute - UI: menu list page and item editor with drag/arrow reordering and per-item permission selectors; shared EditButton/ModifiedStamp components extracted from list pages
There was a problem hiding this comment.
Pull request overview
This PR (VPR-59 2/6) continues the CMS migration by adding new backend services/controllers and Vue admin pages for content blocks (with history + concurrency protection + attached files) and left navigation menus (menu CRUD + batch item save/reorder + per-item permissions), plus a small legacy-parity tweak in left-nav rendering.
Changes:
- Added
CmsContentBlockService+ rebuiltCMSContentControllerendpoints for content-block CRUD, history retrieval, soft/restore/permanent delete, and concurrency conflicts. - Added
CmsLeftNavService+CMSLeftNavControllerfor left-nav menu CRUD and legacy-style batch item save (add/update/delete/reorder). - Added CMS Vue routes/pages and shared UI components to manage content blocks and left-nav menus.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| web/Areas/CMS/Validation/SafeUrlAttribute.cs | Adds AllowRelative support and strengthens URL validation. |
| web/Areas/CMS/Services/CmsLeftNavService.cs | Implements left-nav menu CRUD, cascade delete, and batch item save logic. |
| web/Areas/CMS/Services/CmsContentBlockService.cs | Implements content block CRUD, history semantics, file/permission deltas, and concurrency detection. |
| web/Areas/CMS/Models/DTOs/LeftNavDtos.cs | Adds DTOs for left-nav menus/items and edit payloads with URL validation. |
| web/Areas/CMS/Models/DTOs/ContentBlockDto.cs | Adds DTOs for content blocks, attached files, and history payloads. |
| web/Areas/CMS/Models/CmsContentBlockMapper.cs | Mapperly-based mapping from ContentBlock to DTO including permissions/files. |
| web/Areas/CMS/Models/CMSBlockAddEdit.cs | Extends content block request model with file GUIDs and LastModifiedOn. |
| web/Areas/CMS/Data/LeftNavMenu.cs | Adjusts left-nav visibility rules to match legacy behavior for permission-less items. |
| web/Areas/CMS/Controllers/CMSLeftNavController.cs | Adds API controller for left-nav menu/item management. |
| web/Areas/CMS/Controllers/CMSContentController.cs | Refactors content endpoints to use the new service and adds history/patch/delete semantics. |
| VueApp/src/CMS/types/index.ts | Adds TS types for CMS content blocks/history and left-nav menus/items. |
| VueApp/src/CMS/router/routes.ts | Adds CMS routes for content blocks and left-nav editors. |
| VueApp/src/CMS/pages/LeftNavMenus.vue | Adds list UI for left-nav menus with filtering and delete. |
| VueApp/src/CMS/pages/LeftNavEdit.vue | Adds create/edit UI for left-nav menus with drag/drop reorder and batch save. |
| VueApp/src/CMS/pages/Files.vue | Refactors to shared components for permissions, modified stamp, edit, delete/restore. |
| VueApp/src/CMS/pages/ContentBlocks.vue | Adds list UI for content blocks with filtering and delete/restore actions. |
| VueApp/src/CMS/pages/ContentBlockEdit.vue | Adds editor UI for content blocks including history dropdown and file attachments. |
| VueApp/src/CMS/pages/CmsHome.vue | Adds CMS home cards for Content Blocks and Left Navigation. |
| VueApp/src/CMS/components/PermissionChips.vue | Shared “permissions/people count” chip rendering for tables. |
| VueApp/src/CMS/components/ModifiedStamp.vue | Shared modified date/by rendering for table cells. |
| VueApp/src/CMS/components/EditButton.vue | Shared small edit button component (route link or click handler). |
| VueApp/src/CMS/components/DeleteRestoreButtons.vue | Shared delete/restore toggle button component. |
| test/CMS/SafeUrlAttributeTests.cs | Unit tests for SafeUrlAttribute in absolute-only and relative-allowed modes. |
| test/CMS/CmsLeftNavServiceTests.cs | Unit tests for left-nav service CRUD and batch item save semantics. |
| test/CMS/CmsContentBlockServiceTests.cs | Unit tests for content block service filtering, history semantics, deltas, concurrency, and deletes. |
| if (!url.Split('/', '?', '#')[0].Contains(':')) | ||
| { | ||
| return new ValidationResult("URL protocol must be http, https, mailto, or tel."); | ||
| return AllowRelative | ||
| ? ValidationResult.Success | ||
| : new ValidationResult("URL must be a full address starting with http, https, mailto, or tel."); |
There was a problem hiding this comment.
Already fixed on the branch (on the development branch): AllowRelative mode rejects scheme-relative '//host' URLs and their backslash variants. In the restack this lands in #252 with tests.
| { | ||
| return; | ||
| } | ||
| int existing = await _context.Files.CountAsync(f => fileGuids.Contains(f.FileGuid), ct); |
There was a problem hiding this comment.
Already fixed on the branch: the attached-file GUID existence check uses EF.Parameter(list).Contains for OPENJSON translation. In #252.
| //by default, filter items based on user permissions; items with no | ||
| //permissions are visible to any logged-in user (legacy CMS behavior) | ||
| List<NavMenuItem> items = m.LeftNavItems | ||
| .Where(item => !filterItemsByPermissions | ||
| || item.LeftNavItemToPermissions.Count == 0 | ||
| || item.LeftNavItemToPermissions.Any(p => UserHelper.HasPermission(_rapsContext, currentUser, p.Permission))) | ||
| .Select(item => new NavMenuItem(item)) |
There was a problem hiding this comment.
Fixed in the restack: permission-less items now require a signed-in user (currentUser != null); anonymous requests see none, signed-in users keep the legacy-parity behavior. In #252.
| else | ||
| { | ||
| var newItem = new LeftNavItem | ||
| { | ||
| LeftNavMenuId = leftNavMenuId, | ||
| MenuItemText = requested.MenuItemText, | ||
| IsHeader = requested.IsHeader, | ||
| Url = requested.IsHeader ? null : requested.Url, | ||
| DisplayOrder = order | ||
| }; | ||
| foreach (var permission in CleanList(requested.Permissions)) | ||
| { | ||
| newItem.LeftNavItemToPermissions.Add(new LeftNavItemToPermission { Permission = permission }); | ||
| } | ||
| menu.LeftNavItems.Add(newItem); | ||
| } |
There was a problem hiding this comment.
Already fixed on the branch (on the development branch): a positive id not belonging to the menu is rejected ('One or more items no longer exist in this menu. Reload and try again.') instead of being resurrected as a new item. In #252.
| var updated = await _leftNavService.SaveItemsAsync(leftNavMenuId, items, ct); | ||
| if (updated == null) | ||
| { | ||
| return NotFound(); | ||
| } | ||
| return updated; | ||
| } |
There was a problem hiding this comment.
Already handled: the controller maps ArgumentException to 400 and the unknown-id InvalidOperationException to 409. In #252.
| <q-banner | ||
| v-if="block.deletedOn" | ||
| class="bg-orange-2 q-mb-md" | ||
| dense | ||
| rounded |
There was a problem hiding this comment.
Already fixed on the branch: the editor's deleted-state banner uses the shared StatusBanner component. In #253.
| [Theory] | ||
| [InlineData("javascript:alert(document.cookie)")] | ||
| [InlineData("JAVASCRIPT:alert(1)")] | ||
| [InlineData("data:text/html,<script>alert(1)</script>")] | ||
| [InlineData("vbscript:msgbox(1)")] | ||
| [InlineData("java\tscript:alert(1)")] | ||
| [InlineData("java\nscript:alert(1)")] |
There was a problem hiding this comment.
Already added on the branch: SafeUrlAttributeTests covers '//evil.example' (and backslash variants) in AllowRelative mode. In #252.
|
Superseded by the restacked 3-PR stack: #251 (files/photos/import/rate-limit backend) -> #252 (content blocks/left nav/link collections backend) -> #253 (management SPA). These historical cut-point slices predated the branch's later OS-independent path handling and cleanup commits, so they failed CI on Linux runners; the new slices are built from the CI-verified branch tip, carry every review fix (see thread replies), and each passes the full gate set. All feedback on this PR has been answered inline. |
Part 2 of 6 (stacks on #245 — the diff below shows only this slice).
Scope
CmsContentBlockService+ rebuiltCMSContentController(filters, history list/restore, section paths, permanent delete admin-gated, content PATCH, attached-file GUID deltas, ModifiedOn 409 concurrency). Fixes two legacy-divergence bugs in history writing: create wrote a history row with id 0, and update stored the NEW content instead of the previous version.ContentBlocks.vuelist +ContentBlockEdit.vue(QEditor, version dropdown, attach files via search), sharedPermissionChips/DeleteRestoreButtons.CmsLeftNavService+CMSLeftNavController(menu CRUD, batch item save with ordering, per-item permissions, cascade delete) +LeftNavMenus.vue/LeftNavEdit.vue(drag + arrow reorder, batch save/revert).Data/LeftNavMenu: permission-less items are visible to all logged-in users, matching legacy (affects Layout/Home/Students nav rendering).