feat(ui): replace InputTextArea with TextEditor markdown editor in Create and Edit flows - #330
Closed
mpaulosky wants to merge 13 commits into
Closed
feat(ui): replace InputTextArea with TextEditor markdown editor in Create and Edit flows#330mpaulosky wants to merge 13 commits into
mpaulosky wants to merge 13 commits into
Conversation
…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>
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>
Contributor
🏗️ PR Added to Squad Triage QueueThis PR has been labeled with Next steps:
|
3 tasks
There was a problem hiding this comment.
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
TextEditorRazor component backed byPSC.Blazor.Components.MarkdownEditorand wires required CSS/JS inApp.razor. - Updates Create/Edit flows to use the new markdown editor component instead of a textarea.
- Adds a baseline local-disk
IFileStorageabstraction + DI registration, and updates bUnit tests to run with loose JS interop and provide anIFileStoragemock.
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'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #324
Working as Legolas (Frontend/UI)
Changes
Directory.Packages.props: Fix CPM NU1010 — addMarkdig v0.44.0andPSC.Blazor.Components.MarkdownEditor v10.0.7App.razor: Wire Font Awesome 6.7.2 CDN + PSC EasyMDE CSS/JS +text-editor-helpers.jsinto the app shellCreate.razor: Replace<InputTextArea>with<TextEditor>(label: Content → Markdown)Edit.razor: Replace<InputTextArea>with<TextEditor @bind-Content>RazorSmokeTests.cs: Update 3 Create tests — useFindComponent<TextEditor>().Instance.ContentChanged.InvokeAsync()instead oftextarea.Change()(PSC uses JS interop, not DOM events)EditAclTests.cs: AddJSInterop.Mode = LooseandIFileStoragemockGlobalUsings.cs: Addglobal using MyBlog.Web.Infrastructure.FileStorageTest 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).