Skip to content

[Sprint 19] UX parity hardening for Create/Edit markdown editor (#326) - #332

Merged
1 commit merged into
devfrom
squad/326-ux-parity-hardening
May 13, 2026
Merged

[Sprint 19] UX parity hardening for Create/Edit markdown editor (#326)#332
1 commit merged into
devfrom
squad/326-ux-parity-hardening

Conversation

@mpaulosky

Copy link
Copy Markdown
Owner

Summary

Closes #326

Working as Legolas (Frontend / Blazor)

Parity Gaps Found and Fixed

Gap Create (before) Edit Fix Applied
Label text Markdown (bare) Content (Markdown) with hint Updated Create label to match Edit pattern
Binding style Explicit Content= + ContentChanged= @bind-Content (idiomatic) Switched Create to @bind-Content
Editor height/border/padding ✅ already consistent ✅ already consistent No change needed
Blank flash guard n/a (no async pre-load) _isLoading guard already present No change needed

Files Changed

  • src/Web/Features/BlogPosts/Create/Create.razor — label and binding parity
  • tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs — two new parity tests

Test Results

Suite Result
Architecture.Tests ✅ 16/16
Domain.Tests ✅ 42/42
Web.Tests ✅ 158/158
Web.Tests.Bunit ✅ 94/94
Integration.Tests ✅ passed

All 6 pre-push gates green.

Dependencies

Copilot AI review requested due to automatic review settings May 13, 2026 07:01
@github-actions github-actions Bot added the squad Squad triage inbox — Lead will assign to a member label May 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ PR Added to Squad Triage Queue

This PR has been labeled with squad and added to the triage queue.

Next steps:

  • The squad Lead will review and assign to an appropriate team member
  • A squad:member label will be added after triage

If you know which squad member should handle this, you can add the appropriate squad:member label yourself.

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 aims to harden UX parity between Blog Post Create and Edit by standardizing the markdown authoring experience (labeling + shared editor component), and by wiring in supporting editor assets and dependencies.

Changes:

  • Replaced Create/Edit InputTextArea with a shared TextEditor markdown component and aligned the “Content (Markdown)” label presentation.
  • Introduced baseline file-storage infrastructure (local disk) and registered it in DI to support markdown editor image uploads.
  • Updated bUnit smoke tests to render/interact with TextEditor, and added parity assertions for the label hint.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
Directory.Packages.props Adds centrally-managed package versions for the markdown editor dependencies.
src/Web/Web.csproj Adds package references for Markdig + PSC markdown editor.
src/Web/Program.cs Registers file storage services in DI.
src/Web/GlobalUsings.cs Adds global using for file storage types.
src/Web/Components/App.razor Loads editor CSS/JS and Font Awesome for toolbar icons.
src/Web/Components/Shared/TextEditor.razor New shared markdown editor wrapper with toolbar + image upload hook.
src/Web/wwwroot/js/text-editor-helpers.js Adds JS helper used by TextEditor for selection logic.
src/Web/wwwroot/js/markdown-editor-init.js Adds (currently unused) custom interop wrapper for editor initialization.
src/Web/Infrastructure/FileStorage/IFileStorage.cs Introduces file storage abstraction.
src/Web/Infrastructure/FileStorage/FileStorageModels.cs Adds storage models (FileData, FileMetaData).
src/Web/Infrastructure/FileStorage/FileStorageExtensions.cs Adds DI registration extension method.
src/Web/Infrastructure/FileStorage/LocalDiskFileStorage.cs Implements local-disk storage under wwwroot/uploads.
src/Web/Features/BlogPosts/Create/Create.razor Swaps textarea for TextEditor and updates label hint.
src/Web/Features/BlogPosts/Edit/Edit.razor Swaps textarea for TextEditor and updates label hint.
tests/Web.Tests.Bunit/GlobalUsings.cs Adds global using for file storage types in bUnit tests.
tests/Web.Tests.Bunit/Features/EditAclTests.cs Configures loose JSInterop + registers IFileStorage test substitute.
tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs Updates tests to find/interact with TextEditor; adds label parity tests.
src/AppHost/MongoDbResourceBuilderExtensions.cs Minor formatting fix (brace alignment).


// Persist the file and build a retrieval URL
var fileName = await FileStorage.AddFileAsync(new FileData(stream, new FileMetaData(file.Name, string.Empty, file.LastModified)));
file.UploadUrl = new Uri(new Uri(NavigationManager.BaseUri), $"/api/files/{fileName}").ToString();
Comment on lines +76 to +83
// Convert the base64 string to a byte array and load it into a memory stream
var bytes = Convert.FromBase64String(file.ContentBase64);
var stream = new MemoryStream(bytes);

// Persist the file and build a retrieval URL
var fileName = await FileStorage.AddFileAsync(new FileData(stream, new FileMetaData(file.Name, string.Empty, file.LastModified)));
file.UploadUrl = new Uri(new Uri(NavigationManager.BaseUri), $"/api/files/{fileName}").ToString();
return file;
Comment on lines +72 to +75
public async Task<FileEntry> HandleImageUpload(MarkdownEditor editor, FileEntry file)
{
Console.WriteLine($"Image: {file.Name} Size: {file.Size}");

Comment on lines +86 to +101
public async Task OnCustomButtonClicked(MarkdownButtonEventArgs args)
{
// Get the selected text using JavaScript
var selectedText = await JsRuntime.InvokeAsync<string>("getSelectedText", ".EasyMDEContainer textarea");

if (!string.IsNullOrEmpty(selectedText))
{
// Wrap the selected text with the chosen alignment
var wrappedText = $"<div style='text-align:{args.Value}'>{selectedText}</div>";

// Replace the selected text in the editor
MyContent = MyContent.Replace(selectedText, wrappedText);

// Update the editor
await _textEditor.SetValueAsync(MyContent);
}
Comment on lines +36 to +46
public async Task<string> AddFileAsync(FileData file)
{
if (string.IsNullOrEmpty(_env.WebRootPath))
{
throw new InvalidOperationException("WebRootPath is not configured.");
}

if (file.Content.Length > MaxFileSizeBytes)
{
throw new InvalidOperationException("File exceeds the maximum allowed size of 10 MB.");
}
var uniqueName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, uniqueName);

await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
<ResourcePreloader />
<link rel="stylesheet" href="css/tailwind.css" />
<link rel="stylesheet" href="@Assets["Web.styles.css"]" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
Comment on lines +1 to +71
// Custom initialization wrapper for PSC.Blazor.Components.MarkdownEditor
// This ensures proper loading and initialization of the markdown editor

window.markdownEditorInterop = {
editors: {},

initialize: function (dotNetObjectRef, elementRef, elementId, options) {
try {
// Wait for EasyMDE to be available
if (typeof EasyMDE === 'undefined') {
console.error('EasyMDE is not loaded');
return;
}

// Create the editor instance
const textarea = document.getElementById(elementId);
if (!textarea) {
console.error('Textarea element not found:', elementId);
return;
}

const editor = new EasyMDE({
element: textarea,
...options
});

// Store the editor instance
this.editors[elementId] = {
editor: editor,
dotNetRef: dotNetObjectRef
};

// Set up event handlers
editor.codemirror.on('change', () => {
const value = editor.value();
dotNetObjectRef.invokeMethodAsync('OnEditorChanged', value);
});

console.log('MarkdownEditor initialized successfully:', elementId);
} catch (error) {
console.error('Error initializing markdown editor:', error);
}
},

dispose: function (elementId) {
if (this.editors[elementId]) {
const editorData = this.editors[elementId];
if (editorData.editor && editorData.editor.toTextArea) {
editorData.editor.toTextArea();
}
delete this.editors[elementId];
console.log('MarkdownEditor disposed:', elementId);
}
},

getValue: function (elementId) {
if (this.editors[elementId] && this.editors[elementId].editor) {
return this.editors[elementId].editor.value();
}
return '';
},

setValue: function (elementId, value) {
if (this.editors[elementId] && this.editors[elementId].editor) {
this.editors[elementId].editor.value(value);
}
}
};

// Ensure the interop is available globally
console.log('Markdown Editor Interop Loaded');
Comment thread src/Web/Web.csproj
Comment on lines 12 to 20
<PackageReference Include="Auth0.ManagementApi" />
<PackageReference Include="FluentValidation.AspNetCore" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="Markdig" />
<PackageReference Include="MediatR" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="MongoDB.EntityFrameworkCore" />
<PackageReference Include="PSC.Blazor.Components.MarkdownEditor" />
<PackageReference Include="Snappier" />
@mpaulosky
mpaulosky force-pushed the squad/326-ux-parity-hardening branch from 0e6458b to 3d1f50f Compare May 13, 2026 16:51
@mpaulosky

Copy link
Copy Markdown
Owner Author

🤖 Aragorn — Rebase complete

Branch squad/326-ux-parity-hardening rebased onto squad/325 tip (7d12770) via git rebase --onto squad/325-edit-flow-markdown-slice f65bd63. Clean rebase — no conflicts. All pre-push gates passed ✅

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Test Results Summary

380 tests  +2   379 ✅ +2   21s ⏱️ -1s
  6 suites ±0     1 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 9e567c0. ± Comparison against base commit a269e2a.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.66%. Comparing base (2a75eb4) to head (a5a1000).
⚠️ Report is 3 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #332      +/-   ##
==========================================
- Coverage   81.67%   81.66%   -0.02%     
==========================================
  Files          51       51              
  Lines        1370     1369       -1     
  Branches      166      166              
==========================================
- Hits         1119     1118       -1     
  Misses        184      184              
  Partials       67       67              
Files with missing lines Coverage Δ
src/Web/Features/BlogPosts/Create/Create.razor 79.41% <ø> (-0.59%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mpaulosky
mpaulosky force-pushed the squad/326-ux-parity-hardening branch from 3d1f50f to d78a3a6 Compare May 13, 2026 17:24
@mpaulosky

Copy link
Copy Markdown
Owner Author

🔁 Aragorn restack — rebased onto dev via stacked-chain (after #331). No conflicts. Force-pushed with --force-with-lease. Branch now at d78a3a6.

- Align Create.razor label from 'Markdown' to 'Content (Markdown)'
  matching Edit.razor's label pattern for visual parity
- Switch Create.razor TextEditor binding to @bind-Content for
  idiomatic Blazor two-way binding, consistent with Edit.razor
- Add bUnit parity tests: CreatePostShowsMarkdownContentLabel and
  EditPostShowsMarkdownContentLabel asserting label presence on both
  pages

No behavioral change — form submit, validation, and navigation paths
are unchanged. Editor is not rendered before data loads in Edit
(existing _isLoading guard remains in place).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
mpaulosky force-pushed the squad/326-ux-parity-hardening branch from 9e567c0 to a5a1000 Compare May 13, 2026 18:00
@mpaulosky mpaulosky closed this pull request by merging all changes into dev in 21a173a May 13, 2026
@mpaulosky
mpaulosky deleted the squad/326-ux-parity-hardening branch May 13, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squad Squad triage inbox — Lead will assign to a member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Sprint 19] Shared UX parity + editor presentation hardening

2 participants