Skip to content

feat(web): Edit flow markdown authoring slice (#325) - #331

Merged
mpaulosky merged 1 commit into
devfrom
squad/325-edit-flow-markdown-slice
May 13, 2026
Merged

feat(web): Edit flow markdown authoring slice (#325)#331
mpaulosky merged 1 commit into
devfrom
squad/325-edit-flow-markdown-slice

Conversation

@mpaulosky

@mpaulosky mpaulosky commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

Working as Legolas (UI/Frontend).

Replaces InputTextArea with the TextEditor shared component in the Edit blog post flow, so authors get the full EasyMDE markdown editor when editing posts. Also includes the Create flow wiring (covered by #324) since both depend on the same shared infrastructure.

Changes

Infrastructure (shared with Create flow)

Feature: Edit flow (#325)

  • src/Web/Features/BlogPosts/Edit/Edit.razor — Replaces <InputTextArea> with <TextEditor @bind-Content="_model.Content" AlignmentOptionsEnabled="false" />; initial content loads correctly via the existing @if (_model is not null) guard

Feature: Create flow (#324)

  • src/Web/Features/BlogPosts/Create/Create.razor — Same TextEditor swap for Create form

Tests

  • tests/Web.Tests.Bunit/GlobalUsings.cs — Adds MyBlog.Web.Infrastructure.FileStorage global using
  • tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs — Registers IFileStorage mock + sets JSInterop.Mode = Loose; updates EditPostLoadsExistingPost to assert via FindComponent<TextEditor>().Instance.Content (PSC renders textarea via JS, not static HTML); fixes unawaited-task CS4014 error on NSubstitute verification
  • tests/Web.Tests.Bunit/Features/EditAclTests.cs — Registers IFileStorage mock + sets JSInterop.Mode = Loose

How it works

Edit.razor guards the form with @if (_model is not null), so TextEditor is only instantiated after OnParametersSetAsync loads the existing post. This means the Content parameter receives the existing post content at first render — no extra lifecycle workaround needed.

Test results

All 92 bUnit tests pass (0 failures). All integration and architecture tests pass.

Closes #325
Also addresses #324

⚠️ Blocked by #323 — must be merged into dev first.

Closes #324
Closes #325

Copilot AI review requested due to automatic review settings May 13, 2026 06:53
@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 updates the Blog Post Create and Edit flows to use a shared TextEditor component backed by PSC.Blazor.Components.MarkdownEditor (EasyMDE), adding the supporting web assets, package references, and test adjustments needed to run the editor in bUnit.

Changes:

  • Add shared markdown editor infrastructure (packages, app shell CSS/JS, TextEditor component, JS helper).
  • Wire TextEditor into BlogPost Create/Edit forms and register IFileStorage (local disk) for editor upload integration.
  • Update bUnit smoke/ACL tests to accommodate JS-based editor rendering and DI requirements.

Reviewed changes

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

Show a summary per file
File Description
Directory.Packages.props Adds CPM-pinned versions for Markdig + PSC markdown editor.
src/Web/Web.csproj Adds package references for Markdig and PSC markdown editor.
src/Web/Components/App.razor Adds Font Awesome + PSC editor CSS/JS and a local JS helper script.
src/Web/Components/Shared/TextEditor.razor Introduces shared markdown editor wrapper with toolbar + image upload hooks.
src/Web/wwwroot/js/text-editor-helpers.js Adds JS function used by the editor for selected-text operations.
src/Web/wwwroot/js/markdown-editor-init.js Adds a custom init/dispose interop wrapper for EasyMDE (currently not wired).
src/Web/Program.cs Registers IFileStorage implementation in DI.
src/Web/Infrastructure/FileStorage/IFileStorage.cs Adds file storage abstraction used by the editor upload path.
src/Web/Infrastructure/FileStorage/FileStorageModels.cs Adds file + metadata records for storage operations.
src/Web/Infrastructure/FileStorage/FileStorageExtensions.cs Adds DI registration extension for file storage.
src/Web/Infrastructure/FileStorage/LocalDiskFileStorage.cs Implements local-disk storage under wwwroot/uploads.
src/Web/GlobalUsings.cs Adds global using for the file storage namespace.
src/Web/Features/BlogPosts/Edit/Edit.razor Replaces textarea with TextEditor for markdown editing.
src/Web/Features/BlogPosts/Create/Create.razor Replaces textarea with TextEditor for markdown authoring.
tests/Web.Tests.Bunit/GlobalUsings.cs Adds global using for file storage types in bUnit tests.
tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs Updates Create/Edit assertions + sets Loose JSInterop + registers IFileStorage mock.
tests/Web.Tests.Bunit/Features/EditAclTests.cs Sets Loose JSInterop + registers IFileStorage mock.
src/AppHost/MongoDbResourceBuilderExtensions.cs Minor formatting fix in seed document initialization.
Comments suppressed due to low confidence (3)

src/Web/Components/Shared/TextEditor.razor:83

  • HandleImageUpload allocates a MemoryStream but never disposes it. Wrap the stream in a using/await using (or dispose in a finally) after AddFileAsync completes to avoid leaking memory under repeated uploads.
		// 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;

src/Web/Components/Shared/TextEditor.razor:75

  • Avoid Console.WriteLine in components; it bypasses structured logging and can spam server logs in production. Use an injected ILogger<TextEditor> (or remove the log) for upload diagnostics.
	public async Task<FileEntry> HandleImageUpload(MarkdownEditor editor, FileEntry file)
	{
		Console.WriteLine($"Image: {file.Name} Size: {file.Size}");

src/Web/Components/Shared/TextEditor.razor:101

  • MyContent.Replace(selectedText, wrappedText) will replace all occurrences of the selected text in the document, not just the current selection. Use the editor/CodeMirror API to replace the current selection (or compute a single-range replacement) so alignment only affects what the user selected.
		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);
		}


// 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 +14 to +43
<MarkdownToolbarButton Action="MarkdownAction.Bold" Name="Bold" Icon="fa fa-bold" Title="Bold" />
<MarkdownToolbarButton Action="MarkdownAction.Italic" Name="Italic" Icon="fa fa-italic" Title="Italic" />
<MarkdownToolbarButton Action="MarkdownAction.Strikethrough" Name="Strike Through" Icon="fa fa-strikethrough"
Title="Strike Through" />
<MarkdownToolbarButton Action="MarkdownAction.HorizontalRule" Name="Horizontal Rule" Icon="fa fa-window-minimize"
Title="Horizontal Rule" />
<MarkdownToolbarButton Separator Action="MarkdownAction.Heading" Name="Header" Icon="fa fa-header"
Title="Heading" />
<MarkdownToolbarButton Action="MarkdownAction.Quote" Name="Add Quote" Icon="fa fa-quote-left" Title="Quote" />
<MarkdownToolbarButton Action="MarkdownAction.UnorderedList" Name="Add Un-ordered-List" Icon="fa fa-list-ul"
Title="Unordered List" />
<MarkdownToolbarButton Action="MarkdownAction.OrderedList" Name="Add Ordered-List" Icon="fa fa-list-ol"
Title="ordered List" />

@if (AlignmentOptionsEnabled)
{
<MarkdownToolbarButton Separator Name="Align Left" Value="@("left")" Icon="fa fa-align-left" Title="Align Left" />
<MarkdownToolbarButton Name="Align Centre" Value="@("center")" Icon="fa fa-align-center" Title="Align Centre" />
<MarkdownToolbarButton Name="Align Right" Value="@("right")" Icon="fa fa-align-right" Title="Align Right" />
}

<MarkdownToolbarButton Separator Action="MarkdownAction.Link" Name="Add Link" Icon="fa fa-link" Title="Link" />
<MarkdownToolbarButton Action="MarkdownAction.Image" Name="Add Image" Icon="fa fa-image" Title="Image" />
<MarkdownToolbarButton Action="MarkdownAction.Table" Name="Add Table" Icon="fa fa-table" Title="Add Table" />
<MarkdownToolbarButton Separator Action="MarkdownAction.Preview" Name="preview" Icon="no-disable fa fa-eye"
Title="Toggle Preview" />
<MarkdownToolbarButton Action="MarkdownAction.Code" Name="Code Block" Icon="fa fa-code" Title="Code Block" />
<MarkdownToolbarButton Separator Action="MarkdownAction.Fullscreen" Name="Fullscreen" Icon="fa fa-arrows-alt"
Title="Fullscreen" />
<MarkdownToolbarButton Separator Action="MarkdownAction.Guide" Name="Markdown Guide" Icon="fa fa-question-circle"
{
/// <summary>
/// Persists <paramref name="file"/> and returns the stored filename that callers
/// can use to build a retrieval URL (e.g. <c>/api/files/{filename}</c>).
Comment on lines +43 to +46
if (file.Content.Length > MaxFileSizeBytes)
{
throw new InvalidOperationException("File exceeds the maximum allowed size of 10 MB.");
}
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');
@@ -0,0 +1,11 @@
(function () {
window.getSelectedText = function (cssSelector) {
@mpaulosky
mpaulosky force-pushed the squad/325-edit-flow-markdown-slice branch from f65bd63 to 7d12770 Compare May 13, 2026 16:47
@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Test Results Summary

378 tests  ±0   377 ✅ ±0   21s ⏱️ -1s
  6 suites ±0     1 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit e90feee. ± Comparison against base commit 6d9ffd0.

♻️ This comment has been updated with latest results.

@mpaulosky

Copy link
Copy Markdown
Owner Author

🤖 Aragorn — Rebase complete

Branch squad/325-edit-flow-markdown-slice rebased onto origin/dev. Resolved conflicts in:

  • App.razor — kept PSC EasyMDE scripts
  • Create.razor / Edit.razor — kept <TextEditor> component
  • RazorSmokeTests.cs — removed all conflict markers, restored missing method declaration
  • Web.csproj — removed RTBlazorfied PackageReference
  • Components/_Imports.razor + Features/_Imports.razor — replaced @using RichTextBlazorfied with @using PSC.Blazor.Components.MarkdownEditor (fixed CS0246 build errors)
  • RichTextEditorTests.cs — rewired tests to use TextEditor with IFileStorage mock

All pre-push gates passed ✅

@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.67%. Comparing base (a269e2a) to head (cbc3aaa).
⚠️ Report is 2 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #331      +/-   ##
==========================================
+ Coverage   80.64%   81.67%   +1.03%     
==========================================
  Files          51       51              
  Lines        1369     1370       +1     
  Branches      166      166              
==========================================
+ Hits         1104     1119      +15     
+ Misses        198      184      -14     
  Partials       67       67              
Files with missing lines Coverage Δ
src/Web/Features/BlogPosts/Create/Create.razor 80.00% <100.00%> (+0.58%) ⬆️
src/Web/Features/BlogPosts/Edit/Edit.razor 91.37% <ø> (ø)

... and 1 file with indirect coverage changes

🚀 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

Copy link
Copy Markdown
Owner Author

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

@mpaulosky
mpaulosky enabled auto-merge May 13, 2026 17:25
…eate and Edit flows (#324)

- Fix CPM NU1010: add Markdig v0.44.0 and PSC.Blazor.Components.MarkdownEditor v10.0.7 to Directory.Packages.props
- Wire Font Awesome CDN + PSC EasyMDE CSS/JS into App.razor shell
- Replace InputTextArea with TextEditor component in Create.razor (label: Content → Markdown)
- Replace InputTextArea with TextEditor component in Edit.razor
- Add IFileStorage mock and JSInterop.Mode = Loose to EditAclTests constructor
- Add global using for MyBlog.Web.Infrastructure.FileStorage in bUnit test project
- Update RazorSmokeTests: use FindComponent<TextEditor>() and ContentChanged.InvokeAsync() instead of textarea.Change()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
mpaulosky force-pushed the squad/325-edit-flow-markdown-slice branch from e90feee to cbc3aaa Compare May 13, 2026 17:45
@mpaulosky
mpaulosky merged commit 2a75eb4 into dev May 13, 2026
15 checks passed
@mpaulosky
mpaulosky deleted the squad/325-edit-flow-markdown-slice branch May 13, 2026 17:50
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] Edit flow markdown authoring slice [Sprint 19] Create flow markdown authoring slice

2 participants