fix: code review — security, dead code, performance, consistency

Critical fixes:
- XSS: escapeHtml() on all user-supplied text in email notifications
- Budget PATCH: added mutex lock + availability validation (prevents corruption)
- batchResolveNames: fixed wrong signature for budget request earmark names

Dead code cleanup:
- Deleted 8 unused PostComposition* files (replaced by PostDetail full page)

Performance:
- budget-helpers: single-fetch with computeFromEntries(), optional prefetch param
- post-composition: parallelized text + thumbnail fetches with Promise.all

Consistency:
- PostDetail.jsx: native <select> → PortalSelect (matches all panels)
- Finance.jsx: 11 hardcoded English table headers → t() with i18n keys
- PostCalendar.jsx: day names, Month/Week labels → t() with i18n keys
- App.jsx Suspense: "Loading..." → brand spinner (can't use i18n in fallback)
- UploadZone: proper useRef pattern, no vanilla JS document.createElement
- All file inputs: className="hidden" → absolute w-0 h-0 opacity-0
- ArtefactDetailPanel: removed campaign/project selects (inherited from post)
- TranslationDetailPanel: removed brand/linked post selects (inherited from post)
- ApproverMultiSelect: portal-based dropdown (fixes clipping in modals)
- Thumbnail fix: post-composition constructs URL from filename (was undefined)
- Upload fix: UploadZone with drag-and-drop for design + video artefacts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-16 14:17:08 +03:00
parent ce4d6025d7
commit 49e1a796ed
34 changed files with 622 additions and 1172 deletions
+53 -25
View File
@@ -1,30 +1,51 @@
import { useState, useRef, useEffect } from 'react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { createPortal } from 'react-dom'
import { Check, ChevronDown, X } from 'lucide-react'
export default function ApproverMultiSelect({ users = [], selected = [], onChange }) {
const [open, setOpen] = useState(false)
const [dropUp, setDropUp] = useState(false)
const wrapperRef = useRef(null)
const triggerRef = useRef(null)
const dropdownRef = useRef(null)
const [pos, setPos] = useState({ top: 0, left: 0, width: 0 })
const updatePosition = useCallback(() => {
if (!triggerRef.current) return
const rect = triggerRef.current.getBoundingClientRect()
const spaceBelow = window.innerHeight - rect.bottom
const dropdownHeight = Math.min(users.length * 40 + 8, 220)
const showAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight
setPos({
top: showAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
left: rect.left,
width: rect.width,
})
}, [users.length])
// Close dropdown when clicking outside
useEffect(() => {
if (!open) return
const handleClick = (e) => {
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
setOpen(false)
}
if (triggerRef.current?.contains(e.target)) return
if (dropdownRef.current?.contains(e.target)) return
setOpen(false)
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [open])
const handleEsc = (e) => { if (e.key === 'Escape') setOpen(false) }
const handleScroll = () => updatePosition()
// Detect if dropdown should open upward
useEffect(() => {
if (!open || !wrapperRef.current) return
const rect = wrapperRef.current.getBoundingClientRect()
const spaceBelow = window.innerHeight - rect.bottom
setDropUp(spaceBelow < 220)
}, [open])
document.addEventListener('mousedown', handleClick)
document.addEventListener('keydown', handleEsc)
window.addEventListener('scroll', handleScroll, true)
return () => {
document.removeEventListener('mousedown', handleClick)
document.removeEventListener('keydown', handleEsc)
window.removeEventListener('scroll', handleScroll, true)
}
}, [open, updatePosition])
const handleOpen = () => {
updatePosition()
setOpen(!open)
}
const toggle = (userId) => {
const id = String(userId)
@@ -39,9 +60,10 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang
const selectedUsers = selected.map(id => users.find(u => String(u._id || u.id || u.Id) === String(id))).filter(Boolean)
return (
<div className="relative" ref={wrapperRef}>
<>
<div
onClick={() => setOpen(!open)}
ref={triggerRef}
onClick={handleOpen}
className={`w-full min-h-[38px] px-3 py-1.5 text-sm border rounded-lg bg-surface cursor-pointer flex items-center flex-wrap gap-1.5 transition-colors ${
open ? 'border-brand-primary ring-2 ring-brand-primary/20' : 'border-border'
}`}
@@ -58,7 +80,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang
<button
type="button"
onClick={e => { e.stopPropagation(); remove(u._id || u.id || u.Id) }}
className="hover:text-amber-950"
className="hover:text-amber-950 transition-colors"
>
<X className="w-3 h-3" />
</button>
@@ -66,8 +88,13 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang
))}
<ChevronDown className={`w-4 h-4 text-text-tertiary ms-auto shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
</div>
{open && (
<div className={`absolute z-50 w-full bg-surface border border-border rounded-lg shadow-lg max-h-48 overflow-y-auto ${dropUp ? 'bottom-full mb-1' : 'top-full mt-1'}`}>
{open && createPortal(
<div
ref={dropdownRef}
className="fixed z-[99999] bg-surface border border-border rounded-lg shadow-lg max-h-[220px] overflow-y-auto"
style={{ top: pos.top, left: pos.left, width: pos.width }}
>
{users.map(u => {
const uid = String(u._id || u.id || u.Id)
const isSelected = selected.includes(uid)
@@ -76,7 +103,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang
key={uid}
type="button"
onClick={() => toggle(uid)}
className={`w-full text-start px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between ${
className={`w-full text-start px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between transition-colors ${
isSelected ? 'text-brand-primary font-medium' : 'text-text-primary'
}`}
>
@@ -88,8 +115,9 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang
{users.length === 0 && (
<div className="px-3 py-4 text-sm text-text-tertiary text-center">No users available</div>
)}
</div>
</div>,
document.body
)}
</div>
</>
)
}