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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user