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 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) : [])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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) && (
|
||||||
|
<>
|
||||||
|
<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
|
<button
|
||||||
onClick={handleSubmitReview}
|
onClick={handleSubmitReview}
|
||||||
disabled={submitting}
|
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"
|
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')}
|
{submitting ? t('translations.submitting') : t('translations.submitForReview')}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentReviewUrl && (
|
{currentReviewUrl && (
|
||||||
|
|||||||
@@ -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": "جميع المنشئين",
|
||||||
|
|||||||
@@ -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,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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user