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:
@@ -59,3 +59,87 @@
|
||||
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 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 TabbedModal from './TabbedModal'
|
||||
import { useToast } from './ToastContainer'
|
||||
import ApproverMultiSelect from './ApproverMultiSelect'
|
||||
import PortalSelect from './PortalSelect'
|
||||
import { ArtefactDetailVersionsTab } from './ArtefactDetailVersionsTab'
|
||||
|
||||
@@ -25,7 +24,7 @@ const TYPE_ICONS = {
|
||||
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 { brands } = useContext(AppContext)
|
||||
const toast = useToast()
|
||||
@@ -41,8 +40,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel
|
||||
// Editable fields
|
||||
const [editTitle, setEditTitle] = useState(artefact.title || '')
|
||||
const [editDescription, setEditDescription] = useState(artefact.description || '')
|
||||
const [editProjectId, setEditProjectId] = useState(artefact.project_id || '')
|
||||
const [editCampaignId, setEditCampaignId] = useState(artefact.campaign_id || '')
|
||||
const [editApproverIds, setEditApproverIds] = useState(
|
||||
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(() => {
|
||||
setEditTitle(artefact.title || '')
|
||||
setEditDescription(artefact.description || '')
|
||||
setEditProjectId(artefact.project_id || '')
|
||||
setEditCampaignId(artefact.campaign_id || '')
|
||||
setEditApproverIds(
|
||||
artefact.approvers?.map(a => String(a.id)) || (artefact.approver_ids ? artefact.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [])
|
||||
)
|
||||
|
||||
@@ -76,6 +76,9 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
|
||||
type="button"
|
||||
onClick={handleOpen}
|
||||
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'}`}
|
||||
>
|
||||
<span className={`truncate ${selectedOption ? '' : 'text-text-tertiary'}`}>{displayText}</span>
|
||||
@@ -85,6 +88,7 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
|
||||
{open && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
role="listbox"
|
||||
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 }}
|
||||
>
|
||||
@@ -94,6 +98,8 @@ export default function PortalSelect({ value, onChange, options = [], placeholde
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={String(opt.value) === String(value)}
|
||||
onClick={() => handleSelect(opt.value)}
|
||||
className={`w-full flex items-center gap-2 px-3 py-1.5 text-xs text-start transition-colors ${
|
||||
isSelected
|
||||
|
||||
@@ -7,7 +7,6 @@ import { AVAILABLE_LANGUAGES, TRANSLATION_STATUS_COLORS, isTextSelected, groupTe
|
||||
import Modal from './Modal'
|
||||
import TabbedModal from './TabbedModal'
|
||||
import { useToast } from './ToastContainer'
|
||||
import ApproverMultiSelect from './ApproverMultiSelect'
|
||||
import PortalSelect from './PortalSelect'
|
||||
|
||||
export default function TranslationDetailPanel({ translation, onClose, onUpdate, onDelete, assignableUsers = [], posts: externalPosts }) {
|
||||
@@ -318,14 +317,6 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate,
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -443,13 +434,28 @@ export default function TranslationDetailPanel({ translation, onClose, onUpdate,
|
||||
{activeTab === 'review' && (
|
||||
<div className="p-6 space-y-5">
|
||||
{['draft', 'revision_requested', 'rejected'].includes(translation.status) && (
|
||||
<button
|
||||
onClick={handleSubmitReview}
|
||||
disabled={submitting}
|
||||
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>
|
||||
<>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-text-tertiary uppercase mb-1.5">{t('artefacts.reviewer')}</h4>
|
||||
<PortalSelect
|
||||
value={editApproverIds[0] || ''}
|
||||
onChange={val => {
|
||||
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 && (
|
||||
|
||||
@@ -606,6 +606,9 @@
|
||||
"issues.noIssuesInColumn": "لا توجد مشاكل",
|
||||
"artefacts.details": "التفاصيل",
|
||||
"artefacts.review": "المراجعة",
|
||||
"artefacts.selectVersionFirst": "اختر إصداراً لعرض التعليقات.",
|
||||
"artefacts.pendingReviewInfo": "هذا العنصر قيد المراجعة حالياً.",
|
||||
"artefacts.noReviewInfo": "لا توجد معلومات مراجعة متاحة.",
|
||||
"artefacts.grid": "شبكة",
|
||||
"artefacts.list": "قائمة",
|
||||
"artefacts.allCreators": "جميع المنشئين",
|
||||
|
||||
@@ -606,6 +606,9 @@
|
||||
"issues.noIssuesInColumn": "No issues",
|
||||
"artefacts.details": "Details",
|
||||
"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.list": "List",
|
||||
"artefacts.allCreators": "All Creators",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useToast } from '../components/ToastContainer'
|
||||
import ArtefactVersionTimeline from '../components/ArtefactVersionTimeline'
|
||||
import { SkeletonCard, SkeletonTable } from '../components/SkeletonLoader'
|
||||
import ArtefactDetailPanel from '../components/ArtefactDetailPanel'
|
||||
import ApproverMultiSelect from '../components/ApproverMultiSelect'
|
||||
import PortalSelect from '../components/PortalSelect'
|
||||
|
||||
const STATUS_COLORS = {
|
||||
draft: 'bg-surface-tertiary text-text-secondary',
|
||||
@@ -56,7 +56,7 @@ export default function Artefacts() {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
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)
|
||||
|
||||
// Bulk select
|
||||
@@ -101,12 +101,12 @@ export default function Artefacts() {
|
||||
setSaving(true)
|
||||
try {
|
||||
const created = await api.post('/artefacts', {
|
||||
...newArtefact,
|
||||
approver_ids: newArtefact.approver_ids.length > 0 ? newArtefact.approver_ids.join(',') : null,
|
||||
title: newArtefact.title,
|
||||
type: newArtefact.type,
|
||||
})
|
||||
toast.success(t('artefacts.created'))
|
||||
setShowCreateModal(false)
|
||||
setNewArtefact({ title: '', description: '', type: 'copy', brand_id: '', content: '', project_id: '', campaign_id: '', approver_ids: [] })
|
||||
setNewArtefact({ title: '', type: 'copy' })
|
||||
loadArtefacts()
|
||||
setSelectedArtefact(created)
|
||||
} catch (err) {
|
||||
@@ -480,7 +480,7 @@ export default function Artefacts() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<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 }))}
|
||||
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')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">{t('artefacts.type')}</label>
|
||||
<select
|
||||
<PortalSelect
|
||||
value={newArtefact.type}
|
||||
onChange={e => setNewArtefact(f => ({ ...f, type: 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"
|
||||
>
|
||||
{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')}
|
||||
onChange={val => setNewArtefact(f => ({ ...f, type: val }))}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<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)}
|
||||
onUpdate={loadArtefacts}
|
||||
onDelete={canDeleteResource('artefact', selectedArtefact) ? handleDelete : undefined}
|
||||
projects={projects}
|
||||
campaigns={campaigns}
|
||||
assignableUsers={assignableUsers}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -134,6 +134,7 @@ export default function PostDetail() {
|
||||
}
|
||||
} catch {
|
||||
setLinkCandidates([])
|
||||
toast.error(t('common.error'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,8 +469,6 @@ export default function PostDetail() {
|
||||
onUpdate={loadComposition}
|
||||
onDelete={() => { setOpenArtefact(null); loadComposition() }}
|
||||
assignableUsers={teamMembers}
|
||||
projects={[]}
|
||||
campaigns={campaigns}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
+25
-2
@@ -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' });
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (approverIds.length === 0) {
|
||||
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) => {
|
||||
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 (!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 {
|
||||
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' });
|
||||
}
|
||||
|
||||
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 expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + DEFAULTS.tokenExpiryDays);
|
||||
|
||||
Reference in New Issue
Block a user