VPR-59 [4/6] CMS migration: hub redesign, a11y, form-UX standardization#248
VPR-59 [4/6] CMS migration: hub redesign, a11y, form-UX standardization#248rlorenzo wants to merge 7 commits into
Conversation
- Left nav now mirrors legacy CMS (Home, content blocks, files, left-nav sections, permission-gated); nav, hub cards, and page titles share "Manage X" wording, and /CMS/ canonicalizes to /CMS/Home so the Home link highlights - Hub: permission-filtered 2x2 tool cards plus a recent-activity rail (recently edited blocks/files deep-link to their editors via existing list APIs); zero-permission visitors get an info banner instead of a blank page - Import and Bulk Encrypt moved off the hub into a File Tools menu on Manage Files; the Upload File nav link opens the upload dialog via ?upload=1 - Shell a11y (all areas): visible focus ring on flat card actions, left nav items announce as links, drawer landmark labeled, new "Skip to section menu" link, and in-page anchor navigation no longer steals focus or scroll position - Shared router: scroll position survives query-only navigation, hash links scroll to their target; createSpaRouter.ts renamed to create-spa-router.ts (kebab convention) with new unit tests
- Workspace anchors now use the brand link color and dense table action buttons keep a 24px minimum hit area (base.css, applies app-wide) - Icon-only status and result indicators get screen-reader text; links that open new windows announce it - Loading buttons use the design-system spinner+label slot; deleted banner on content block edit uses StatusBanner; sidebar/card headings are real h2s - Create buttons match nav wording (Add Content Block, Add Left-Nav Menu, Add Collection); removed the duplicate Audit Log button on Manage Files - Hand-rolled "file(s)" plurals replaced with inflect(); inline styles extracted to classes
- File URLs now point at the VIPER2 download handler (/CMS/Files, permission-checked, serves public files anonymously) instead of the ColdFusion /cms/files path; host keeps non-default ports and the URL respects PathBase on TEST/PROD - Manage Files: Old URL column collapses to a compact link (full URL in tooltip) that resolves through the download handler; filters sync to the query string for shareable deep links; new "Public only" toggle (isPublic was already supported by the API) - Folder filter lists disk folders unioned with folders that exist only on file records (e.g. VMDEVSignatures in dev), with an explicit "All" default; upload destinations keep the disk-only allow-list since the first path segment is a security check - Hub cards render as equal-height tiles via a CSS grid with 1fr rows
Matches the legacy UI and exercises the real resolution chain old links take (VIPER 1 404 handler -> oldURL lookup) instead of calling the CMS handler lookup directly.
- Replace the MakeUnique flag with a check-name endpoint plus explicit FileName/Overwrite request fields, so an upload detects a name conflict up front and prompts to rename or overwrite - Add an import dry-run preview that reports the resulting names and any auto-renames without moving files, sharing path validation with the real import so it can't promise what import would reject - Reconcile the on-disk file back to its pre-save state when a metadata save fails across create, update, import, and bulk-encrypt - Require a replacement upload to keep the original file's extension so a stored .pdf can't end up holding .png bytes - Add per-folder file counts for the filter dropdowns, and extract shared BreadcrumbHeading and StatusIcon components
- Surface validation and save errors in an inline banner beside the submit button on the content block, left-nav, file, and link forms, replacing transient toasts that scrolled out of view. - Guard unsaved edits on those forms: route-leave prompts on the pages and confirm-on-close on the dialogs, via useUnsavedChanges. - Make Add Left-Nav Menu a settings-only modal opened from the list (and the nav via ?add=1) that routes to the page for item editing; a shared LeftNavMenuSettingsFields backs both the modal and page. - Auto-enable Public access when a content block's System is set to "Public", with an inline notice, since AllowPublicAccess is the only field that actually gates unauthenticated visibility. - Add a "Public only" filter to the content block list, backed by a new isPublic parameter on the list endpoint and service. - Replace "Back to ..." buttons with breadcrumb headings and align the page titles and entry labels to "Add" (including "Add File"). - Default the System filter to an explicit "All" on the menu and block lists.
There was a problem hiding this comment.
Pull request overview
Implements the next slice of the CMS migration by redesigning the CMS hub and left-nav experience, standardizing CMS form UX and accessibility behaviors across pages, and extending the CMS Files API/UI with preview/import and name-conflict workflows.
Changes:
- Backend: expanded CMS Files APIs (folder counts, filter folders, name checks, import preview) plus safer disk/DB reconciliation for uploads/crypto transitions.
- Frontend: CMS hub redesign with recent-activity rail, shareable filter URLs, standardized headings/banners/spinners, and improved a11y (skip links, focus management, SR-only labels).
- Shared SPA plumbing: router scroll + focus behavior improvements and new unit tests.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| web/Areas/CMS/Services/CmsNavMenu.cs | Rebuilds CMS nav structure with new “Home” and grouped sections based on permissions. |
| web/Areas/CMS/Services/CmsFileStorageService.cs | Adds filter-folder unioning and managed-path/name helper APIs. |
| web/Areas/CMS/Services/CmsFileService.cs | Adds folder counts + name checks; refines upload/overwrite behavior and crypto rollback safety. |
| web/Areas/CMS/Services/CmsFileImportService.cs | Adds import preview flow and hardens import cleanup/logging paths. |
| web/Areas/CMS/Services/CmsContentBlockService.cs | Adds isPublic filtering support to content-block listing. |
| web/Areas/CMS/Models/DTOs/CmsFileRequests.cs | Updates file-create request to support name override + overwrite semantics. |
| web/Areas/CMS/Models/DTOs/CmsFileImportDtos.cs | Adds DTOs for import preview results. |
| web/Areas/CMS/Models/DTOs/CmsFileDto.cs | Adds DTOs for folder counts and pre-upload name checks. |
| web/Areas/CMS/Data/CMS.cs | Updates legacy-friendly URL builders to point at VIPER2 handler and respect PathBase. |
| web/Areas/CMS/Controllers/CMSFilesController.cs | Adds endpoints for folder counts, name checks, and import preview; extends folders API. |
| web/Areas/CMS/Controllers/CMSContentController.cs | Plumbs isPublic filter into content-block list API. |
| web/Areas/CMS/Constants/CmsFileNaming.cs | Centralizes “friendly name” construction rules shared across services. |
| VueApp/src/styles/base.css | Adds focus-ring fixes and heading/breadcrumb/link styling plus hit-area minimums. |
| VueApp/src/Students/router/index.ts | Updates shared router factory import path. |
| VueApp/src/Effort/router/index.ts | Updates shared router factory import path. |
| VueApp/src/CTS/router/index.ts | Updates shared router factory import path. |
| VueApp/src/Computing/router/index.ts | Updates shared router factory import path. |
| VueApp/src/ClinicalScheduler/router/index.ts | Updates shared router factory import path. |
| VueApp/src/CAHFS/router/index.ts | Updates shared router factory import path. |
| VueApp/src/CMS/router/index.ts | Canonicalizes CMS auth/root routing for nav highlighting behavior. |
| VueApp/src/CMS/router/routes.ts | Refines CMS route permissions and adds per-route guard for block editing rules. |
| VueApp/src/shared/createSpaRouter.ts | Removes old shared router module (renamed). |
| VueApp/src/shared/create-spa-router.ts | Replaces shared router module with improved scroll/hash behavior. |
| VueApp/src/shared/tests/create-spa-router.test.ts | Adds tests for router scroll behavior + focus management. |
| VueApp/src/composables/use-route-focus.ts | Prevents focus stealing on in-page hash navigation (skip links). |
| VueApp/src/layouts/ViperLayout.vue | Adds “Skip to section menu” link (responsive). |
| VueApp/src/layouts/LeftNav.vue | Improves left-nav accessibility semantics and landmark labeling. |
| VueApp/src/Effort/components/EffortLeftNav.vue | Adds tabindex target for skip-link focus behavior. |
| VueApp/src/components/BreadcrumbHeading.vue | Introduces shared breadcrumb-style page heading component. |
| VueApp/src/CMS/pages/CmsHome.vue | Hub redesign with section cards + optional recent-activity rail. |
| VueApp/src/CMS/pages/Files.vue | Adds file tools menu, shareable filter URLs, old-URL linking, folder counts, and a11y status icons. |
| VueApp/src/CMS/pages/FileAuditLog.vue | Standardizes headings, filter URL sync, and adds action badges + improved date formatting. |
| VueApp/src/CMS/pages/ImportFiles.vue | Adds preview/confirm import step and standardizes UX + accessibility labels. |
| VueApp/src/CMS/pages/BulkEncrypt.vue | Standardizes UX, adds folder counts, and improves confirmation pluralization and a11y. |
| VueApp/src/CMS/pages/ContentBlocks.vue | Adds “Public only” filtering and status icon a11y improvements. |
| VueApp/src/CMS/pages/ContentBlockEdit.vue | Standardizes UX/a11y, adds unsaved-changes guard, and improves public-system behavior messaging. |
| VueApp/src/CMS/pages/LeftNavMenus.vue | Standardizes UX and adds “add menu” dialog flow + query-triggered open. |
| VueApp/src/CMS/pages/LeftNavEdit.vue | Standardizes UX, adds unsaved-changes guard, and improves inline error surfacing. |
| VueApp/src/CMS/pages/ManageLinkCollections.vue | Standardizes dialog UX, adds unsaved-changes behavior and inline error banners. |
| VueApp/src/CMS/components/StatusIcon.vue | Adds reusable tooltip + SR-only status icon component. |
| VueApp/src/CMS/components/RecentActivity.vue | Adds recent activity list for CMS hub. |
| VueApp/src/CMS/components/LeftNavMenuSettingsFields.vue | Shares left-nav menu settings form fields between dialog and edit page. |
| VueApp/src/CMS/components/LeftNavMenuDialog.vue | Adds modal create flow for left-nav menus with validation/unsaved guard. |
| VueApp/src/CMS/components/FileFormDialog.vue | Replaces “make unique” checkbox with rename/overwrite conflict workflow and improved a11y/UX. |
| test/CMS/CmsFileServiceTests.cs | Adds coverage for overwrite/name-check/folder-count/rollback behaviors. |
| test/CMS/CmsFileImportServiceTests.cs | Adds coverage for import preview and non-DB save failure rollback. |
| test/CMS/CmsContentBlockServiceTests.cs | Updates tests for new optional parameter signature. |
| public static string GetFriendlyURL(string friendlyName, bool allowPublicAccess = false) | ||
| { | ||
| string rootURL = String.Empty; | ||
| string pathBase = String.Empty; |
There was a problem hiding this comment.
Fixed in the restack: the unused allowPublicAccess parameter was removed from GetFriendlyURL and all call sites. In #251.
| public static string GetURL(string fileGUID, bool allowPublicAccess = false) | ||
| { | ||
| return (allowPublicAccess ? @"/public" : "") + @"/cms/files/?id=" + fileGUID; | ||
| string pathBase = HttpHelper.HttpContext?.Request.PathBase ?? String.Empty; | ||
| return pathBase + @"/CMS/Files?id=" + fileGUID; |
There was a problem hiding this comment.
Fixed together with the sibling comment: GetURL's unused parameter removed as well. In #251.
| const baseUrl = import.meta.env.VITE_VIPER_HOME | ||
| const router = createRouter({ | ||
| scrollBehavior: (to, from) => { |
There was a problem hiding this comment.
Fixed in the restack: createSpaRouter now applies the same normalization as bootstrap-spa (missing env value tolerated, trailing slash stripped). In #253.
| function oldUrlHref(oldUrl: string): string { | ||
| return import.meta.env.VITE_VIPER_1_HOME + oldUrl.replace(/^\//, "") | ||
| } |
There was a problem hiding this comment.
Fixed in the restack: the link joins host and path with exactly one slash regardless of the env var's trailing slash. In #253.
| async function loadBlocks(): Promise<ActivityItem[]> { | ||
| const res = await get(apiURL + "CMS/content?" + createUrlSearchParams({ status: "active" })) | ||
| if (!res.success) throw new Error("blocks") | ||
| return [...((res.result ?? []) as CmsContentBlock[])] | ||
| .sort((a, b) => new Date(b.modifiedOn).getTime() - new Date(a.modifiedOn).getTime()) |
There was a problem hiding this comment.
Already fixed on the branch: the activity rail requests server-sorted, server-limited pages (page/perPage/sortBy params) instead of sorting the full list client-side. In #253.
| catch (Exception) | ||
| { | ||
| // The upload is already in the managed store; any failed save would orphan it on | ||
| // disk, so drop the pending changes and remove the stored copy before rethrowing. | ||
| _context.ChangeTracker.Clear(); | ||
| _storage.DeleteManagedFile(finalPath); | ||
| throw; | ||
| } |
There was a problem hiding this comment.
Fixed in the restack: the create reconcile path logs the underlying save failure (no user-controlled strings) before rethrowing. In #251.
There was a problem hiding this comment.
Follow-up: the added log line tripped Sonar S2139 (log-and-rethrow double-logs) on the restack's ReSharper gate. Since the rethrown exception is already logged at its handling layer (LogFileConflict for expected 400s, the global handler for 500s) and reconcile failures log critically in the revert helpers, the extra log was removed again in the second-round fixes.
| catch (Exception) | ||
| { | ||
| // Any failure type means the new state was not persisted; drop the pending changes | ||
| // and reconcile the on-disk file back to its original encryption state before rethrowing. | ||
| _context.ChangeTracker.Clear(); | ||
| RevertCryptoOnSaveFailure(entity, originalEncrypted, originalKey); | ||
| throw; | ||
| } |
There was a problem hiding this comment.
Fixed in the restack: the update/crypto reconcile path logs the failure with the file GUID before rethrowing. In #251.
There was a problem hiding this comment.
Follow-up: removed again with its sibling for S2139 (see the reply above) - the crypto reconcile path's failures still log critically in RevertCryptoOnSaveFailure.
|
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 4 of 6 (stacks on #247 — the diff below shows only this slice).
Scope