Skip to content

VPR-59 [2/6] CMS migration: content blocks + left navigation#246

Closed
rlorenzo wants to merge 2 commits into
VPR-59-cms-1-filesfrom
VPR-59-cms-2-content-nav
Closed

VPR-59 [2/6] CMS migration: content blocks + left navigation#246
rlorenzo wants to merge 2 commits into
VPR-59-cms-1-filesfrom
VPR-59-cms-2-content-nav

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Part 2 of 6 (stacks on #245 — the diff below shows only this slice).

Scope

  • Content blocks: CmsContentBlockService + rebuilt CMSContentController (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.
  • UI: ContentBlocks.vue list + ContentBlockEdit.vue (QEditor, version dropdown, attach files via search), shared PermissionChips/DeleteRestoreButtons.
  • Left nav: CmsLeftNavService + CMSLeftNavController (menu CRUD, batch item save with ordering, per-item permissions, cascade delete) + LeftNavMenus.vue/LeftNavEdit.vue (drag + arrow reorder, batch save/revert).
  • Display-parity fix in Data/LeftNavMenu: permission-less items are visible to all logged-in users, matching legacy (affects Layout/Home/Students nav rendering).

rlorenzo added 2 commits July 2, 2026 14:38
- 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + rebuilt CMSContentController endpoints for content-block CRUD, history retrieval, soft/restore/permanent delete, and concurrency conflicts.
  • Added CmsLeftNavService + CMSLeftNavController for 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.

Comment on lines +34 to +38
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.");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed on the branch: the attached-file GUID existence check uses EF.Parameter(list).Contains for OPENJSON translation. In #252.

Comment on lines +53 to 59
//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))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +157 to +172
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);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +76 to +82
var updated = await _leftNavService.SaveItemsAsync(leftNavMenuId, items, ct);
if (updated == null)
{
return NotFound();
}
return updated;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled: the controller maps ArgumentException to 400 and the unknown-id InvalidOperationException to 409. In #252.

Comment on lines +19 to +23
<q-banner
v-if="block.deletedOn"
class="bg-orange-2 q-mb-md"
dense
rounded

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed on the branch: the editor's deleted-state banner uses the shared StatusBanner component. In #253.

Comment on lines +30 to +36
[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)")]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already added on the branch: SafeUrlAttributeTests covers '//evil.example' (and backslash variants) in AllowRelative mode. In #252.

@rlorenzo

rlorenzo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

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.

@rlorenzo rlorenzo closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants