Skip to content

feat(ui): replace InputTextArea with TextEditor markdown editor in Create and Edit flows - #330

Closed
mpaulosky wants to merge 13 commits into
devfrom
squad/324-create-flow-markdown-slice
Closed

feat(ui): replace InputTextArea with TextEditor markdown editor in Create and Edit flows#330
mpaulosky wants to merge 13 commits into
devfrom
squad/324-create-flow-markdown-slice

Conversation

@mpaulosky

Copy link
Copy Markdown
Owner

Summary

Closes #324

Working as Legolas (Frontend/UI)

Changes

  • Directory.Packages.props: Fix CPM NU1010 — add Markdig v0.44.0 and PSC.Blazor.Components.MarkdownEditor v10.0.7
  • App.razor: Wire Font Awesome 6.7.2 CDN + PSC EasyMDE CSS/JS + text-editor-helpers.js into the app shell
  • Create.razor: Replace <InputTextArea> with <TextEditor> (label: Content → Markdown)
  • Edit.razor: Replace <InputTextArea> with <TextEditor @bind-Content>
  • RazorSmokeTests.cs: Update 3 Create tests — use FindComponent<TextEditor>().Instance.ContentChanged.InvokeAsync() instead of textarea.Change() (PSC uses JS interop, not DOM events)
  • EditAclTests.cs: Add JSInterop.Mode = Loose and IFileStorage mock
  • GlobalUsings.cs: Add global using MyBlog.Web.Infrastructure.FileStorage

Test results

All pre-push gates passed: format, build, unit/arch/bUnit (308 tests), integration.

Blocked by

Requires #323 merged first (based on squad/323-restore-markdown-editor-compile-path).

Boromir and others added 13 commits May 11, 2026 10:45
…ession

- Merge decision inbox into .squad/decisions.md (decisions 30-31)
- Decision 30: Dark Mode Base Text Colours Must Contrast Against Dark Backgrounds (Legolas)
- Decision 31: CSS Visual Regressions and Test/Visual Finding Arbitration (Aragorn)
- Update Legolas history: UI regression review findings (Sprint 16 fan-out)
- Update Gimli history: Blazor UI regression testing (Issue #292 coverage + visual findings)
- Update Aragorn history: Issue triage (#292) and PR #295 arbitration workflow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…und 1)

- Closed duplicate issue #294, merged PR #295 with all 19 CI checks green
- Sprint-stamped issues #293 and #296 with title normalization
- Removed go:needs-research from #293 (sufficient context), kept for #296 (Auth investigation needed)
- Decision #32: PR self-authored approval pattern and Sprint triage workflow

Co-authored-by: Boromir <lead-organizer@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace old non-existent project ID (PVT_kwHOA5k0b84BVFTy) with new one (PVT_kwHOA5k0b84BXZpa)
- Update STATUS_FIELD_ID to match new project (PVTSSF_lAHOA5k0b84BXZpazhSmuGY)
- Map old status options to available field options (Backlog/InSprint: f75ad846, InReview: 47fc9ee4, Done: 98236657)
- Use GH_PROJECT_TOKEN secret for GraphQL mutations (fallback to GITHUB_TOKEN)
- Fixes issue where project board sync was failing silently due to missing project

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace token fallback with explicit GH_PROJECT_TOKEN requirement
- Ensures workflows have required 'project' scope for user-owned project boards
- Fixes 'Resource not accessible by integration' errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Kept HEAD version with correct option IDs from board fix:
- IN_SPRINT_OPTION_ID: f75ad846
- IN_REVIEW_OPTION_ID: 47fc9ee4

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t test

- Edit.razor: null-value branch now navigates to /blog instead of
  leaving _model null (which caused infinite 'Loading...' state)
- EditAclTests: added EditRedirectsToBlogWhenPostNotFound covering
  the Result.Ok(null) path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#309, #310)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ox, seed schema)

Resolves merge conflict in MongoDbResourceBuilderExtensions.cs — indentation-only divergence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add PSC.Blazor.Components.MarkdownEditor and Markdig package references
  to Web.csproj (both centrally pinned in Directory.Packages.props)
- Add IFileStorage infrastructure (interface, FileData/FileMetaData models,
  LocalDiskFileStorage impl, DI extension) under Infrastructure/FileStorage/
- Register IFileStorage via AddFileStorage() in Program.cs
- Fix TextEditor.razor: add @using for Infrastructure.FileStorage namespace,
  rename AddFile -> AddFileAsync to match the new interface
- Add GlobalUsings entry for MyBlog.Web.Infrastructure.FileStorage
- Include JS helpers (markdown-editor-init.js, text-editor-helpers.js)

Build: Release 0 errors
Tests: Architecture 16 / Domain 42 / Web.Tests 158 / bUnit 92 - all pass

Closes #323

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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>
Copilot AI review requested due to automatic review settings May 13, 2026 06:49
@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.

@github-actions github-actions Bot added the squad Squad triage inbox — Lead will assign to a member label May 13, 2026
@mpaulosky

Copy link
Copy Markdown
Owner Author

Superseded by #331 which covers identical scope (both Create and Edit flows). Closing to avoid merge conflicts — #331 will close both #324 and #325.

@mpaulosky mpaulosky closed this May 13, 2026

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 upgrades the Blog Post Create/Edit authoring experience by replacing the plain <InputTextArea> with a shared markdown editor component, including the required client assets and test updates to accommodate JS interop.

Changes:

  • Introduces a shared TextEditor Razor component backed by PSC.Blazor.Components.MarkdownEditor and wires required CSS/JS in App.razor.
  • Updates Create/Edit flows to use the new markdown editor component instead of a textarea.
  • Adds a baseline local-disk IFileStorage abstraction + DI registration, and updates bUnit tests to run with loose JS interop and provide an IFileStorage mock.

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 versions for Markdig + PSC markdown editor packages.
src/Web/Web.csproj References Markdig and PSC markdown editor packages from CPM.
src/Web/Components/App.razor Loads Font Awesome + PSC editor CSS/JS and text-editor-helpers.js.
src/Web/Features/BlogPosts/Create/Create.razor Swaps textarea for TextEditor in Create form.
src/Web/Features/BlogPosts/Edit/Edit.razor Swaps textarea for TextEditor in Edit form (binds to content).
src/Web/Components/Shared/TextEditor.razor New shared wrapper component for markdown editing + image upload + custom toolbar behavior.
src/Web/wwwroot/js/text-editor-helpers.js Adds helper to read selected text for custom toolbar actions.
src/Web/wwwroot/js/markdown-editor-init.js Adds a custom EasyMDE init wrapper (currently not referenced).
src/Web/Infrastructure/FileStorage/IFileStorage.cs Introduces internal storage abstraction for editor uploads.
src/Web/Infrastructure/FileStorage/FileStorageModels.cs Adds FileData + FileMetaData records used by storage.
src/Web/Infrastructure/FileStorage/LocalDiskFileStorage.cs Implements local-disk storage under wwwroot/uploads.
src/Web/Infrastructure/FileStorage/FileStorageExtensions.cs Registers storage implementation with DI.
src/Web/Program.cs Registers file storage via AddFileStorage().
src/Web/GlobalUsings.cs Adds global using for FileStorage namespace in Web.
tests/Web.Tests.Bunit/GlobalUsings.cs Adds global using for FileStorage namespace in bUnit tests.
tests/Web.Tests.Bunit/Features/EditAclTests.cs Enables loose JS interop and registers IFileStorage mock.
tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs Enables loose JS interop, registers IFileStorage mock, updates Create/Edit assertions to use TextEditor.
src/AppHost/MongoDbResourceBuilderExtensions.cs Minor formatting/indentation fix.


// 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 +88 to +100
// 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 +72 to +78
public async Task<FileEntry> HandleImageUpload(MarkdownEditor editor, FileEntry file)
{
Console.WriteLine($"Image: {file.Name} Size: {file.Size}");

// 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);
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 +43 to +63
if (file.Content.Length > MaxFileSizeBytes)
{
throw new InvalidOperationException("File exceeds the maximum allowed size of 10 MB.");
}

var extension = Path.GetExtension(file.Metadata.FileName).ToUpperInvariant();
if (string.IsNullOrEmpty(extension) || !AllowedExtensions.Contains(extension))
{
throw new InvalidOperationException(
$"File type '{extension}' is not allowed. Only images are permitted.");
}

var uploadsPath = Path.Combine(_env.WebRootPath, "uploads");
Directory.CreateDirectory(uploadsPath);

var uniqueName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsPath, uniqueName);

await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write);
await file.Content.CopyToAsync(fs).ConfigureAwait(false);

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');
@mpaulosky
mpaulosky deleted the squad/324-create-flow-markdown-slice branch May 14, 2026 23:14
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] Create flow markdown authoring slice

2 participants