changes made to fix the VAPT testing

This commit is contained in:
laxmanhalaki 2026-02-07 14:57:21 +05:30
parent c97053e0e3
commit 81565d294b
11 changed files with 229 additions and 221 deletions

View File

@ -1,61 +1,23 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<!-- CSP: Allows blob URLs for file previews and cross-origin API calls during development -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
<meta name="theme-color" content="#2d4a3e" />
<title>Royal Enfield | Approval Portal</title>
<!-- Preload critical fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Ensure proper icon rendering and layout -->
<style>
/* Ensure Lucide icons render properly */
svg {
display: inline-block;
vertical-align: middle;
}
/* Fix for icon alignment in buttons */
button svg {
flex-shrink: 0;
}
/* Ensure proper text rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Fix for mobile viewport and sidebar */
@media (max-width: 768px) {
html {
overflow-x: hidden;
}
}
/* Ensure proper sidebar toggle behavior */
.sidebar-toggle {
transition: all 0.3s ease-in-out;
}
/* Fix for icon button hover states */
button:hover svg {
transform: scale(1.05);
transition: transform 0.2s ease;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description"
content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
<meta name="theme-color" content="#2d4a3e" />
<title>Royal Enfield | Approval Portal</title>
<!-- Preload critical fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -15,24 +15,24 @@ interface FormattedDescriptionProps {
export function FormattedDescription({ content, className }: FormattedDescriptionProps) { export function FormattedDescription({ content, className }: FormattedDescriptionProps) {
const processedContent = React.useMemo(() => { const processedContent = React.useMemo(() => {
if (!content) return ''; if (!content) return '';
// Wrap tables that aren't already wrapped in a scrollable container using regex // Wrap tables that aren't already wrapped in a scrollable container using regex
// Match <table> tags that aren't already inside a .table-wrapper // Match <table> tags that aren't already inside a .table-wrapper
let processed = content; let processed = content;
// Pattern to match table tags that aren't already wrapped // Pattern to match table tags that aren't already wrapped
const tablePattern = /<table[^>]*>[\s\S]*?<\/table>/gi; const tablePattern = /<table[^>]*>[\s\S]*?<\/table>/gi;
processed = processed.replace(tablePattern, (match) => { processed = processed.replace(tablePattern, (match) => {
// Check if this table is already wrapped // Check if this table is already wrapped
if (match.includes('table-wrapper')) { if (match.includes('table-wrapper')) {
return match; return match;
} }
// Wrap the table in a scrollable container // Wrap the table in a scrollable container
return `<div class="table-wrapper" style="overflow-x: auto; max-width: 100%; margin: 8px 0;">${match}</div>`; return `<div class="table-wrapper">${match}</div>`;
}); });
return processed; return processed;
}, [content]); }, [content]);

View File

@ -68,55 +68,55 @@ export function RichTextEditor({
const cleanWordHTML = React.useCallback((html: string): string => { const cleanWordHTML = React.useCallback((html: string): string => {
// Remove HTML comments (like Word style definitions) // Remove HTML comments (like Word style definitions)
html = html.replace(/<!--[\s\S]*?-->/g, ''); html = html.replace(/<!--[\s\S]*?-->/g, '');
// Remove style tags (Word CSS) // Remove style tags (Word CSS)
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ''); html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
// Remove script tags // Remove script tags
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ''); html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
// Remove meta tags // Remove meta tags
html = html.replace(/<meta[^>]*>/gi, ''); html = html.replace(/<meta[^>]*>/gi, '');
// Remove Word-specific classes and attributes // Remove Word-specific classes and attributes
html = html.replace(/\s*class="Mso[^"]*"/gi, ''); html = html.replace(/\s*class="Mso[^"]*"/gi, '');
html = html.replace(/\s*class="mso[^"]*"/gi, ''); html = html.replace(/\s*class="mso[^"]*"/gi, '');
html = html.replace(/\s*style="[^"]*mso-[^"]*"/gi, ''); html = html.replace(/\s*style="[^"]*mso-[^"]*"/gi, '');
html = html.replace(/\s*style="[^"]*font-family:[^"]*"/gi, ''); html = html.replace(/\s*style="[^"]*font-family:[^"]*"/gi, '');
// Remove xmlns attributes // Remove xmlns attributes
html = html.replace(/\s*xmlns[^=]*="[^"]*"/gi, ''); html = html.replace(/\s*xmlns[^=]*="[^"]*"/gi, '');
// Remove o:p tags (Word paragraph markers) // Remove o:p tags (Word paragraph markers)
html = html.replace(/<\/?o:p[^>]*>/gi, ''); html = html.replace(/<\/?o:p[^>]*>/gi, '');
// Remove v:shapes and other Word-specific elements // Remove v:shapes and other Word-specific elements
html = html.replace(/<v:[^>]*>[\s\S]*?<\/v:[^>]*>/gi, ''); html = html.replace(/<v:[^>]*>[\s\S]*?<\/v:[^>]*>/gi, '');
html = html.replace(/<v:[^>]*\/>/gi, ''); html = html.replace(/<v:[^>]*\/>/gi, '');
// Clean up empty paragraphs // Clean up empty paragraphs
html = html.replace(/<p[^>]*>\s*<\/p>/gi, ''); html = html.replace(/<p[^>]*>\s*<\/p>/gi, '');
html = html.replace(/<div[^>]*>\s*<\/div>/gi, ''); html = html.replace(/<div[^>]*>\s*<\/div>/gi, '');
// Remove excessive whitespace // Remove excessive whitespace
html = html.replace(/\s+/g, ' '); html = html.replace(/\s+/g, ' ');
html = html.trim(); html = html.trim();
return html; return html;
}, []); }, []);
// Handle paste event to preserve formatting // Handle paste event to preserve formatting
const handlePaste = React.useCallback((e: React.ClipboardEvent<HTMLDivElement>) => { const handlePaste = React.useCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
const clipboardData = e.clipboardData; const clipboardData = e.clipboardData;
let pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain'); let pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
// Clean Word/Office metadata if HTML // Clean Word/Office metadata if HTML
if (pastedData.includes('<!--') || pastedData.includes('<style') || pastedData.includes('MsoNormal')) { if (pastedData.includes('<!--') || pastedData.includes('<style') || pastedData.includes('MsoNormal')) {
pastedData = cleanWordHTML(pastedData); pastedData = cleanWordHTML(pastedData);
} }
if (!editorRef.current) return; if (!editorRef.current) return;
const selection = window.getSelection(); const selection = window.getSelection();
@ -131,12 +131,12 @@ export function RichTextEditor({
// Clean and preserve formatting // Clean and preserve formatting
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
// Process each node to preserve lists, tables, and basic formatting // Process each node to preserve lists, tables, and basic formatting
Array.from(tempDiv.childNodes).forEach((node) => { Array.from(tempDiv.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement; const element = node as HTMLElement;
// Preserve lists (ul, ol) // Preserve lists (ul, ol)
if (element.tagName === 'UL' || element.tagName === 'OL') { if (element.tagName === 'UL' || element.tagName === 'OL') {
const list = element.cloneNode(true) as HTMLElement; const list = element.cloneNode(true) as HTMLElement;
@ -169,9 +169,6 @@ export function RichTextEditor({
// Wrap table in scrollable container for mobile // Wrap table in scrollable container for mobile
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper'; wrapper.className = 'table-wrapper';
wrapper.style.overflowX = 'auto';
wrapper.style.maxWidth = '100%';
wrapper.style.margin = '8px 0';
wrapper.appendChild(table); wrapper.appendChild(table);
fragment.appendChild(wrapper); fragment.appendChild(wrapper);
} }
@ -182,7 +179,7 @@ export function RichTextEditor({
const innerHTML = element.innerHTML; const innerHTML = element.innerHTML;
// Remove style tags and comments from inner HTML // Remove style tags and comments from inner HTML
const cleaned = innerHTML.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') const cleaned = innerHTML.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<!--[\s\S]*?-->/g, ''); .replace(/<!--[\s\S]*?-->/g, '');
p.innerHTML = cleaned; p.innerHTML = cleaned;
p.removeAttribute('style'); p.removeAttribute('style');
p.removeAttribute('class'); p.removeAttribute('class');
@ -227,7 +224,7 @@ export function RichTextEditor({
} }
range.insertNode(fragment); range.insertNode(fragment);
// Move cursor to end of inserted content // Move cursor to end of inserted content
range.collapse(false); range.collapse(false);
selection.removeAllRanges(); selection.removeAllRanges();
@ -242,21 +239,21 @@ export function RichTextEditor({
// Check active formats (bold, italic, etc.) // Check active formats (bold, italic, etc.)
const checkActiveFormats = React.useCallback(() => { const checkActiveFormats = React.useCallback(() => {
if (!editorRef.current || !isFocused) return; if (!editorRef.current || !isFocused) return;
const formats = new Set<string>(); const formats = new Set<string>();
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer; const commonAncestor = range.commonAncestorContainer;
let element: HTMLElement | null = null; let element: HTMLElement | null = null;
if (commonAncestor.nodeType === Node.TEXT_NODE) { if (commonAncestor.nodeType === Node.TEXT_NODE) {
element = commonAncestor.parentElement; element = commonAncestor.parentElement;
} else { } else {
element = commonAncestor as HTMLElement; element = commonAncestor as HTMLElement;
} }
while (element && element !== editorRef.current) { while (element && element !== editorRef.current) {
const tagName = element.tagName.toLowerCase(); const tagName = element.tagName.toLowerCase();
if (tagName === 'strong' || tagName === 'b') formats.add('bold'); if (tagName === 'strong' || tagName === 'b') formats.add('bold');
@ -267,40 +264,40 @@ export function RichTextEditor({
if (tagName === 'h3') formats.add('h3'); if (tagName === 'h3') formats.add('h3');
if (tagName === 'ul') formats.add('ul'); if (tagName === 'ul') formats.add('ul');
if (tagName === 'ol') formats.add('ol'); if (tagName === 'ol') formats.add('ol');
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
if (style.textAlign === 'center') formats.add('center'); if (style.textAlign === 'center') formats.add('center');
if (style.textAlign === 'right') formats.add('right'); if (style.textAlign === 'right') formats.add('right');
if (style.textAlign === 'left') formats.add('left'); if (style.textAlign === 'left') formats.add('left');
// Convert RGB/RGBA to hex for comparison // Convert RGB/RGBA to hex for comparison
const colorToHex = (color: string): string | null => { const colorToHex = (color: string): string | null => {
// If already hex format // If already hex format
if (color.startsWith('#')) { if (color.startsWith('#')) {
return color.toUpperCase(); return color.toUpperCase();
} }
// If RGB/RGBA format // If RGB/RGBA format
const result = color.match(/\d+/g); const result = color.match(/\d+/g);
if (!result || result.length < 3) return null; if (!result || result.length < 3) return null;
const r = result[0]; const r = result[0];
const g = result[1]; const g = result[1];
const b = result[2]; const b = result[2];
if (!r || !g || !b) return null; if (!r || !g || !b) return null;
const rHex = parseInt(r).toString(16).padStart(2, '0'); const rHex = parseInt(r).toString(16).padStart(2, '0');
const gHex = parseInt(g).toString(16).padStart(2, '0'); const gHex = parseInt(g).toString(16).padStart(2, '0');
const bHex = parseInt(b).toString(16).padStart(2, '0'); const bHex = parseInt(b).toString(16).padStart(2, '0');
return `#${rHex}${gHex}${bHex}`.toUpperCase(); return `#${rHex}${gHex}${bHex}`.toUpperCase();
}; };
// Check for background color (highlight) // Check for background color (highlight)
const bgColor = style.backgroundColor; const bgColor = style.backgroundColor;
// Check if background color is set and not transparent/default // Check if background color is set and not transparent/default
if (bgColor && if (bgColor &&
bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'rgba(0, 0, 0, 0)' &&
bgColor !== 'transparent' && bgColor !== 'transparent' &&
bgColor !== 'rgb(255, 255, 255)' && bgColor !== 'rgb(255, 255, 255)' &&
bgColor !== '#ffffff' && bgColor !== '#ffffff' &&
bgColor !== '#FFFFFF') { bgColor !== '#FFFFFF') {
formats.add('highlight'); formats.add('highlight');
const hexColor = colorToHex(bgColor); const hexColor = colorToHex(bgColor);
if (hexColor) { if (hexColor) {
@ -321,15 +318,15 @@ export function RichTextEditor({
// Only reset if we haven't found a highlight yet // Only reset if we haven't found a highlight yet
setCurrentHighlightColor(null); setCurrentHighlightColor(null);
} }
// Check for text color // Check for text color
const textColor = style.color; const textColor = style.color;
// Convert to hex for comparison // Convert to hex for comparison
const hexTextColor = colorToHex(textColor); const hexTextColor = colorToHex(textColor);
// Check if text color is set and not default black // Check if text color is set and not default black
if (textColor && hexTextColor && if (textColor && hexTextColor &&
textColor !== 'rgba(0, 0, 0, 0)' && textColor !== 'rgba(0, 0, 0, 0)' &&
hexTextColor !== '#000000') { hexTextColor !== '#000000') {
formats.add('textColor'); formats.add('textColor');
// Find matching color from our palette // Find matching color from our palette
const matchedColor = HIGHLIGHT_COLORS.find(c => { const matchedColor = HIGHLIGHT_COLORS.find(c => {
@ -350,23 +347,23 @@ export function RichTextEditor({
setCurrentTextColor(null); setCurrentTextColor(null);
} }
} }
element = element.parentElement; element = element.parentElement;
} }
} }
setActiveFormats(formats); setActiveFormats(formats);
}, [isFocused]); }, [isFocused]);
// Apply formatting command // Apply formatting command
const applyFormat = React.useCallback((command: string, value?: string) => { const applyFormat = React.useCallback((command: string, value?: string) => {
if (!editorRef.current) return; if (!editorRef.current) return;
// Restore focus if needed // Restore focus if needed
if (!isFocused) { if (!isFocused) {
editorRef.current.focus(); editorRef.current.focus();
} }
// Save current selection // Save current selection
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
@ -374,15 +371,15 @@ export function RichTextEditor({
editorRef.current.focus(); editorRef.current.focus();
return; return;
} }
// Execute formatting command // Execute formatting command
document.execCommand(command, false, value); document.execCommand(command, false, value);
// Update content // Update content
if (editorRef.current) { if (editorRef.current) {
onChange(editorRef.current.innerHTML); onChange(editorRef.current.innerHTML);
} }
// Check active formats after a short delay // Check active formats after a short delay
setTimeout(checkActiveFormats, 10); setTimeout(checkActiveFormats, 10);
}, [isFocused, onChange, checkActiveFormats]); }, [isFocused, onChange, checkActiveFormats]);
@ -390,12 +387,12 @@ export function RichTextEditor({
// Apply highlight color // Apply highlight color
const applyHighlight = React.useCallback((color: string) => { const applyHighlight = React.useCallback((color: string) => {
if (!editorRef.current) return; if (!editorRef.current) return;
// Restore focus if needed // Restore focus if needed
if (!isFocused) { if (!isFocused) {
editorRef.current.focus(); editorRef.current.focus();
} }
// Save current selection // Save current selection
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
@ -403,26 +400,26 @@ export function RichTextEditor({
editorRef.current.focus(); editorRef.current.focus();
return; return;
} }
// Check if this color is already applied by checking the selection's style // Check if this color is already applied by checking the selection's style
let isAlreadyApplied = false; let isAlreadyApplied = false;
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer; const commonAncestor = range.commonAncestorContainer;
let element: HTMLElement | null = null; let element: HTMLElement | null = null;
if (commonAncestor.nodeType === Node.TEXT_NODE) { if (commonAncestor.nodeType === Node.TEXT_NODE) {
element = commonAncestor.parentElement; element = commonAncestor.parentElement;
} else { } else {
element = commonAncestor as HTMLElement; element = commonAncestor as HTMLElement;
} }
// Check if the selected element has the same background color // Check if the selected element has the same background color
while (element && element !== editorRef.current) { while (element && element !== editorRef.current) {
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
const bgColor = style.backgroundColor; const bgColor = style.backgroundColor;
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' && if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' &&
bgColor !== 'rgb(255, 255, 255)' && bgColor !== '#ffffff' && bgColor !== '#FFFFFF') { bgColor !== 'rgb(255, 255, 255)' && bgColor !== '#ffffff' && bgColor !== '#FFFFFF') {
// Convert to hex and compare // Convert to hex and compare
const colorToHex = (c: string): string | null => { const colorToHex = (c: string): string | null => {
if (c.startsWith('#')) return c.toUpperCase(); if (c.startsWith('#')) return c.toUpperCase();
@ -446,7 +443,7 @@ export function RichTextEditor({
element = element.parentElement; element = element.parentElement;
} }
} }
// Use backColor command for highlight (background color) // Use backColor command for highlight (background color)
if (color === 'transparent' || isAlreadyApplied) { if (color === 'transparent' || isAlreadyApplied) {
// Remove highlight - use a more aggressive approach to fully remove // Remove highlight - use a more aggressive approach to fully remove
@ -454,10 +451,10 @@ export function RichTextEditor({
if (!range.collapsed) { if (!range.collapsed) {
// Store the range before manipulation // Store the range before manipulation
const contents = range.extractContents(); const contents = range.extractContents();
// Create a new text node or span without background color // Create a new text node or span without background color
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
// Process extracted contents to remove background colors // Process extracted contents to remove background colors
const processNode = (node: Node) => { const processNode = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
@ -465,14 +462,14 @@ export function RichTextEditor({
} else if (node.nodeType === Node.ELEMENT_NODE) { } else if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as HTMLElement; const el = node as HTMLElement;
const newEl = document.createElement(el.tagName.toLowerCase()); const newEl = document.createElement(el.tagName.toLowerCase());
// Copy all attributes except style-related ones // Copy all attributes except style-related ones
Array.from(el.attributes).forEach(attr => { Array.from(el.attributes).forEach(attr => {
if (attr.name !== 'style' && attr.name !== 'class') { if (attr.name !== 'style' && attr.name !== 'class') {
newEl.setAttribute(attr.name, attr.value); newEl.setAttribute(attr.name, attr.value);
} }
}); });
// Process children and copy without background color // Process children and copy without background color
Array.from(el.childNodes).forEach(child => { Array.from(el.childNodes).forEach(child => {
const processed = processNode(child); const processed = processNode(child);
@ -480,27 +477,27 @@ export function RichTextEditor({
newEl.appendChild(processed); newEl.appendChild(processed);
} }
}); });
// Remove background color if present // Remove background color if present
if (el.style.backgroundColor) { if (el.style.backgroundColor) {
newEl.style.backgroundColor = ''; newEl.style.backgroundColor = '';
} }
return newEl; return newEl;
} }
return null; return null;
}; };
Array.from(contents.childNodes).forEach(child => { Array.from(contents.childNodes).forEach(child => {
const processed = processNode(child); const processed = processNode(child);
if (processed) { if (processed) {
fragment.appendChild(processed); fragment.appendChild(processed);
} }
}); });
// Insert the cleaned fragment // Insert the cleaned fragment
range.insertNode(fragment); range.insertNode(fragment);
// Also use execCommand to ensure removal // Also use execCommand to ensure removal
document.execCommand('removeFormat', false); document.execCommand('removeFormat', false);
} else { } else {
@ -523,21 +520,21 @@ export function RichTextEditor({
return; return;
} }
} }
// Clear selection immediately after applying to prevent "sticky" highlight mode // Clear selection immediately after applying to prevent "sticky" highlight mode
const sel = window.getSelection(); const sel = window.getSelection();
if (sel) { if (sel) {
sel.removeAllRanges(); sel.removeAllRanges();
} }
// Update content // Update content
if (editorRef.current) { if (editorRef.current) {
onChange(editorRef.current.innerHTML); onChange(editorRef.current.innerHTML);
} }
// Close popover // Close popover
setHighlightColorOpen(false); setHighlightColorOpen(false);
// Refocus editor after a short delay and check formats // Refocus editor after a short delay and check formats
setTimeout(() => { setTimeout(() => {
if (editorRef.current) { if (editorRef.current) {
@ -550,12 +547,12 @@ export function RichTextEditor({
// Apply text color // Apply text color
const applyTextColor = React.useCallback((color: string) => { const applyTextColor = React.useCallback((color: string) => {
if (!editorRef.current) return; if (!editorRef.current) return;
// Restore focus if needed // Restore focus if needed
if (!isFocused) { if (!isFocused) {
editorRef.current.focus(); editorRef.current.focus();
} }
// Save current selection // Save current selection
const selection = window.getSelection(); const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
@ -563,20 +560,20 @@ export function RichTextEditor({
editorRef.current.focus(); editorRef.current.focus();
return; return;
} }
// Check if this color is already applied by checking the selection's style // Check if this color is already applied by checking the selection's style
let isAlreadyApplied = false; let isAlreadyApplied = false;
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer; const commonAncestor = range.commonAncestorContainer;
let element: HTMLElement | null = null; let element: HTMLElement | null = null;
if (commonAncestor.nodeType === Node.TEXT_NODE) { if (commonAncestor.nodeType === Node.TEXT_NODE) {
element = commonAncestor.parentElement; element = commonAncestor.parentElement;
} else { } else {
element = commonAncestor as HTMLElement; element = commonAncestor as HTMLElement;
} }
// Check if the selected element has the same text color // Check if the selected element has the same text color
while (element && element !== editorRef.current) { while (element && element !== editorRef.current) {
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
@ -612,7 +609,7 @@ export function RichTextEditor({
element = element.parentElement; element = element.parentElement;
} }
} }
// Use foreColor command for text color // Use foreColor command for text color
if (color === 'transparent' || color === 'default' || isAlreadyApplied) { if (color === 'transparent' || color === 'default' || isAlreadyApplied) {
// Remove text color by removing format or setting to default // Remove text color by removing format or setting to default
@ -633,15 +630,15 @@ export function RichTextEditor({
setCustomTextColor(color); setCustomTextColor(color);
} }
} }
// Update content // Update content
if (editorRef.current) { if (editorRef.current) {
onChange(editorRef.current.innerHTML); onChange(editorRef.current.innerHTML);
} }
// Close popover // Close popover
setTextColorOpen(false); setTextColorOpen(false);
// Check active formats after a short delay // Check active formats after a short delay
setTimeout(checkActiveFormats, 10); setTimeout(checkActiveFormats, 10);
}, [isFocused, onChange, checkActiveFormats]); }, [isFocused, onChange, checkActiveFormats]);
@ -692,11 +689,11 @@ export function RichTextEditor({
// Handle selection change to update active formats // Handle selection change to update active formats
React.useEffect(() => { React.useEffect(() => {
if (!isFocused) return; if (!isFocused) return;
const handleSelectionChange = () => { const handleSelectionChange = () => {
checkActiveFormats(); checkActiveFormats();
}; };
document.addEventListener('selectionchange', handleSelectionChange); document.addEventListener('selectionchange', handleSelectionChange);
return () => { return () => {
document.removeEventListener('selectionchange', handleSelectionChange); document.removeEventListener('selectionchange', handleSelectionChange);
@ -748,7 +745,7 @@ export function RichTextEditor({
> >
<Underline className="h-4 w-4" /> <Underline className="h-4 w-4" />
</Button> </Button>
{/* Highlight Color Picker */} {/* Highlight Color Picker */}
<Popover open={highlightColorOpen} onOpenChange={setHighlightColorOpen}> <Popover open={highlightColorOpen} onOpenChange={setHighlightColorOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -765,8 +762,8 @@ export function RichTextEditor({
<Highlighter className="h-4 w-4" /> <Highlighter className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-auto p-2" className="w-auto p-2"
align="start" align="start"
onPointerDownOutside={(e) => { onPointerDownOutside={(e) => {
// Prevent closing when clicking inside popover // Prevent closing when clicking inside popover
@ -791,7 +788,7 @@ export function RichTextEditor({
> >
<X className="h-4 w-4 text-gray-500" /> <X className="h-4 w-4 text-gray-500" />
</Button> </Button>
<div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Highlight Color</div> <div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Highlight Color</div>
<div className="grid grid-cols-6 gap-1.5 pr-1 mb-2"> <div className="grid grid-cols-6 gap-1.5 pr-1 mb-2">
{HIGHLIGHT_COLORS.map((color) => { {HIGHLIGHT_COLORS.map((color) => {
@ -833,7 +830,7 @@ export function RichTextEditor({
); );
})} })}
</div> </div>
{/* Remove Highlight Button - Standard pattern */} {/* Remove Highlight Button - Standard pattern */}
{currentHighlightColor && currentHighlightColor !== 'transparent' && ( {currentHighlightColor && currentHighlightColor !== 'transparent' && (
<div className="mb-2"> <div className="mb-2">
@ -852,7 +849,7 @@ export function RichTextEditor({
</Button> </Button>
</div> </div>
)} )}
{/* Custom Color Picker */} {/* Custom Color Picker */}
<div className="border-t border-gray-200 pt-2 mt-2"> <div className="border-t border-gray-200 pt-2 mt-2">
<div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div> <div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div>
@ -899,7 +896,7 @@ export function RichTextEditor({
// Get pasted text from clipboard // Get pasted text from clipboard
const pastedText = e.clipboardData.getData('text').trim(); const pastedText = e.clipboardData.getData('text').trim();
e.preventDefault(); e.preventDefault();
// Process after paste event completes // Process after paste event completes
setTimeout(() => { setTimeout(() => {
// Check if it's a valid hex color with # // Check if it's a valid hex color with #
@ -980,7 +977,7 @@ export function RichTextEditor({
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{/* Text Color Picker */} {/* Text Color Picker */}
<Popover open={textColorOpen} onOpenChange={setTextColorOpen}> <Popover open={textColorOpen} onOpenChange={setTextColorOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -997,8 +994,8 @@ export function RichTextEditor({
<Type className="h-4 w-4" /> <Type className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="w-auto p-2" className="w-auto p-2"
align="start" align="start"
onPointerDownOutside={(e) => { onPointerDownOutside={(e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
@ -1022,7 +1019,7 @@ export function RichTextEditor({
> >
<X className="h-4 w-4 text-gray-500" /> <X className="h-4 w-4 text-gray-500" />
</Button> </Button>
<div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Text Color</div> <div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Text Color</div>
<div className="grid grid-cols-6 gap-1.5 pr-1 mb-2"> <div className="grid grid-cols-6 gap-1.5 pr-1 mb-2">
{/* Default/Black Color Option - First position (standard) */} {/* Default/Black Color Option - First position (standard) */}
@ -1085,7 +1082,7 @@ export function RichTextEditor({
); );
})} })}
</div> </div>
{/* Remove Text Color Button - Standard pattern */} {/* Remove Text Color Button - Standard pattern */}
{currentTextColor && currentTextColor !== '#000000' && ( {currentTextColor && currentTextColor !== '#000000' && (
<div className="mb-2"> <div className="mb-2">
@ -1104,7 +1101,7 @@ export function RichTextEditor({
</Button> </Button>
</div> </div>
)} )}
{/* Custom Text Color Picker */} {/* Custom Text Color Picker */}
<div className="border-t border-gray-200 pt-2 mt-2"> <div className="border-t border-gray-200 pt-2 mt-2">
<div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div> <div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div>

View File

@ -5,6 +5,7 @@ import { AuthProvider } from './contexts/AuthContext';
import { AuthenticatedApp } from './pages/Auth'; import { AuthenticatedApp } from './pages/Auth';
import { store } from './redux/store'; import { store } from './redux/store';
import './styles/globals.css'; import './styles/globals.css';
import './styles/base-layout.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>

View File

@ -33,8 +33,7 @@ export function CriticalAlertsSection({
}: CriticalAlertsSectionProps) { }: CriticalAlertsSectionProps) {
return ( return (
<Card <Card
className="lg:col-span-2 shadow-md hover:shadow-lg transition-shadow flex flex-col overflow-hidden" className="lg:col-span-2 shadow-md hover:shadow-lg transition-shadow flex flex-col overflow-hidden h-full"
style={{ height: '100%' }}
data-testid="critical-alerts-section" data-testid="critical-alerts-section"
> >
<CardHeader className="pb-3 sm:pb-4 flex-shrink-0"> <CardHeader className="pb-3 sm:pb-4 flex-shrink-0">
@ -60,8 +59,7 @@ export function CriticalAlertsSection({
</div> </div>
</CardHeader> </CardHeader>
<CardContent <CardContent
className="overflow-y-auto flex-1 p-4" className={`overflow-y-auto flex-1 p-4 ${pagination.totalPages > 1 ? 'max-h-[calc(100%-140px)]' : 'max-h-[calc(100%-80px)]'}`}
style={{ maxHeight: pagination.totalPages > 1 ? 'calc(100% - 140px)' : 'calc(100% - 80px)' }}
> >
<div className="space-y-3 sm:space-y-4"> <div className="space-y-3 sm:space-y-4">
{breachedRequests.length === 0 ? ( {breachedRequests.length === 0 ? (

View File

@ -84,11 +84,7 @@ export function PriorityDistributionReport({
fill="#1f2937" fill="#1f2937"
textAnchor={x > cx ? 'start' : 'end'} textAnchor={x > cx ? 'start' : 'end'}
dominantBaseline="central" dominantBaseline="central"
style={{ className="text-sm font-semibold pointer-events-none"
fontSize: '14px',
fontWeight: '600',
pointerEvents: 'none',
}}
> >
{`${name}: ${percentage}%`} {`${name}: ${percentage}%`}
</text> </text>
@ -102,13 +98,13 @@ export function PriorityDistributionReport({
onNavigate(`requests?priority=${data.priority}`); onNavigate(`requests?priority=${data.priority}`);
} }
}} }}
style={{ cursor: 'pointer' }} className="cursor-pointer"
> >
{priorityDistribution.map((priority, index) => ( {priorityDistribution.map((priority, index) => (
<Cell <Cell
key={`cell-${index}`} key={`cell-${index}`}
fill={priority.priority === 'express' ? '#ef4444' : '#3b82f6'} fill={priority.priority === 'express' ? '#ef4444' : '#3b82f6'}
style={{ cursor: 'pointer' }} className="cursor-pointer"
/> />
))} ))}
</Pie> </Pie>

View File

@ -40,8 +40,7 @@ export function RecentActivitySection({
}: RecentActivitySectionProps) { }: RecentActivitySectionProps) {
return ( return (
<Card <Card
className="lg:col-span-1 shadow-md hover:shadow-lg transition-shadow flex flex-col overflow-hidden" className="lg:col-span-1 shadow-md hover:shadow-lg transition-shadow flex flex-col overflow-hidden h-full"
style={{ height: '100%' }}
data-testid="recent-activity-section" data-testid="recent-activity-section"
> >
<CardHeader className="pb-3 sm:pb-4 flex-shrink-0"> <CardHeader className="pb-3 sm:pb-4 flex-shrink-0">
@ -73,8 +72,7 @@ export function RecentActivitySection({
</div> </div>
</CardHeader> </CardHeader>
<CardContent <CardContent
className="overflow-y-auto flex-1 p-4" className={`overflow-y-auto flex-1 p-4 ${pagination.totalPages > 1 ? 'max-h-[calc(100%-140px)]' : 'max-h-[calc(100%-80px)]'}`}
style={{ maxHeight: pagination.totalPages > 1 ? 'calc(100% - 140px)' : 'calc(100% - 80px)' }}
> >
<div className="space-y-2 sm:space-y-3"> <div className="space-y-2 sm:space-y-3">
{recentActivity.length === 0 ? ( {recentActivity.length === 0 ? (

View File

@ -16,23 +16,29 @@ import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
*/ */
const stripHtmlTags = (html: string): string => { const stripHtmlTags = (html: string): string => {
if (!html) return ''; if (!html) return '';
// Check if we're in a browser environment // 1. Replace block-level tags with a space to avoid merging words (e.g. </div><div> -> " ")
if (typeof document === 'undefined') { // This preserves readability for the card preview
// Fallback for SSR: use regex to strip HTML tags let text = html.replace(/<(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|tfoot|ul|video)[^>]*>/gi, ' ');
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
} // 2. Replace <br> with space
text = text.replace(/<br\s*\/?>/gi, ' ');
// Create a temporary div to parse HTML
const tempDiv = document.createElement('div'); // 3. Strip all other tags
tempDiv.innerHTML = html; text = text.replace(/<[^>]*>/g, '');
// Get text content (automatically strips HTML tags) // 4. Clean up extra whitespace
let text = tempDiv.textContent || tempDiv.innerText || '';
// Clean up extra whitespace
text = text.replace(/\s+/g, ' ').trim(); text = text.replace(/\s+/g, ' ').trim();
// 5. Basic HTML entity decoding for common characters
text = text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'");
return text; return text;
}; };
@ -101,18 +107,18 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
{(() => { {(() => {
const templateType = request?.templateType || (request as any)?.template_type || ''; const templateType = request?.templateType || (request as any)?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || ''; const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType // Direct mapping from templateType
let templateLabel = 'Non-Templatized'; let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200'; let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') { if (templateTypeUpper === 'DEALER CLAIM') {
templateLabel = 'Dealer Claim'; templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200'; templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') { } else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template'; templateLabel = 'Template';
} }
return ( return (
<Badge <Badge
variant="outline" variant="outline"

View File

@ -16,22 +16,28 @@ import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
const stripHtmlTags = (html: string): string => { const stripHtmlTags = (html: string): string => {
if (!html) return ''; if (!html) return '';
// Check if we're in a browser environment // 1. Replace block-level tags with a space to avoid merging words (e.g. </div><div> -> " ")
if (typeof document === 'undefined') { // This preserves readability for the card preview
// Fallback for SSR: use regex to strip HTML tags let text = html.replace(/<(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|tfoot|ul|video)[^>]*>/gi, ' ');
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
}
// Create a temporary div to parse HTML // 2. Replace <br> with space
const tempDiv = document.createElement('div'); text = text.replace(/<br\s*\/?>/gi, ' ');
tempDiv.innerHTML = html;
// Get text content (automatically strips HTML tags) // 3. Strip all other tags
let text = tempDiv.textContent || tempDiv.innerText || ''; text = text.replace(/<[^>]*>/g, '');
// Clean up extra whitespace // 4. Clean up extra whitespace
text = text.replace(/\s+/g, ' ').trim(); text = text.replace(/\s+/g, ' ').trim();
// 5. Basic HTML entity decoding for common characters
text = text
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'");
return text; return text;
}; };

View File

@ -0,0 +1,42 @@
/* Ensure Lucide icons render properly */
svg {
display: inline-block;
vertical-align: middle;
}
/* Fix for icon alignment in buttons */
button svg {
flex-shrink: 0;
}
/* Ensure proper text rendering */
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Fix for mobile viewport and sidebar */
@media (max-width: 768px) {
html {
overflow-x: hidden;
}
}
/* Ensure proper sidebar toggle behavior */
.sidebar-toggle {
transition: all 0.3s ease-in-out;
}
/* Fix for icon button hover states */
button:hover svg {
transform: scale(1.05);
transition: transform 0.2s ease;
}
/* Table wrapper for CSP-compliant horizontal scrolling */
.table-wrapper {
overflow-x: auto;
max-width: 100%;
margin: 8px 0;
}

View File

@ -43,7 +43,7 @@ const ensureChunkOrder = () => {
const reactChunk = Object.keys(bundle).find( const reactChunk = Object.keys(bundle).find(
(key) => bundle[key].type === 'chunk' && bundle[key].name === 'react-vendor' (key) => bundle[key].type === 'chunk' && bundle[key].name === 'react-vendor'
); );
if (reactChunk) { if (reactChunk) {
// Ensure Radix vendor chunk depends on React vendor chunk // Ensure Radix vendor chunk depends on React vendor chunk
Object.keys(bundle).forEach((key) => { Object.keys(bundle).forEach((key) => {
@ -75,8 +75,6 @@ export default defineConfig({
server: { server: {
port: 3000, port: 3000,
open: true, open: true,
host: true,
allowedHosts: ['9b89f4bfd360.ngrok-free.app','c6ba819712b5.ngrok-free.app'],
}, },
build: { build: {
outDir: 'dist', outDir: 'dist',
@ -126,10 +124,10 @@ export default defineConfig({
// Radix UI components try to access React before it's initialized // Radix UI components try to access React before it's initialized
// Option 1: Don't split React - keep it in main bundle (most reliable) // Option 1: Don't split React - keep it in main bundle (most reliable)
// Option 2: Keep React in separate chunk but ensure it loads first // Option 2: Keep React in separate chunk but ensure it loads first
// For now, let's keep React in main bundle to avoid initialization issues // For now, let's keep React in main bundle to avoid initialization issues
// Only split other vendors // Only split other vendors
// Radix UI - CRITICAL: ALL Radix packages MUST stay together in ONE chunk // Radix UI - CRITICAL: ALL Radix packages MUST stay together in ONE chunk
// This chunk will import React from the main bundle, avoiding initialization issues // This chunk will import React from the main bundle, avoiding initialization issues
if (id.includes('node_modules/@radix-ui')) { if (id.includes('node_modules/@radix-ui')) {
@ -173,6 +171,10 @@ export default defineConfig({
}, },
chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks
}, },
esbuild: {
// CRITICAL: Strip all legal comments to prevent "Suspicious Comments" alerts (e.g. from Redux docs)
legalComments: 'none',
},
optimizeDeps: { optimizeDeps: {
include: [ include: [
'react', 'react',