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:
@@ -1,11 +1,13 @@
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { Copy, Eye, Lock, Send, Upload, FileText, Trash2, Check, Clock, CheckCircle2, XCircle, FileEdit, Wrench, MessageSquare, Paperclip } from 'lucide-react'
|
||||
import { Copy, Eye, Lock, Send, FileText, Trash2, Check, Clock, CheckCircle2, XCircle, FileEdit, Wrench, MessageSquare, Paperclip } from 'lucide-react'
|
||||
import UploadZone from './UploadZone'
|
||||
import { api, STATUS_CONFIG, PRIORITY_CONFIG } from '../utils/api'
|
||||
import TabbedModal from './TabbedModal'
|
||||
import Modal from './Modal'
|
||||
import { useToast } from './ToastContainer'
|
||||
import { AppContext } from '../App'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
import PortalSelect from './PortalSelect'
|
||||
|
||||
export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers, teams = [] }) {
|
||||
const { brands } = useContext(AppContext)
|
||||
@@ -284,67 +286,53 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
{/* Assigned To */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-text-primary mb-2">{t('issues.assignedTo')}</label>
|
||||
<select
|
||||
<PortalSelect
|
||||
value={assignedTo}
|
||||
onChange={(e) => handleAssignmentChange(e.target.value)}
|
||||
onChange={val => handleAssignmentChange(val)}
|
||||
options={[{ value: '', label: t('issues.unassigned') }, ...teamMembers.map(member => ({ value: member.id || member._id, label: member.name }))]}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('issues.unassigned')}</option>
|
||||
{teamMembers.map((member) => (
|
||||
<option key={member.id || member._id} value={member.id || member._id}>
|
||||
{member.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Team */}
|
||||
{teams.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-text-primary mb-2">{t('issues.team')}</label>
|
||||
<select
|
||||
<PortalSelect
|
||||
value={teamId}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.value || null
|
||||
setTeamId(val || '')
|
||||
onChange={async (val) => {
|
||||
const resolvedVal = val || null
|
||||
setTeamId(resolvedVal || '')
|
||||
try {
|
||||
await api.patch(`/issues/${issueId}`, { team_id: val })
|
||||
await api.patch(`/issues/${issueId}`, { team_id: resolvedVal })
|
||||
await onUpdate()
|
||||
await loadIssueDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to update team:', err)
|
||||
}
|
||||
}}
|
||||
options={[{ value: '', label: t('issues.allTeams') }, ...teams.map(team => ({ value: team.id || team._id, label: team.name }))]}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('issues.allTeams')}</option>
|
||||
{teams.map((team) => (
|
||||
<option key={team.id || team._id} value={team.id || team._id}>{team.name}</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-text-primary mb-2">{t('issues.brandLabel')}</label>
|
||||
<select
|
||||
<PortalSelect
|
||||
value={issueData.brand_id || ''}
|
||||
onChange={async (e) => {
|
||||
const val = e.target.value || null;
|
||||
onChange={async (val) => {
|
||||
const resolvedVal = val || null;
|
||||
try {
|
||||
await api.patch(`/issues/${issueId}`, { brand_id: val });
|
||||
await api.patch(`/issues/${issueId}`, { brand_id: resolvedVal });
|
||||
loadIssueDetails();
|
||||
onUpdate();
|
||||
} catch {}
|
||||
}}
|
||||
options={[{ value: '', label: t('issues.noBrand') }, ...(brands || []).map(b => ({ value: b._id || b.Id, label: b.name }))]}
|
||||
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
|
||||
>
|
||||
<option value="">{t('issues.noBrand')}</option>
|
||||
{(brands || []).map((b) => (
|
||||
<option key={b._id || b.Id} value={b._id || b.Id}>{b.name}</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Internal Notes */}
|
||||
@@ -501,15 +489,12 @@ export default function IssueDetailPanel({ issue, onClose, onUpdate, teamMembers
|
||||
{activeTab === 'attachments' && (
|
||||
<div className="p-6 space-y-5">
|
||||
{/* Upload */}
|
||||
<label className="block">
|
||||
<input type="file" onChange={handleFileUpload} disabled={uploadingFile} className="hidden" />
|
||||
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center cursor-pointer hover:bg-surface-secondary transition-colors">
|
||||
<Upload className="w-6 h-6 mx-auto mb-2 text-text-tertiary" />
|
||||
<p className="text-sm text-text-secondary">
|
||||
{uploadingFile ? t('issues.uploading') : t('issues.clickToUpload')}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
<UploadZone
|
||||
onUpload={handleFileUpload}
|
||||
uploading={uploadingFile}
|
||||
label={t('issues.clickToUpload')}
|
||||
compact
|
||||
/>
|
||||
|
||||
{/* Attachments List */}
|
||||
<div className="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user