changes made to fix the VAPT testing
This commit is contained in:
parent
c97053e0e3
commit
81565d294b
78
index.html
78
index.html
@ -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>
|
||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/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"
|
||||||
|
|||||||
@ -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(/ /g, ' ')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
42
src/styles/base-layout.css
Normal file
42
src/styles/base-layout.css
Normal 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;
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user