UI consistency & code quality improvements

- Standardized InvoiceEditor CreateNew tab styling to match ObjectEditor design
- Fixed CodeMirror focus issues during editing by removing problematic dependencies
- Removed copy button from mindmap view for cleaner interface
- Resolved ESLint warnings in PostmanTreeTable.js with useCallback optimization
- Enhanced PDF generation with dynamic style swapping for better print output
- Updated commits.json with latest changes
This commit is contained in:
dwindown
2025-09-28 23:30:44 +07:00
parent 78570f04f0
commit 68db19e076
13 changed files with 789 additions and 156 deletions

View File

@@ -0,0 +1,152 @@
import React, { useRef, useEffect, useState } from 'react';
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { basicSetup } from 'codemirror';
import { json } from '@codemirror/lang-json';
import { oneDark } from '@codemirror/theme-one-dark';
import { Maximize2, Minimize2 } from 'lucide-react';
const CodeMirrorEditor = ({
value,
onChange,
placeholder = '',
className = '',
language = 'json',
maxLines = 12,
showToggle = true
}) => {
const editorRef = useRef(null);
const viewRef = useRef(null);
const [isExpanded, setIsExpanded] = useState(false);
const [isDark, setIsDark] = useState(false);
// Check for dark mode
useEffect(() => {
const checkDarkMode = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkDarkMode();
// Watch for dark mode changes
const observer = new MutationObserver(checkDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
// Initialize editor only once
useEffect(() => {
if (!editorRef.current || viewRef.current) return;
const extensions = [
basicSetup,
language === 'json' ? json() : [],
EditorView.theme({
'&': {
fontSize: '14px',
width: '100%',
},
'.cm-content': {
padding: '12px',
},
'.cm-focused': {
outline: 'none',
},
'.cm-editor': {
borderRadius: '6px',
}
}),
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
onChange(update.state.doc.toString());
}
}),
...(isDark ? [oneDark] : [])
].filter(Boolean);
const state = EditorState.create({
doc: value || '',
extensions
});
const view = new EditorView({
state,
parent: editorRef.current
});
viewRef.current = view;
return () => {
if (viewRef.current) {
viewRef.current.destroy();
viewRef.current = null;
}
};
}, [isDark]); // Only recreate on theme change
// eslint-disable-next-line react-hooks/exhaustive-deps
// Handle height changes without recreating editor
useEffect(() => {
if (!viewRef.current) return;
const editorElement = editorRef.current?.querySelector('.cm-editor');
if (editorElement) {
if (isExpanded) {
editorElement.style.height = 'auto';
editorElement.style.maxHeight = 'none';
} else {
editorElement.style.height = '350px';
editorElement.style.maxHeight = '350px';
}
}
}, [isExpanded]);
// Update content when value changes externally
useEffect(() => {
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
const transaction = viewRef.current.state.update({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value || ''
}
});
viewRef.current.dispatch(transaction);
}
}, [value]);
return (
<div className={`relative ${className}`}>
<div
ref={editorRef}
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md ${
isDark ? 'bg-gray-900' : 'bg-white'
} ${isExpanded ? 'h-auto' : 'h-[350px]'}`}
/>
{showToggle && (
<button
onClick={() => {
setIsExpanded(!isExpanded);
setTimeout(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
}, 50);
}}
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</button>
)}
</div>
);
};
export default CodeMirrorEditor;

View File

@@ -20,8 +20,6 @@ import {
ToggleLeft,
FileText,
Zap,
Copy,
Check,
Eye,
Code,
Maximize,
@@ -35,27 +33,10 @@ import {
// Custom node component for different data types
const CustomNode = ({ data, selected }) => {
const [renderHtml, setRenderHtml] = React.useState(true);
const [isCopied, setIsCopied] = React.useState(false);
// Check if value contains HTML
const isHtmlContent = data.value && typeof data.value === 'string' &&
(data.value.includes('<') && data.value.includes('>'));
// Copy value to clipboard
const copyValue = async () => {
if (data.value) {
try {
await navigator.clipboard.writeText(String(data.value));
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2500);
} catch (err) {
console.error('Failed to copy:', err);
// Still show feedback even if copy failed
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2500);
}
}
};
const getIcon = () => {
switch (data.type) {
case 'object':
@@ -169,27 +150,6 @@ const CustomNode = ({ data, selected }) => {
)}
</div>
{/* Actions - Third flex item */}
<div className="flex-shrink-0 flex flex-col items-center space-y-1">
{data.value && (
<button
onClick={copyValue}
className={`rounded-full p-1 opacity-80 hover:opacity-100 transition-all shadow-md ${
isCopied
? 'bg-green-500 hover:bg-green-600'
: 'bg-blue-500 hover:bg-blue-600'
} text-white`}
title={isCopied ? "Copied!" : "Copy value to clipboard"}
>
{isCopied ? (
<Check className="h-2.5 w-2.5" />
) : (
<Copy className="h-2.5 w-2.5" />
)}
</button>
)}
{/* Future action buttons can be added here */}
</div>
</div>
{/* Output handle (right side) */}

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { ChevronRight, ChevronDown, Copy, Search, Filter } from 'lucide-react';
import CopyButton from './CopyButton';
@@ -8,7 +8,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
const [filterType, setFilterType] = useState('all');
// Flatten the data structure for table display
const flattenData = (obj, path = '', level = 0) => {
const flattenData = useCallback((obj, path = '', level = 0) => {
const result = [];
if (obj === null || obj === undefined) {
@@ -74,7 +74,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
}
return result;
};
}, [expandedPaths]);
const toggleExpanded = (path) => {
const newExpanded = new Set(expandedPaths);
@@ -86,7 +86,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
setExpandedPaths(newExpanded);
};
const flatData = useMemo(() => flattenData(data), [data, expandedPaths]);
const flatData = useMemo(() => flattenData(data), [data, flattenData]);
const filteredData = useMemo(() => {
return flatData.filter(item => {
@@ -129,7 +129,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
};
const getKeyDisplay = (key, level) => {
const parts = key.split(/[.\[\]]+/).filter(Boolean);
const parts = key.split(/[.[\]]+/).filter(Boolean);
return parts[parts.length - 1] || key;
};

View File

@@ -1,5 +1,6 @@
import React from 'react';
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
// Get the chosen color scheme or default to golden
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
@@ -21,7 +22,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
fontSize: '13px',
lineHeight: '1.4',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000'
color: '#000000',
position: 'relative'
}}>
{/* Header Section - 2 Column Layout */}
<div style={{
@@ -118,7 +120,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
{/* FROM Section */}
{(invoiceData.settings?.showFromSection ?? true) && (
<div style={{
<div className={'invoice-from-to-card'} style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
@@ -147,7 +149,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
)}
{/* TO Section */}
<div style={{
<div className={'invoice-from-to-card'} style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
@@ -167,8 +169,8 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{invoiceData.client.name || 'Acme Corporation'}
</div>
<div>{invoiceData.client.address || '456 Business Ave'}</div>
<div>{invoiceData.client.city || 'New York, NY 10001'}</div>
{invoiceData.client.address && <div>{invoiceData.client.address}</div>}
{invoiceData.client.city && <div>{invoiceData.client.city}</div>}
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
</div>
@@ -312,7 +314,7 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<table className="invoice-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{
@@ -320,36 +322,40 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
}}>
<th style={{
padding: '12px 0 12px 16px',
padding: '12px 0px 12px 16px',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '48px'
}}>Item</th>
<th style={{
padding: '12px 0',
padding: '12px 0px',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '48px'
}}>Quantity</th>
<th style={{
padding: '12px 0',
padding: '12px 0px',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '48px'
}}>Unit Price</th>
<th style={{
padding: '12px 16px 12px 0',
padding: '12px 16px 12px 0px',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '48px'
}}>Total</th>
</tr>
</thead>
@@ -360,38 +366,42 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
borderBottom: '1px solid #E5E5E5'
}}>
<td style={{
padding: '16px 0 16px 16px',
padding: '16px 0px 16px 16px',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '52px'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0',
padding: '16px 0px',
textAlign: 'center',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '52px'
}}>
{item.quantity}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
padding: '16px 0px',
textAlign: 'right',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '52px'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 16px 16px 0',
padding: '16px 16px 16px 0px',
textAlign: 'right',
fontSize: '14px',
color: '#000000',
fontWeight: 'bold',
verticalAlign: 'middle'
verticalAlign: 'middle',
height: '52px'
}}>
{formatCurrency(item.amount, true)}
</td>
@@ -439,7 +449,11 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
{invoiceData.paymentMethod.bankDetails?.iban && (
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
)}
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
{invoiceData.dueDate && (
<div>
Pay by: {invoiceData.dueDate}
</div>
)}
</div>
)}
@@ -484,17 +498,73 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
</div>
{invoiceData.dueDate && <div>Pay by: {invoiceData.dueDate}</div>}
{invoiceData.dueDate && (
<div>
Pay by: {invoiceData.dueDate}
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
<div style={{ color: '#22c55e', fontWeight: 'bold', marginTop: '4px' }}>
Paid at: {invoiceData.settings.paymentDate}
</div>
)}
</div>
)}
</div>
)}
{/* Payment Status Stamp */}
{invoiceData.settings?.paymentStatus && (
<div style={{
marginTop: '20px',
textAlign: 'center'
}}>
<div className={"invoice-payment-status-stamp"} style={{
display: 'inline-block',
transform: 'rotate(-15deg)',
border: `3px solid ${
invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444'
}`,
borderRadius: '8px',
padding: '8px 16px',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
fontSize: '24px',
fontWeight: 'bold',
color: invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444',
textAlign: 'center',
letterSpacing: '2px'
}}>
{invoiceData.settings.paymentStatus}
</div>
{/* Payment Date for PAID status */}
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
<div style={{
marginTop: '10px',
fontSize: '12px',
color: '#22c55e',
fontWeight: 'bold'
}}>
Paid at: {invoiceData.settings.paymentDate}
</div>
)}
</div>
)}
</div>
)}
{/* Totals */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
<div style={{ textAlign: 'right', minWidth: '300px', width: '100%', maxWidth: '400px' }}>
<div className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{formatCurrency(invoiceData.subtotal, true)}
</span>
</div>
@@ -502,11 +572,18 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
{/* Dynamic Fees */}
{invoiceData.fees && invoiceData.fees.map((fee) => (
<div key={fee.id} style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
<div key={fee.id} className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
</span>
</div>
@@ -514,40 +591,38 @@ const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
{/* Dynamic Discounts */}
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
<div key={discount.id} style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
<div key={discount.id} className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
</span>
</div>
))}
{/* Legacy Discount */}
{invoiceData.discount > 0 && (
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Discount</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
-{formatCurrency(invoiceData.discount, true)}
</span>
</div>
)}
<div style={{
padding: '12px 16px 12px 16px', // Match table head padding
marginTop: '16px',
<div className="invoice-total-final" style={{
padding: '16px',
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
borderTop: `2px solid ${accentColor}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
alignItems: 'center',
minHeight: '56px'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor }}>Total</span>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor, lineHeight: '1.4' }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: accentColor
color: accentColor,
lineHeight: '1.4'
}}>
{formatCurrency(invoiceData.total, true)}
</span>