polish: cleanup unused code, i18n gaps, a11y, error handling

- Removed unused ApproverMultiSelect imports (ArtefactDetailPanel, TranslationDetailPanel)
- Removed stale editProjectId/editCampaignId state from ArtefactDetailPanel
- Added 3 missing i18n keys (selectVersionFirst, pendingReviewInfo, noReviewInfo)
- Added error toast on link picker API failure (PostDetail)
- Added ARIA attributes to PortalSelect (role=combobox, aria-expanded, listbox, option)
- Deleted test screenshots from project root
- Simplified artefact creation modal: title + type only (removed brand/project/campaign/approver/description)
- Cleaned up ArtefactDetailPanel props (removed unused projects/campaigns)
- Translation submit-review: requires source_content before allowing review
- Artefact submit-review: requires at least one attachment for design/video
- Translation reviewer moved to Review tab (single select, mandatory)
- Server blocks translation submit without reviewer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-16 15:48:19 +03:00
parent eb23931ce0
commit 16a94a2f19
12 changed files with 156 additions and 90 deletions
@@ -59,3 +59,87 @@
at commitHookPassiveMountEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:9465:60) at commitHookPassiveMountEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:9465:60)
at commitPassiveMountOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11040:29) at commitPassiveMountOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11040:29)
at recursivelyTraversePassiveMountEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11010:13) @ http://localhost:5173/src/App.jsx?t=1773661195572:135 at recursivelyTraversePassiveMountEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11010:13) @ http://localhost:5173/src/App.jsx?t=1773661195572:135
[11275011ms] [ERROR] %o
%s
%s
ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664494925:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) The above error occurred in the <Artefacts> component. React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary. @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7000
[11275012ms] [ERROR] ErrorBoundary caught: ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664494925:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) {componentStack:
at Artefacts (http://localhost:5173/src/pages…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12
[11282373ms] [ERROR] %o
%s
%s
ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664502312:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) The above error occurred in the <Artefacts> component. React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary. @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7000
[11282374ms] [ERROR] ErrorBoundary caught: ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664502312:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) {componentStack:
at Artefacts (http://localhost:5173/src/pages…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12
[11301530ms] [ERROR] %o
%s
%s
ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664521350:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) The above error occurred in the <Artefacts> component. React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary. @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7000
[11301531ms] [ERROR] ErrorBoundary caught: ReferenceError: PortalSelect is not defined
at Artefacts (http://localhost:5173/src/pages/Artefacts.jsx?t=1773664521350:936:11)
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20)
at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24)
at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21)
at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8484:199)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98)
at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43)
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13)
at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) {componentStack:
at Artefacts (http://localhost:5173/src/pages…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

@@ -6,7 +6,6 @@ import { api } from '../utils/api'
import Modal from './Modal' import Modal from './Modal'
import TabbedModal from './TabbedModal' import TabbedModal from './TabbedModal'
import { useToast } from './ToastContainer' import { useToast } from './ToastContainer'
import ApproverMultiSelect from './ApproverMultiSelect'
import PortalSelect from './PortalSelect' import PortalSelect from './PortalSelect'
import { ArtefactDetailVersionsTab } from './ArtefactDetailVersionsTab' import { ArtefactDetailVersionsTab } from './ArtefactDetailVersionsTab'
@@ -25,7 +24,7 @@ const TYPE_ICONS = {
other: Sparkles, other: Sparkles,
} }
export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDelete, projects = [], campaigns = [], assignableUsers = [] }) { export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDelete, assignableUsers = [] }) {
const { t } = useLanguage() const { t } = useLanguage()
const { brands } = useContext(AppContext) const { brands } = useContext(AppContext)
const toast = useToast() const toast = useToast()
@@ -41,8 +40,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
// Editable fields // Editable fields
const [editTitle, setEditTitle] = useState(artefact.title || '') const [editTitle, setEditTitle] = useState(artefact.title || '')
const [editDescription, setEditDescription] = useState(artefact.description || '') const [editDescription, setEditDescription] = useState(artefact.description || '')
const [editProjectId, setEditProjectId] = useState(artefact.project_id || '')
const [editCampaignId, setEditCampaignId] = useState(artefact.campaign_id || '')
const [editApproverIds, setEditApproverIds] = useState( const [editApproverIds, setEditApproverIds] = useState(
artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []) artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [])
) )
@@ -67,8 +64,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
useEffect(() => { useEffect(() => {
setEditTitle(artefact.title || '') setEditTitle(artefact.title || '')
setEditDescription(artefact.description || '') setEditDescription(artefact.description || '')
setEditProjectId(artefact.project_id || '')
setEditCampaignId(artefact.campaign_id || '')
setEditApproverIds( setEditApproverIds(
artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : []) artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [])
) )
+6
View File
@@ -76,6 +76,9 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
type="button" type="button"
onClick={handleOpen} onClick={handleOpen}
disabled={disabled} disabled={disabled}
role="combobox"
aria-expanded={open}
aria-haspopup="listbox"
className={`flex items-center justify-between gap-1 text-start ${className} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} className={`flex items-center justify-between gap-1 text-start ${className} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
> >
<span className={`truncate ${selectedOption ? '' : 'text-text-tertiary'}`}>{displayText}</span> <span className={`truncate ${selectedOption ? '' : 'text-text-tertiary'}`}>{displayText}</span>
@@ -85,6 +88,7 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
{open && createPortal( {open && createPortal(
<div <div
ref={dropdownRef} ref={dropdownRef}
role="listbox"
className="fixed z-[99999] bg-surface border border-border rounded-lg shadow-lg overflow-y-auto animate-scale-in" className="fixed z-[99999] bg-surface border border-border rounded-lg shadow-lg overflow-y-auto animate-scale-in"
style={{ top: pos.top, left: pos.left, width: pos.width, maxHeight: 240 }} style={{ top: pos.top, left: pos.left, width: pos.width, maxHeight: 240 }}
> >
@@ -94,6 +98,8 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
<button <button
key={opt.value} key={opt.value}
type="button" type="button"
role="option"
aria-selected={String(opt.value) === String(value)}
onClick={() => handleSelect(opt.value)} onClick={() => handleSelect(opt.value)}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-start transition-colors ${ className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-start transition-colors ${
isSelected isSelected
@@ -7,7 +7,6 @@ import { AVAILABLE_LANGUAGES, TRANSLATION_STATUS_COLORS, isTextSelected, groupTe
import Modal from './Modal' import Modal from './Modal'
import TabbedModal from './TabbedModal' import TabbedModal from './TabbedModal'
import { useToast } from './ToastContainer' import { useToast } from './ToastContainer'
import ApproverMultiSelect from './ApproverMultiSelect'
import PortalSelect from './PortalSelect' import PortalSelect from './PortalSelect'
export default function TranslationDetailPanel({ translation, onClose, onUpdate, onDelete, assignableUsers = [], posts: externalPosts }) { export default function TranslationDetailPanel({ translation, onClose, onUpdate, onDelete, assignableUsers = [], posts: externalPosts }) {
@@ -318,14 +317,6 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate,
</div> </div>
<div>
<h4 className="text-xs font-semibold text-text-tertiary uppercase mb-1.5">{t('translations.approversLabel')}</h4>
<ApproverMultiSelect
selected={editApproverIds}
onChange={setEditApproverIds}
users={assignableUsers}
/>
</div>
</div> </div>
)} )}
@@ -443,13 +434,28 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate,
{activeTab === 'review' && ( {activeTab === 'review' && (
<div className="p-6 space-y-5"> <div className="p-6 space-y-5">
{['draft', 'revision_requested', 'rejected'].includes(translation.status) && ( {['draft', 'revision_requested', 'rejected'].includes(translation.status) && (
<button <>
onClick={handleSubmitReview} <div>
disabled={submitting} <h4 className="text-xs font-semibold text-text-tertiary uppercase mb-1.5">{t('artefacts.reviewer')}</h4>
className="w-full py-3 bg-brand-primary text-white rounded-lg font-medium hover:bg-brand-primary-light disabled:opacity-50 transition-colors shadow-sm" <PortalSelect
> value={editApproverIds[0] || ''}
{submitting ? t('translations.submitting') : t('translations.submitForReview')} onChange={val => {
</button> const ids = val ? [val] : []
setEditApproverIds(ids)
handleFieldUpdate('approver_ids', val || '')
}}
options={[{ value: '', label: t('artefacts.selectReviewer') }, ...(assignableUsers || []).map(u => ({ value: u.id || u.Id, label: u.name }))]}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary"
/>
</div>
<button
onClick={handleSubmitReview}
disabled={submitting || editApproverIds.length === 0}
className="w-full py-3 bg-brand-primary text-white rounded-lg font-medium hover:bg-brand-primary-light disabled:opacity-50 transition-colors shadow-sm"
>
{submitting ? t('translations.submitting') : t('translations.submitForReview')}
</button>
</>
)} )}
{currentReviewUrl && ( {currentReviewUrl && (
+3
View File
@@ -606,6 +606,9 @@
"issues.noIssuesInColumn": "لا توجد مشاكل", "issues.noIssuesInColumn": "لا توجد مشاكل",
"artefacts.details": "التفاصيل", "artefacts.details": "التفاصيل",
"artefacts.review": "المراجعة", "artefacts.review": "المراجعة",
"artefacts.selectVersionFirst": "اختر إصداراً لعرض التعليقات.",
"artefacts.pendingReviewInfo": "هذا العنصر قيد المراجعة حالياً.",
"artefacts.noReviewInfo": "لا توجد معلومات مراجعة متاحة.",
"artefacts.grid": "شبكة", "artefacts.grid": "شبكة",
"artefacts.list": "قائمة", "artefacts.list": "قائمة",
"artefacts.allCreators": "جميع المنشئين", "artefacts.allCreators": "جميع المنشئين",
+3
View File
@@ -606,6 +606,9 @@
"issues.noIssuesInColumn": "No issues", "issues.noIssuesInColumn": "No issues",
"artefacts.details": "Details", "artefacts.details": "Details",
"artefacts.review": "Review", "artefacts.review": "Review",
"artefacts.selectVersionFirst": "Select a version to view comments.",
"artefacts.pendingReviewInfo": "This artefact is currently pending review.",
"artefacts.noReviewInfo": "No review information available.",
"artefacts.grid": "Grid", "artefacts.grid": "Grid",
"artefacts.list": "List", "artefacts.list": "List",
"artefacts.allCreators": "All Creators", "artefacts.allCreators": "All Creators",
+11 -64
View File
@@ -11,7 +11,7 @@ import { useToast } from '../components/ToastContainer'
import ArtefactVersionTimeline from '../components/ArtefactVersionTimeline' import ArtefactVersionTimeline from '../components/ArtefactVersionTimeline'
import { SkeletonCard, SkeletonTable } from '../components/SkeletonLoader' import { SkeletonCard, SkeletonTable } from '../components/SkeletonLoader'
import ArtefactDetailPanel from '../components/ArtefactDetailPanel' import ArtefactDetailPanel from '../components/ArtefactDetailPanel'
import ApproverMultiSelect from '../components/ApproverMultiSelect' import PortalSelect from '../components/PortalSelect'
const STATUS_COLORS = { const STATUS_COLORS = {
draft: 'bg-surface-tertiary text-text-secondary', draft: 'bg-surface-tertiary text-text-secondary',
@@ -56,7 +56,7 @@ export default function Artefacts() {
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedArtefact, setSelectedArtefact] = useState(null) const [selectedArtefact, setSelectedArtefact] = useState(null)
const [newArtefact, setNewArtefact] = useState({ title: '', description: '', type: 'copy', brand_id: '', content: '', project_id: '', campaign_id: '', approver_ids: [] }) const [newArtefact, setNewArtefact] = useState({ title: '', type: 'copy' })
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
// Bulk select // Bulk select
@@ -101,12 +101,12 @@ export default function Artefacts() {
setSaving(true) setSaving(true)
try { try {
const created = await api.post('/artefacts', { const created = await api.post('/artefacts', {
...newArtefact, title: newArtefact.title,
approver_ids: newArtefact.approver_ids.length > 0 ? newArtefact.approver_ids.join(',') : null, type: newArtefact.type,
}) })
toast.success(t('artefacts.created')) toast.success(t('artefacts.created'))
setShowCreateModal(false) setShowCreateModal(false)
setNewArtefact({ title: '', description: '', type: 'copy', brand_id: '', content: '', project_id: '', campaign_id: '', approver_ids: [] }) setNewArtefact({ title: '', type: 'copy' })
loadArtefacts() loadArtefacts()
setSelectedArtefact(created) setSelectedArtefact(created)
} catch (err) { } catch (err) {
@@ -480,7 +480,7 @@ export default function Artefacts() {
)} )}
{/* Create Modal */} {/* Create Modal */}
<Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title={t('artefacts.createArtefact')} size="md"> <Modal isOpen={showCreateModal} onClose={() => setShowCreateModal(false)} title={t('artefacts.createArtefact')} size="sm">
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.titleLabel')} *</label> <label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.titleLabel')} *</label>
@@ -490,67 +490,16 @@ export default function Artefacts() {
onChange={e => setNewArtefact(f => ({ ...f, title: e.target.value }))} onChange={e => setNewArtefact(f => ({ ...f, title: e.target.value }))}
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 focus:border-brand-primary" 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 focus:border-brand-primary"
placeholder={t('artefacts.titlePlaceholder')} placeholder={t('artefacts.titlePlaceholder')}
autoFocus
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.type')}</label> <label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.type')}</label>
<select <PortalSelect
value={newArtefact.type} value={newArtefact.type}
onChange={e => setNewArtefact(f => ({ ...f, type: e.target.value }))} onChange={val => setNewArtefact(f => ({ ...f, type: val }))}
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 focus:border-brand-primary" options={TYPES.map(t => ({ value: t, label: t.charAt(0).toUpperCase() + t.slice(1) }))}
> className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-surface text-text-primary"
{TYPES.map(t => <option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.brand')}</label>
<select
value={newArtefact.brand_id}
onChange={e => setNewArtefact(f => ({ ...f, brand_id: e.target.value }))}
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 focus:border-brand-primary"
>
<option value=""></option>
{brands.map(b => <option key={b._id} value={b._id}>{b.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.project')}</label>
<select
value={newArtefact.project_id}
onChange={e => setNewArtefact(f => ({ ...f, project_id: e.target.value }))}
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 focus:border-brand-primary"
>
<option value=""></option>
{projects.map(p => <option key={p.Id || p._id || p.id} value={p.Id || p._id || p.id}>{p.name || p.title}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.campaign')}</label>
<select
value={newArtefact.campaign_id}
onChange={e => setNewArtefact(f => ({ ...f, campaign_id: e.target.value }))}
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 focus:border-brand-primary"
>
<option value=""></option>
{campaigns.map(c => <option key={c.Id || c._id || c.id} value={c.Id || c._id || c.id}>{c.name || c.title}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.approvers')}</label>
<ApproverMultiSelect
users={assignableUsers}
selected={newArtefact.approver_ids}
onChange={ids => setNewArtefact(f => ({ ...f, approver_ids: ids }))}
/>
</div>
<div>
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.description')}</label>
<textarea
value={newArtefact.description}
onChange={e => setNewArtefact(f => ({ ...f, description: e.target.value }))}
rows={3}
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 focus:border-brand-primary"
placeholder={t('artefacts.descriptionPlaceholder')}
/> />
</div> </div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border"> <div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
@@ -591,8 +540,6 @@ export default function Artefacts() {
onClose={() => setSelectedArtefact(null)} onClose={() => setSelectedArtefact(null)}
onUpdate={loadArtefacts} onUpdate={loadArtefacts}
onDelete={canDeleteResource('artefact', selectedArtefact) ? handleDelete : undefined} onDelete={canDeleteResource('artefact', selectedArtefact) ? handleDelete : undefined}
projects={projects}
campaigns={campaigns}
assignableUsers={assignableUsers} assignableUsers={assignableUsers}
/> />
)} )}
+1 -2
View File
@@ -134,6 +134,7 @@ export default function PostDetail() {
} }
} catch { } catch {
setLinkCandidates([]) setLinkCandidates([])
toast.error(t('common.error'))
} }
} }
@@ -468,8 +469,6 @@ export default function PostDetail() {
onUpdate={loadComposition} onUpdate={loadComposition}
onDelete={() => { setOpenArtefact(null); loadComposition() }} onDelete={() => { setOpenArtefact(null); loadComposition() }}
assignableUsers={teamMembers} assignableUsers={teamMembers}
projects={[]}
campaigns={campaigns}
/> />
)} )}
</div> </div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

+25 -2
View File
@@ -4205,6 +4205,22 @@ app.post('/api/artefacts/:id/submit-review', requireAuth, async (req, res) => {
return res.status(403).json({ error: 'You can only submit your own artefacts' }); return res.status(403).json({ error: 'You can only submit your own artefacts' });
} }
// Design/video must have at least one attachment uploaded
if (existing.type === 'design' || existing.type === 'video') {
const versions = await nocodb.list('ArtefactVersions', {
where: `(artefact_id,eq,${sanitizeWhereValue(existing.Id)})`,
sort: '-version_number', limit: 1,
});
if (versions.length > 0) {
const attachments = await nocodb.list('ArtefactAttachments', {
where: `(version_id,eq,${versions[0].Id})`, limit: 1,
});
if (attachments.length === 0) {
return res.status(400).json({ error: 'Upload at least one file before submitting for review' });
}
}
}
const approverIds = parseApproverIds(existing.approver_ids); const approverIds = parseApproverIds(existing.approver_ids);
if (approverIds.length === 0) { if (approverIds.length === 0) {
return res.status(400).json({ error: 'Select a reviewer before submitting for review' }); return res.status(400).json({ error: 'Select a reviewer before submitting for review' });
@@ -4995,8 +5011,6 @@ app.get('/api/translations/:id', requireAuth, async (req, res) => {
app.post('/api/translations', requireAuth, async (req, res) => { app.post('/api/translations', requireAuth, async (req, res) => {
const { title, source_language, source_content, brand_id, post_id, approver_ids, copy_type } = req.body; const { title, source_language, source_content, brand_id, post_id, approver_ids, copy_type } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' }); if (!title) return res.status(400).json({ error: 'Title is required' });
if (!source_language) return res.status(400).json({ error: 'Source language is required' });
if (!source_content) return res.status(400).json({ error: 'Source content is required' });
try { try {
const created = await nocodb.create('Translations', { const created = await nocodb.create('Translations', {
@@ -5286,6 +5300,15 @@ app.post('/api/translations/:id/submit-review', requireAuth, async (req, res) =>
return res.status(403).json({ error: 'You can only submit your own translations' }); return res.status(403).json({ error: 'You can only submit your own translations' });
} }
if (!existing.source_content || !existing.source_content.trim()) {
return res.status(400).json({ error: 'Add content before submitting for review' });
}
const approverIds = parseApproverIds(existing.approver_ids);
if (approverIds.length === 0) {
return res.status(400).json({ error: 'Select a reviewer before submitting for review' });
}
const token = require('crypto').randomUUID(); const token = require('crypto').randomUUID();
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays); expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays);