refactor: unify post composition — all assets are artefacts

PostDetail now uses Artefacts exclusively for all 4 asset types:
- Caption copy = Artefact with type='copy', copy_type='caption'
- Body copy = Artefact with type='copy', copy_type='body'
- Design = Artefact with type='design'
- Video = Artefact with type='video'

Removed TranslationDetailPanel from PostDetail entirely.
Same ArtefactDetailPanel, same workflow, same version management
for all asset types. Link picker searches artefacts only.

Server: copy_type added to Artefacts schema, accepted in POST/PATCH.
post-composition.js rewritten to use Artefacts table for all pieces.
Content preview fetched from ArtefactVersionTexts.

Translations entity still exists for the standalone Copy page.

Also: version creation rules, submit-review content validation,
reviewer mandatory for translations, artefact creation simplified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-16 17:15:21 +03:00
parent 16a94a2f19
commit 378d91648b
6 changed files with 106 additions and 105 deletions
+28 -65
View File
@@ -9,7 +9,6 @@ import PlatformIcon from '../components/PlatformIcon'
import StatusBadge from '../components/StatusBadge'
import PortalSelect from '../components/PortalSelect'
import CommentsSection from '../components/CommentsSection'
import TranslationDetailPanel from '../components/TranslationDetailPanel'
import ArtefactDetailPanel from '../components/ArtefactDetailPanel'
import { useToast } from '../components/ToastContainer'
@@ -45,7 +44,6 @@ export default function PostDetail() {
const [linking, setLinking] = useState(false)
// Sub-panels
const [openTranslation, setOpenTranslation] = useState(null)
const [openArtefact, setOpenArtefact] = useState(null)
useEffect(() => {
@@ -116,22 +114,17 @@ export default function PostDetail() {
setActivePicker(type)
setPickerSearch('')
try {
if (type === 'caption' || type === 'body') {
const all = await api.get('/translations')
// Show all translations not already linked to THIS post
setLinkCandidates((Array.isArray(all) ? all : []).filter(t => {
const linkedTo = t.post_id || t.postId
return !linkedTo || String(linkedTo) !== String(id)
}))
} else {
const all = await api.get('/artefacts')
const at = type === 'video' ? 'video' : 'design'
setLinkCandidates((Array.isArray(all) ? all : []).filter(a => {
const linkedTo = a.post_id || a.postId
const matchesType = (a.type || 'design') === at
return matchesType && (!linkedTo || String(linkedTo) !== String(id))
}))
}
const all = await api.get('/artefacts')
let typeFilter
if (type === 'caption') typeFilter = a => a.type === 'copy' && a.copy_type === 'caption'
else if (type === 'body') typeFilter = a => a.type === 'copy' && (a.copy_type === 'body' || !a.copy_type)
else if (type === 'video') typeFilter = a => a.type === 'video'
else typeFilter = a => (a.type || 'design') === 'design'
setLinkCandidates((Array.isArray(all) ? all : []).filter(a => {
const linkedTo = a.post_id || a.postId
return typeFilter(a) && (!linkedTo || String(linkedTo) !== String(id))
}))
} catch {
setLinkCandidates([])
toast.error(t('common.error'))
@@ -141,12 +134,7 @@ export default function PostDetail() {
const handleLink = async (itemId) => {
setLinking(true)
try {
const copyType = activePicker === 'caption' || activePicker === 'body' ? activePicker : null
if (activePicker === 'caption' || activePicker === 'body') {
await api.patch(`/translations/${itemId}`, { post_id: Number(id), copy_type: copyType })
} else {
await api.patch(`/artefacts/${itemId}`, { post_id: Number(id) })
}
await api.patch(`/artefacts/${itemId}`, { post_id: Number(id) })
toast.success(t('posts.updated'))
setActivePicker(null)
loadComposition()
@@ -164,8 +152,7 @@ export default function PostDetail() {
: composition?.video
if (!piece) return
try {
const endpoint = (type === 'caption' || type === 'body') ? `/translations/${piece.id}` : `/artefacts/${piece.id}`
await api.patch(endpoint, { post_id: null })
await api.patch(`/artefacts/${piece.id}`, { post_id: null })
toast.success(t('posts.updated'))
loadComposition()
} catch {
@@ -179,39 +166,25 @@ export default function PostDetail() {
: type === 'design' ? composition?.design
: composition?.video
if (!piece) return
if (type === 'caption' || type === 'body') {
try {
const full = await api.get(`/translations/${piece.id}`)
setOpenTranslation(full)
} catch { toast.error(t('common.saveFailed')) }
} else {
try {
const full = await api.get(`/artefacts/${piece.id}`)
setOpenArtefact(full)
} catch { toast.error(t('common.saveFailed')) }
}
try {
const full = await api.get(`/artefacts/${piece.id}`)
setOpenArtefact(full)
} catch { toast.error(t('common.saveFailed')) }
}
const handleCreate = async (type) => {
const isCopy = type === 'caption' || type === 'body'
const label = type === 'caption' ? t('postDetail.captionCopy')
: type === 'body' ? t('postDetail.bodyCopy')
: type === 'design' ? t('postDetail.design')
: t('postDetail.video')
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)
}
const created = await api.post('/artefacts', {
title: `${label}${title}`,
type: type === 'caption' || type === 'body' ? 'copy' : type,
copy_type: type === 'caption' ? 'caption' : type === 'body' ? 'body' : undefined,
post_id: Number(id),
})
setOpenArtefact(created)
loadComposition()
} catch (err) {
toast.error(t('common.saveFailed'))
@@ -452,16 +425,6 @@ export default function PostDetail() {
</div>
{/* ─── SUB-PANELS (they render their own SlidePanel internally) ─── */}
{openTranslation && (
<TranslationDetailPanel
translation={openTranslation}
onClose={() => { setOpenTranslation(null); loadComposition() }}
onUpdate={loadComposition}
onDelete={() => { setOpenTranslation(null); loadComposition() }}
assignableUsers={teamMembers}
/>
)}
{openArtefact && (
<ArtefactDetailPanel
artefact={openArtefact}