refactor: PostDetail cards as status dashboard, panel for creation

Reverted inline asset creation — the copy workflow (original → approve →
translate → approve translations) needs the full panel, not a card form.

PostDetail cards now:
- "Create new" → creates asset (type pre-set, post linked) → opens panel
- "Open" → opens panel for editing/reviewing
- Card shows: title, status, preview, approval info (status dashboard)

Panel handles: write copy, add translations, upload files, select reviewer,
submit for review — the full workflow in its proper workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-16 15:10:40 +03:00
parent 49e1a796ed
commit eb23931ce0
5 changed files with 164 additions and 76 deletions
@@ -26,3 +26,36 @@
at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13) 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 performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) {componentStack:
at ArtefactDetailVersionsTab (http://localhos…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12 at ArtefactDetailVersionsTab (http://localhos…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12
[ 7975521ms] [ERROR] Failed to load team: TypeError: Failed to fetch
at Object.get (http://localhost:5173/src/utils/api.js:59:18)
at loadTeam (http://localhost:5173/src/App.jsx?t=1773661195572:114:30)
at loadInitialData (http://localhost:5173/src/App.jsx?t=1773661195572:143:11)
at http://localhost:5173/src/App.jsx?t=1773661195572:93:7
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18567:20)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at commitHookEffectListMount (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:9411:163)
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:118
[ 7975522ms] [ERROR] Failed to load teams: TypeError: Failed to fetch
at Object.get (http://localhost:5173/src/utils/api.js:59:18)
at loadTeams (http://localhost:5173/src/App.jsx?t=1773661195572:125:30)
at loadInitialData (http://localhost:5173/src/App.jsx?t=1773661195572:145:11)
at http://localhost:5173/src/App.jsx?t=1773661195572:93:7
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18567:20)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at commitHookEffectListMount (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:9411:163)
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:127
[ 7975522ms] [ERROR] Failed to load roles: TypeError: Failed to fetch
at Object.get (http://localhost:5173/src/utils/api.js:59:18)
at loadRoles (http://localhost:5173/src/App.jsx?t=1773661195572:133:30)
at loadInitialData (http://localhost:5173/src/App.jsx?t=1773661195572:146:11)
at http://localhost:5173/src/App.jsx?t=1773661195572:93:7
at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18567:20)
at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72)
at commitHookEffectListMount (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:9411:163)
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
+14
View File
@@ -1185,6 +1185,20 @@
"postDetail.createNew": "إنشاء جديد", "postDetail.createNew": "إنشاء جديد",
"postDetail.open": "فتح", "postDetail.open": "فتح",
"postDetail.unlink": "إلغاء الربط", "postDetail.unlink": "إلغاء الربط",
"postDetail.viewDetails": "عرض التفاصيل",
"postDetail.reviewer": "المراجع",
"postDetail.selectReviewer": "اختر المراجع",
"postDetail.submitForReview": "إرسال للمراجعة",
"postDetail.pendingReviewBy": "بانتظار مراجعة",
"postDetail.approved": "تمت الموافقة",
"postDetail.sourceLanguage": "اللغة المصدر",
"postDetail.content": "المحتوى",
"postDetail.contentPlaceholder": "اكتب النص...",
"postDetail.files": "الملفات",
"postDetail.dragDropFiles": "اسحب وأفلت أو انقر للرفع",
"postDetail.addMoreFiles": "إضافة ملفات أخرى",
"postDetail.createAndSubmit": "إنشاء وإرسال للمراجعة",
"postDetail.create": "إنشاء",
"finance.campaign": "الحملة", "finance.campaign": "الحملة",
"finance.budgetAssigned": "الميزانية المخصصة", "finance.budgetAssigned": "الميزانية المخصصة",
"finance.trackAllocated": "المسار المخصص", "finance.trackAllocated": "المسار المخصص",
+14
View File
@@ -1185,6 +1185,20 @@
"postDetail.createNew": "Create new", "postDetail.createNew": "Create new",
"postDetail.open": "Open", "postDetail.open": "Open",
"postDetail.unlink": "Unlink", "postDetail.unlink": "Unlink",
"postDetail.viewDetails": "View details",
"postDetail.reviewer": "Reviewer",
"postDetail.selectReviewer": "Select reviewer",
"postDetail.submitForReview": "Submit for Review",
"postDetail.pendingReviewBy": "Pending review by",
"postDetail.approved": "Approved",
"postDetail.sourceLanguage": "Source Language",
"postDetail.content": "Content",
"postDetail.contentPlaceholder": "Write the copy text...",
"postDetail.files": "Files",
"postDetail.dragDropFiles": "Drag & drop or click to upload",
"postDetail.addMoreFiles": "Add more files",
"postDetail.createAndSubmit": "Create & Submit for Review",
"postDetail.create": "Create",
"finance.campaign": "Campaign", "finance.campaign": "Campaign",
"finance.budgetAssigned": "Budget Assigned", "finance.budgetAssigned": "Budget Assigned",
"finance.trackAllocated": "Track Allocated", "finance.trackAllocated": "Track Allocated",
+81 -72
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useContext, useCallback } from 'react' import { useState, useEffect, useContext, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Save, FileText, Image as ImageIcon, Film, Type, Search, Link2, Unlink, Plus, CheckCircle, Clock, X } from 'lucide-react' import { ArrowLeft, Save, FileText, Image as ImageIcon, Film, Type, Search, Link2, Unlink, Plus, CheckCircle, Clock, X, ExternalLink } from 'lucide-react'
import { AppContext } from '../App' import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext' import { useLanguage } from '../i18n/LanguageContext'
@@ -172,46 +172,6 @@ export default function PostDetail() {
} }
} }
const handleCreate = async (type) => {
try {
if (type === 'caption') {
await api.post('/translations', {
post_id: Number(id),
copy_type: 'caption',
title: (title || 'Post') + ' - Caption',
source_language: 'EN',
source_content: ' ',
})
} else if (type === 'body') {
await api.post('/translations', {
post_id: Number(id),
copy_type: 'body',
title: (title || 'Post') + ' - Copy',
source_language: 'EN',
source_content: ' ',
})
} else if (type === 'design') {
await api.post('/artefacts', {
post_id: Number(id),
type: 'design',
title: (title || 'Post') + ' - Design',
status: 'draft',
})
} else if (type === 'video') {
await api.post('/artefacts', {
post_id: Number(id),
type: 'video',
title: (title || 'Post') + ' - Video',
status: 'draft',
})
}
toast.success(t('posts.created'))
loadComposition()
} catch {
toast.error(t('common.saveFailed'))
}
}
const handleOpenPiece = async (type) => { const handleOpenPiece = async (type) => {
const piece = type === 'caption' ? composition?.caption const piece = type === 'caption' ? composition?.caption
: type === 'body' ? composition?.body_copy : type === 'body' ? composition?.body_copy
@@ -231,6 +191,32 @@ export default function PostDetail() {
} }
} }
const handleCreate = async (type) => {
const isCopy = type === 'caption' || type === 'body'
try {
if (isCopy) {
const created = await api.post('/translations', {
title: `${title}${type === 'caption' ? t('postDetail.captionCopy') : t('postDetail.bodyCopy')}`,
source_language: 'AR',
source_content: '',
post_id: Number(id),
copy_type: type,
})
setOpenTranslation(created)
} else {
const created = await api.post('/artefacts', {
title: `${type === 'design' ? t('postDetail.design') : t('postDetail.video')}${title}`,
type: type,
post_id: Number(id),
})
setOpenArtefact(created)
}
loadComposition()
} catch (err) {
toast.error(t('common.saveFailed'))
}
}
// ─── Rendering ─── // ─── Rendering ───
if (loading) { if (loading) {
@@ -371,14 +357,14 @@ export default function PostDetail() {
label={t('postDetail.captionCopy')} label={t('postDetail.captionCopy')}
icon={Type} icon={Type}
piece={composition?.caption} piece={composition?.caption}
onCreate={() => handleCreate('caption')}
onOpen={() => handleOpenPiece('caption')}
onUnlink={() => handleUnlink('caption')}
onOpenPicker={() => openLinkPicker('caption')}
activePicker={activePicker} activePicker={activePicker}
pickerSearch={pickerSearch} pickerSearch={pickerSearch}
filteredCandidates={filteredCandidates} filteredCandidates={filteredCandidates}
linking={linking} linking={linking}
onOpen={() => handleOpenPiece('caption')}
onUnlink={() => handleUnlink('caption')}
onOpenPicker={() => openLinkPicker('caption')}
onCreate={() => handleCreate('caption')}
onLink={handleLink} onLink={handleLink}
onPickerSearchChange={setPickerSearch} onPickerSearchChange={setPickerSearch}
onClosePicker={() => setActivePicker(null)} onClosePicker={() => setActivePicker(null)}
@@ -389,14 +375,14 @@ export default function PostDetail() {
label={t('postDetail.bodyCopy')} label={t('postDetail.bodyCopy')}
icon={FileText} icon={FileText}
piece={composition?.body_copy} piece={composition?.body_copy}
onCreate={() => handleCreate('body')}
onOpen={() => handleOpenPiece('body')}
onUnlink={() => handleUnlink('body')}
onOpenPicker={() => openLinkPicker('body')}
activePicker={activePicker} activePicker={activePicker}
pickerSearch={pickerSearch} pickerSearch={pickerSearch}
filteredCandidates={filteredCandidates} filteredCandidates={filteredCandidates}
linking={linking} linking={linking}
onOpen={() => handleOpenPiece('body')}
onUnlink={() => handleUnlink('body')}
onOpenPicker={() => openLinkPicker('body')}
onCreate={() => handleCreate('body')}
onLink={handleLink} onLink={handleLink}
onPickerSearchChange={setPickerSearch} onPickerSearchChange={setPickerSearch}
onClosePicker={() => setActivePicker(null)} onClosePicker={() => setActivePicker(null)}
@@ -407,14 +393,14 @@ export default function PostDetail() {
label={t('postDetail.design')} label={t('postDetail.design')}
icon={ImageIcon} icon={ImageIcon}
piece={composition?.design} piece={composition?.design}
onCreate={() => handleCreate('design')}
onOpen={() => handleOpenPiece('design')}
onUnlink={() => handleUnlink('design')}
onOpenPicker={() => openLinkPicker('design')}
activePicker={activePicker} activePicker={activePicker}
pickerSearch={pickerSearch} pickerSearch={pickerSearch}
filteredCandidates={filteredCandidates} filteredCandidates={filteredCandidates}
linking={linking} linking={linking}
onOpen={() => handleOpenPiece('design')}
onUnlink={() => handleUnlink('design')}
onOpenPicker={() => openLinkPicker('design')}
onCreate={() => handleCreate('design')}
onLink={handleLink} onLink={handleLink}
onPickerSearchChange={setPickerSearch} onPickerSearchChange={setPickerSearch}
onClosePicker={() => setActivePicker(null)} onClosePicker={() => setActivePicker(null)}
@@ -425,14 +411,14 @@ export default function PostDetail() {
label={t('postDetail.video')} label={t('postDetail.video')}
icon={Film} icon={Film}
piece={composition?.video} piece={composition?.video}
onCreate={() => handleCreate('video')}
onOpen={() => handleOpenPiece('video')}
onUnlink={() => handleUnlink('video')}
onOpenPicker={() => openLinkPicker('video')}
activePicker={activePicker} activePicker={activePicker}
pickerSearch={pickerSearch} pickerSearch={pickerSearch}
filteredCandidates={filteredCandidates} filteredCandidates={filteredCandidates}
linking={linking} linking={linking}
onOpen={() => handleOpenPiece('video')}
onUnlink={() => handleUnlink('video')}
onOpenPicker={() => openLinkPicker('video')}
onCreate={() => handleCreate('video')}
onLink={handleLink} onLink={handleLink}
onPickerSearchChange={setPickerSearch} onPickerSearchChange={setPickerSearch}
onClosePicker={() => setActivePicker(null)} onClosePicker={() => setActivePicker(null)}
@@ -494,21 +480,26 @@ export default function PostDetail() {
function AssetCard({ function AssetCard({
type, label, icon: Icon, piece, type, label, icon: Icon, piece,
activePicker, pickerSearch, filteredCandidates, linking, onCreate, onOpen, onUnlink,
onOpen, onUnlink, onOpenPicker, onCreate, onLink, onOpenPicker, activePicker, pickerSearch, filteredCandidates, linking,
onPickerSearchChange, onClosePicker, t, onLink, onPickerSearchChange, onClosePicker, t,
}) { }) {
const isPickerOpen = activePicker === type const isPickerOpen = activePicker === type
const isCopy = type === 'caption' || type === 'body' const isCopy = type === 'caption' || type === 'body'
const isPending = piece?.status === 'pending_review'
const isApproved = piece?.status === 'approved'
return ( return (
<div className="bg-surface rounded-xl border border-border p-4 flex flex-col"> <div className="bg-surface rounded-xl border border-border p-4 flex flex-col">
{/* Header */}
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Icon className="w-4 h-4 text-text-tertiary" /> <Icon className="w-4 h-4 text-text-tertiary" />
<h4 className="text-xs uppercase tracking-wider text-text-tertiary font-medium">{label}</h4> <h4 className="text-xs uppercase tracking-wider text-text-tertiary font-medium flex-1">{label}</h4>
</div> </div>
{piece ? ( {/* ─── State 2: Linked ─── */}
{piece && (
<> <>
<div className="flex-1"> <div className="flex-1">
{/* Thumbnail for design/video */} {/* Thumbnail for design/video */}
@@ -528,7 +519,7 @@ function AssetCard({
<StatusBadge status={piece.status} size="xs" /> <StatusBadge status={piece.status} size="xs" />
</div> </div>
{/* Copy: show content preview + languages */} {/* Copy: content preview + languages */}
{isCopy && piece.content_preview && ( {isCopy && piece.content_preview && (
<p className="text-xs text-text-secondary mt-1 line-clamp-2">{piece.content_preview}</p> <p className="text-xs text-text-secondary mt-1 line-clamp-2">{piece.content_preview}</p>
)} )}
@@ -553,14 +544,32 @@ function AssetCard({
{!isCopy && piece.current_version && ( {!isCopy && piece.current_version && (
<p className="text-xs text-text-tertiary mt-1">v{piece.current_version}</p> <p className="text-xs text-text-tertiary mt-1">v{piece.current_version}</p>
)} )}
{/* Approval info */}
<div className="mt-3 space-y-2">
{isPending && piece.approver_name && (
<p className="text-xs text-amber-600 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{t('postDetail.pendingReviewBy')} {piece.approver_name}
</p>
)}
{isApproved && (
<p className="text-xs text-emerald-600 flex items-center gap-1.5">
<CheckCircle className="w-3.5 h-3.5" />
{t('postDetail.approved')}{piece.approver_name ? `${piece.approver_name}` : ''}
</p>
)}
</div>
</div> </div>
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border-light">
{/* Open + Unlink */}
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border-light">
<button <button
onClick={onOpen} onClick={onOpen}
className="flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/10 rounded-lg transition-colors" className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/10 rounded-lg transition-colors"
> >
<Link2 className="w-3.5 h-3.5" /> <ExternalLink className="w-3.5 h-3.5" />
{t('postDetail.open')} {t('postDetail.viewDetails')}
</button> </button>
<button <button
onClick={onUnlink} onClick={onUnlink}
@@ -571,7 +580,10 @@ function AssetCard({
</button> </button>
</div> </div>
</> </>
) : ( )}
{/* ─── State 1: Empty (no asset) ─── */}
{!piece && (
<> <>
<div className="flex-1 flex items-center justify-center py-4"> <div className="flex-1 flex items-center justify-center py-4">
<p className="text-sm text-text-tertiary">{t('postDetail.notLinked')}</p> <p className="text-sm text-text-tertiary">{t('postDetail.notLinked')}</p>
@@ -597,7 +609,7 @@ function AssetCard({
</> </>
)} )}
{/* Inline picker */} {/* Inline link picker */}
{isPickerOpen && ( {isPickerOpen && (
<div className="mt-3 pt-3 border-t border-border-light animate-fade-in"> <div className="mt-3 pt-3 border-t border-border-light animate-fade-in">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
@@ -627,7 +639,6 @@ function AssetCard({
disabled={linking} disabled={linking}
className="w-full text-start px-2 py-2 text-xs rounded-lg hover:bg-surface-secondary transition-colors flex items-start gap-2 disabled:opacity-50" className="w-full text-start px-2 py-2 text-xs rounded-lg hover:bg-surface-secondary transition-colors flex items-start gap-2 disabled:opacity-50"
> >
{/* Thumbnail for artefacts */}
{!isCopy && (c.thumbnail_url || c.file_url) && ( {!isCopy && (c.thumbnail_url || c.file_url) && (
<img src={c.thumbnail_url || c.file_url} alt="" className="w-10 h-10 rounded object-cover shrink-0" loading="lazy" /> <img src={c.thumbnail_url || c.file_url} alt="" className="w-10 h-10 rounded object-cover shrink-0" loading="lazy" />
)} )}
@@ -636,14 +647,12 @@ function AssetCard({
<span className="truncate text-text-primary font-medium">{c.title || t('common.untitled')}</span> <span className="truncate text-text-primary font-medium">{c.title || t('common.untitled')}</span>
<StatusBadge status={c.status} size="xs" /> <StatusBadge status={c.status} size="xs" />
</div> </div>
{/* Copy: show source language + content preview */}
{isCopy && ( {isCopy && (
<p className="text-text-tertiary mt-0.5 truncate"> <p className="text-text-tertiary mt-0.5 truncate">
{c.source_language && <span className="uppercase">{c.source_language} · </span>} {c.source_language && <span className="uppercase">{c.source_language} · </span>}
{(c.source_content || '').slice(0, 60)} {(c.source_content || '').slice(0, 60)}
</p> </p>
)} )}
{/* Artefact: show type */}
{!isCopy && c.type && ( {!isCopy && c.type && (
<p className="text-text-tertiary mt-0.5 capitalize">{c.type}</p> <p className="text-text-tertiary mt-0.5 capitalize">{c.type}</p>
)} )}
+22 -4
View File
@@ -56,11 +56,29 @@ async function getPostComposition(postId) {
video ? (video.thumbnail_url || getFirstAttachment(video.Id)) : null, video ? (video.thumbnail_url || getFirstAttachment(video.Id)) : null,
]); ]);
// Resolve approver names for each piece
const resolveApprover = async (record) => {
if (!record || !record.approver_ids) return { approver_ids: null, approver_name: null };
const ids = record.approver_ids.split(',').map(s => s.trim()).filter(Boolean);
if (ids.length === 0) return { approver_ids: null, approver_name: null };
try {
const user = await nocodb.get('Users', Number(ids[0]));
return { approver_ids: record.approver_ids, approver_name: user ? (user.display_name || user.name || user.email) : null };
} catch { return { approver_ids: record.approver_ids, approver_name: null }; }
};
const [captionApprover, bodyApprover, designApprover, videoApprover] = await Promise.all([
resolveApprover(caption),
resolveApprover(bodyCopy),
resolveApprover(design),
resolveApprover(video),
]);
return { return {
caption: caption ? { id: caption.Id, title: caption.title, status: caption.status, language: caption.source_language, content_preview: (caption.source_content || '').slice(0, 120), languages: captionTexts } : null, caption: caption ? { id: caption.Id, title: caption.title, status: caption.status, language: caption.source_language, content_preview: (caption.source_content || '').slice(0, 120), languages: captionTexts, ...captionApprover } : null,
body_copy: bodyCopy ? { id: bodyCopy.Id, title: bodyCopy.title, status: bodyCopy.status, language: bodyCopy.source_language, content_preview: (bodyCopy.source_content || '').slice(0, 120), languages: bodyTexts } : null, body_copy: bodyCopy ? { id: bodyCopy.Id, title: bodyCopy.title, status: bodyCopy.status, language: bodyCopy.source_language, content_preview: (bodyCopy.source_content || '').slice(0, 120), languages: bodyTexts, ...bodyApprover } : null,
design: design ? { id: design.Id, title: design.title, status: design.status, thumbnail_url: designThumb, current_version: design.current_version } : null, design: design ? { id: design.Id, title: design.title, status: design.status, thumbnail_url: designThumb, current_version: design.current_version, ...designApprover } : null,
video: video ? { id: video.Id, title: video.title, status: video.status, thumbnail_url: videoThumb, current_version: video.current_version } : null, video: video ? { id: video.Id, title: video.title, status: video.status, thumbnail_url: videoThumb, current_version: video.current_version, ...videoApprover } : null,
platforms, platforms,
pieces_ready: piecesReady, pieces_ready: piecesReady,
waiting_on: waitingOn, waiting_on: waitingOn,