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
+33 -29
View File
@@ -7,6 +7,7 @@ import { useLanguage } from '../i18n/LanguageContext'
import { api, PLATFORMS } from '../utils/api'
import PlatformIcon from '../components/PlatformIcon'
import StatusBadge from '../components/StatusBadge'
import PortalSelect from '../components/PortalSelect'
import CommentsSection from '../components/CommentsSection'
import TranslationDetailPanel from '../components/TranslationDetailPanel'
import ArtefactDetailPanel from '../components/ArtefactDetailPanel'
@@ -285,42 +286,45 @@ export default function PostDetail() {
</div>
<div className="flex items-center gap-2 flex-wrap">
<select
<PortalSelect
value={status}
onChange={e => setStatus(e.target.value)}
className="text-xs border border-border rounded-lg px-2.5 py-1.5 bg-surface text-text-secondary focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
>
{STATUS_OPTS.map(s => (
<option key={s} value={s}>{t(`posts.status.${s}`)}</option>
))}
</select>
onChange={val => setStatus(val)}
options={STATUS_OPTS.map(s => ({ value: s, label: t(`posts.status.${s}`) }))}
className="text-xs"
/>
<select
<PortalSelect
value={brandId}
onChange={e => setBrandId(e.target.value)}
className="text-xs border border-border rounded-lg px-2.5 py-1.5 bg-surface text-text-secondary focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
>
<option value="">{t('posts.selectBrand')}</option>
{brands.map(b => <option key={b._id} value={b._id}>{lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
</select>
onChange={val => setBrandId(val)}
options={[
{ value: '', label: t('posts.selectBrand') },
...brands.map(b => ({ value: String(b._id), label: lang === 'ar' && b.name_ar ? b.name_ar : b.name }))
]}
placeholder={t('posts.selectBrand')}
className="text-xs"
/>
<select
<PortalSelect
value={campaignId}
onChange={e => setCampaignId(e.target.value)}
className="text-xs border border-border rounded-lg px-2.5 py-1.5 bg-surface text-text-secondary focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
>
<option value="">{t('posts.noCampaign')}</option>
{campaigns.map(c => <option key={c._id || c.id} value={c._id || c.id}>{c.name}</option>)}
</select>
onChange={val => setCampaignId(val)}
options={[
{ value: '', label: t('posts.noCampaign') },
...campaigns.map(c => ({ value: String(c._id || c.id), label: c.name }))
]}
placeholder={t('posts.noCampaign')}
className="text-xs"
/>
<select
<PortalSelect
value={assignedTo}
onChange={e => setAssignedTo(e.target.value)}
className="text-xs border border-border rounded-lg px-2.5 py-1.5 bg-surface text-text-secondary focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
>
<option value="">{t('common.unassigned')}</option>
{teamMembers.map(m => <option key={m._id} value={String(m._id)}>{m.name}</option>)}
</select>
onChange={val => setAssignedTo(val)}
options={[
{ value: '', label: t('common.unassigned') },
...teamMembers.map(m => ({ value: String(m._id), label: m.name }))
]}
placeholder={t('common.unassigned')}
className="text-xs"
/>
</div>
{/* Platforms */}