From 5050c901bcdb68b06f679dc64fdf239288d353cc Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 23:36:03 +0200 Subject: [PATCH] Fixed notification composition --- .../notifications/ComposeMessageModal.tsx | 280 +- .../notifications/MarkdownViewer.tsx | 70 +- .../src/hooks/useInAppNotifications.ts | 218 +- .../src/lib/markdownUtils.ts | 18 +- .../src/pages/InboxPage.tsx | 132 +- .../Extensions/Constants.cs | 2 +- ..._IncreasedLargeTextLengthTo1MB.Designer.cs | 2519 +++++++++++++++++ ...022212219_IncreasedLargeTextLengthTo1MB.cs | 60 + .../ApplicationDbContextModelSnapshot.cs | 8 +- 9 files changed, 2949 insertions(+), 358 deletions(-) create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20261022212219_IncreasedLargeTextLengthTo1MB.Designer.cs create mode 100644 PostFundManagement.Infrastructure/Database/Migrations/20261022212219_IncreasedLargeTextLengthTo1MB.cs diff --git a/PostFundManagement.ConsoleUI/src/components/notifications/ComposeMessageModal.tsx b/PostFundManagement.ConsoleUI/src/components/notifications/ComposeMessageModal.tsx index 29cb8dd..83c0672 100644 --- a/PostFundManagement.ConsoleUI/src/components/notifications/ComposeMessageModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/notifications/ComposeMessageModal.tsx @@ -9,12 +9,12 @@ import { CheckCircle2, Bold, Italic, + Underline, Heading2, List, ListOrdered, Quote, Table, - Code, Minus, Sparkles, Eye, @@ -25,7 +25,13 @@ import { import { UserSearchPicker, SelectedUser } from './UserSearchPicker'; import { useInAppNotifications, DraftMessage } from '../../hooks/useInAppNotifications'; import { NotificationPlatform } from '../../types'; -import { renderMarkdownToHtml, parseMarkdownToHtml, PFM_LOGO_DATA_URI } from '../../lib/markdownUtils'; +import { + renderMarkdownToHtml, + parseMarkdownToHtml, + isHtmlContent, + stripMarkdown, + PFM_LOGO_DATA_URI, +} from '../../lib/markdownUtils'; import { BrandLogo } from '../ui/BrandLogo'; import { useAuth } from '../../auth/AuthProvider'; @@ -49,7 +55,10 @@ export const ComposeMessageModal: React.FC = ({ draftToEdit, }) => { const { user, claims } = useAuth(); - const tenantName = (claims?.tenant as string) || user?.tenant || 'Department of Science, Innovation & Technology'; + const tenantName = + (claims?.tenant as string) || + user?.tenant || + 'Department of Science, Innovation & Technology'; const { sendMessage, isSending, saveDraft, deleteDraft } = useInAppNotifications(); const [selectedUsers, setSelectedUsers] = useState([]); @@ -60,10 +69,19 @@ export const ComposeMessageModal: React.FC = ({ const [error, setError] = useState(null); const [successNotice, setSuccessNotice] = useState(null); - const textareaRef = useRef(null); + const editorRef = useRef(null); + + const toInitialRichHtml = (raw: string): string => { + if (!raw || !raw.trim()) return ''; + if (isHtmlContent(raw)) return raw; + return parseMarkdownToHtml(raw); + }; // Initialize or populate from draft or props useEffect(() => { + if (!isOpen) return; + + let initialHtml = ''; if (draftToEdit) { setSelectedUsers( draftToEdit.recipientId @@ -77,23 +95,40 @@ export const ComposeMessageModal: React.FC = ({ : [] ); setSubject(draftToEdit.subject || ''); - setMessage(draftToEdit.message || ''); - setPlatform(NotificationPlatform.InApp); + initialHtml = toInitialRichHtml(draftToEdit.message || ''); + setMessage(initialHtml); + setPlatform(draftToEdit.platform || NotificationPlatform.InApp); } else { setSelectedUsers(initialRecipient ? [initialRecipient] : []); setSubject(initialSubject || ''); - setMessage(initialMessage || ''); + initialHtml = toInitialRichHtml(initialMessage || ''); + setMessage(initialHtml); setPlatform(initialPlatform); } setActiveTab('write'); setError(null); setSuccessNotice(null); + + setTimeout(() => { + if (editorRef.current) { + editorRef.current.innerHTML = initialHtml; + } + }, 0); }, [isOpen, initialRecipient, initialSubject, initialMessage, initialPlatform, draftToEdit]); + // Restore editor innerHTML when switching back from preview tab + useEffect(() => { + if (activeTab === 'write' && editorRef.current) { + if (editorRef.current.innerHTML !== message) { + editorRef.current.innerHTML = message; + } + } + }, [activeTab]); + if (!isOpen) return null; - const handleSelectUser = (user: SelectedUser) => { - setSelectedUsers([user]); + const handleSelectUser = (selected: SelectedUser) => { + setSelectedUsers([selected]); setError(null); }; @@ -101,51 +136,59 @@ export const ComposeMessageModal: React.FC = ({ setSelectedUsers([]); }; - // Helper to insert markdown text at cursor in textarea - const insertMarkdown = (before: string, after: string = '', defaultText: string = '') => { - const textarea = textareaRef.current; - if (!textarea) return; + const syncEditorContent = () => { + if (editorRef.current) { + setMessage(editorRef.current.innerHTML); + } + }; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const text = textarea.value; - const selection = text.substring(start, end) || defaultText; + const applyFormat = (command: string, value?: string) => { + const editor = editorRef.current; + if (!editor) return; + editor.focus(); + document.execCommand(command, false, value); + syncEditorContent(); + }; - const replacement = `${before}${selection}${after}`; - const newText = text.substring(0, start) + replacement + text.substring(end); - - setMessage(newText); - - setTimeout(() => { - textarea.focus(); - textarea.setSelectionRange( - start + before.length, - start + before.length + selection.length - ); - }, 0); + const insertHtmlBlock = (htmlSnippet: string) => { + const editor = editorRef.current; + if (!editor) return; + editor.focus(); + const inserted = document.execCommand('insertHTML', false, htmlSnippet); + if (!inserted) { + editor.innerHTML += htmlSnippet; + } + syncEditorContent(); }; // Quick insertions for PFM branding and templates const insertBrandedCallout = () => { - insertMarkdown( - `> [!NOTE]\n> **Post-Fund Management Official Notice**\n> Please verify all supporting evidence documentation before tranche disbursement sign-off.\n\n` + insertHtmlBlock( + `
Post-Fund Management Official Notice
Please verify all supporting evidence documentation before tranche disbursement sign-off.


` ); }; const insertMilestoneTable = () => { - insertMarkdown( - `| Deliverable Code | Milestone Name | Status | Due Date |\n| :--- | :--- | :--- | :--- |\n| DEL-01 | Technical Architecture Review | Verified | 2026-11-15 |\n| DEL-02 | Financial Expenditure Proof | Under Review | 2026-12-01 |\n\n` + insertHtmlBlock( + `
Deliverable CodeMilestone NameStatusDue Date
DEL-01Technical Architecture ReviewVerified2026-11-15
DEL-02Financial Expenditure ProofUnder Review2026-12-01


` ); }; const insertPlatformLogo = () => { - insertMarkdown( - `![PFM Platform Logo](${PFM_LOGO_DATA_URI})\n\n` + insertHtmlBlock( + `

PFM Platform Logo


` ); }; + const hasMeaningfulContent = (html: string) => { + if (!html) return false; + if (stripMarkdown(html).trim().length > 0) return true; + return /<(img|table|hr)\b/i.test(html); + }; + const handleSaveDraft = () => { - if (!subject.trim() && !message.trim()) { + const currentHtml = editorRef.current ? editorRef.current.innerHTML : message; + if (!subject.trim() && !hasMeaningfulContent(currentHtml)) { setError('Please add a subject or message to save as draft'); return; } @@ -157,18 +200,21 @@ export const ComposeMessageModal: React.FC = ({ recipientEmail: firstUser?.email || '', recipientName: firstUser?.name || '', subject: subject.trim() || 'Untitled Draft', - message: message.trim(), + message: currentHtml.trim(), + platform, }); setSuccessNotice('Draft saved successfully in Drafts folder'); setTimeout(() => { onClose(); - }, 800); + }, 600); }; const handleSend = async (e: React.FormEvent) => { e.preventDefault(); + const currentHtml = editorRef.current ? editorRef.current.innerHTML : message; + if (selectedUsers.length === 0) { setError('Please select a recipient from the tenant directory'); return; @@ -179,7 +225,7 @@ export const ComposeMessageModal: React.FC = ({ return; } - if (!message.trim()) { + if (!hasMeaningfulContent(currentHtml)) { setError('Please enter a message body'); return; } @@ -188,18 +234,15 @@ export const ComposeMessageModal: React.FC = ({ try { const recipient = selectedUsers[0]; - // Format payload according to chosen platform: - // InApp messages are saved in markdown format. - // Email notifications are transformed from markdown to branded executive HTML before saving. const finalMessage = platform === NotificationPlatform.Email - ? renderMarkdownToHtml(message.trim(), { + ? renderMarkdownToHtml(currentHtml.trim(), { applyBranding: true, emailFormat: true, subject: subject.trim(), tenantName, }) - : message.trim(); + : currentHtml.trim(); await sendMessage({ recipientId: recipient.id, @@ -207,13 +250,14 @@ export const ComposeMessageModal: React.FC = ({ recipientName: recipient.name, subject: subject.trim(), message: finalMessage, + platform, draftId: draftToEdit?.id, }); setSuccessNotice( platform === NotificationPlatform.Email - ? 'Email notification rendered to HTML and dispatched!' - : 'In-App Markdown message sent successfully!' + ? 'Email notification dispatched!' + : 'In-App message sent successfully!' ); setTimeout(() => { onClose(); @@ -239,7 +283,9 @@ export const ComposeMessageModal: React.FC = ({ subject: subject || 'Notice Preview', tenantName, }) - : parseMarkdownToHtml(message); + : message; + + const plainCharCount = stripMarkdown(message).length; return (
@@ -258,11 +304,11 @@ export const ComposeMessageModal: React.FC = ({ : 'Compose New Communication'} - Rich Markdown Engine + Rich Text Composer

- Author in Markdown with live preview, SVG branding, and dual InApp / Email delivery + Type and format your message using the toolbar for In-App or branded Email delivery

@@ -312,7 +358,7 @@ export const ComposeMessageModal: React.FC = ({ }`} > - In-App (Markdown) + In-App @@ -335,12 +381,12 @@ export const ComposeMessageModal: React.FC = ({ {platform === NotificationPlatform.InApp ? ( - Saved directly as Markdown. Delivered instantly to the recipient's InApp Mailbox & Notification Bell. + Delivered instantly to the recipient's In-App Mailbox & Notification Bell. ) : ( - Composed in Markdown, transformed into Branded Executive HTML with embedded SVG logo before dispatching. + Formatted with PFM Executive Branding and embedded logo before dispatching via email. )} @@ -373,14 +419,14 @@ export const ComposeMessageModal: React.FC = ({ /> - {/* Markdown Editor / Preview Tabs */} + {/* Rich Text Editor / Branded Preview Tabs */}
- {/* Write vs Preview Tabs */} + {/* Compose vs Email Preview Tabs */}
- {/* Markdown Toolbar (Only when activeTab === 'write') */} {activeTab === 'write' ? (
- {/* Toolbar */} + {/* WYSIWYG Formatting Toolbar */}
+ @@ -443,25 +503,28 @@ export const ComposeMessageModal: React.FC = ({ @@ -470,25 +533,19 @@ export const ComposeMessageModal: React.FC = ({