Skip to content

VPR-59 [4/6] CMS migration: hub redesign, a11y, form-UX standardization#248

Closed
rlorenzo wants to merge 7 commits into
VPR-59-cms-3-import-photosfrom
VPR-59-cms-4-hub-ux
Closed

VPR-59 [4/6] CMS migration: hub redesign, a11y, form-UX standardization#248
rlorenzo wants to merge 7 commits into
VPR-59-cms-3-import-photosfrom
VPR-59-cms-4-hub-ux

Conversation

@rlorenzo

@rlorenzo rlorenzo commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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

Scope

  • CMS hub redesign with a recent-activity rail; CMS left nav; shell a11y in the shared router (skip-to-section-menu, focus ring, link roles, scroll/focus guards).
  • a11y and label-consistency pass over all nine CMS pages (SR text for icon-only status, loading slots, brand link color, 24px targets).
  • VIPER2 download links, shareable filter URLs, folder fixes; copy-link button removed from row actions; Old URL links point at the VIPER 1 host.
  • Upload name-conflict resolution (rename/overwrite prompt instead of an always-visible checkbox) and import preview.
  • Standardized form UX, navigation, and list filters across the area.

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

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

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.

Comment thread web/Areas/CMS/Data/CMS.cs
Comment on lines 284 to +287
public static string GetFriendlyURL(string friendlyName, bool allowPublicAccess = false)
{
string rootURL = String.Empty;
string pathBase = String.Empty;

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: the unused allowPublicAccess parameter was removed from GetFriendlyURL and all call sites. In #251.

Comment thread web/Areas/CMS/Data/CMS.cs
Comment on lines 305 to +308
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;

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 together with the sibling comment: GetURL's unused parameter removed as well. In #251.

Comment on lines +14 to +16
const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
scrollBehavior: (to, from) => {

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: createSpaRouter now applies the same normalization as bootstrap-spa (missing env value tolerated, trailing slash stripped). In #253.

Comment on lines +255 to +257
function oldUrlHref(oldUrl: string): string {
return import.meta.env.VITE_VIPER_1_HOME + oldUrl.replace(/^\//, "")
}

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: the link joins host and path with exactly one slash regardless of the env var's trailing slash. In #253.

Comment on lines +95 to +99
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())

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 activity rail requests server-sorted, server-limited pages (page/perPage/sortBy params) instead of sorting the full list client-side. In #253.

Comment on lines +291 to +298
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;
}

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: the create reconcile path logs the underlying save failure (no user-controlled strings) before rethrowing. In #251.

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.

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.

Comment on lines +418 to +425
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;
}

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: the update/crypto reconcile path logs the failure with the file GUID before rethrowing. In #251.

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.

Follow-up: removed again with its sibling for S2139 (see the reply above) - the crypto reconcile path's failures still log critically in RevertCryptoOnSaveFailure.

@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