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
+17 -15
View File
@@ -4,15 +4,12 @@ async function getPostComposition(postId) {
const post = await nocodb.get('Posts', postId);
if (!post) return null;
const translations = await nocodb.list('Translations', {
where: `(post_id,eq,${postId})`, limit: 100,
});
const caption = translations.find(t => t.copy_type === 'caption') || null;
const bodyCopy = translations.find(t => t.copy_type === 'body' || !t.copy_type) || null;
const artefacts = await nocodb.list('Artefacts', {
where: `(post_id,eq,${postId})`, limit: 100,
});
const caption = artefacts.find(a => a.type === 'copy' && a.copy_type === 'caption') || null;
const bodyCopy = artefacts.find(a => a.type === 'copy' && (a.copy_type === 'body' || !a.copy_type)) || null;
const design = artefacts.find(a => (a.type || 'design') === 'design') || null;
const video = artefacts.find(a => a.type === 'video') || null;
@@ -28,16 +25,21 @@ async function getPostComposition(postId) {
const hasPieces = caption || bodyCopy || design || video;
const piecesReady = hasPieces && waitingOn.length === 0;
// Get translation texts for languages preview
const getTexts = async (translationId) => {
// Get texts from ArtefactVersionTexts for copy artefacts (content preview + languages)
const getTexts = async (artefactId) => {
try {
const texts = await nocodb.list('TranslationTexts', { where: `(translation_id,eq,${translationId})`, limit: 20 });
return texts.map(tt => ({ language: tt.language_code || tt.language, status: tt.status || 'draft' }));
} catch { return []; }
const versions = await nocodb.list('ArtefactVersions', { where: `(artefact_id,eq,${artefactId})`, sort: '-version_number', limit: 1 });
if (versions.length === 0) return { texts: [], contentPreview: '' };
const texts = await nocodb.list('ArtefactVersionTexts', { where: `(version_id,eq,${versions[0].Id})`, limit: 20 });
const languages = texts.map(tt => ({ language: tt.language_code || tt.language, status: tt.status || 'draft' }));
const contentPreview = texts.length > 0 ? (texts[0].content || '').slice(0, 120) : '';
return { texts: languages, contentPreview };
} catch { return { texts: [], contentPreview: '' }; }
};
const [captionTexts, bodyTexts] = await Promise.all([
caption ? getTexts(caption.Id) : [],
bodyCopy ? getTexts(bodyCopy.Id) : [],
caption ? getTexts(caption.Id) : { texts: [], contentPreview: '' },
bodyCopy ? getTexts(bodyCopy.Id) : { texts: [], contentPreview: '' },
]);
// Get first attachment for design/video thumbnail
@@ -75,8 +77,8 @@ async function getPostComposition(postId) {
]);
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, ...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, ...bodyApprover } : null,
caption: caption ? { id: caption.Id, title: caption.title, status: caption.status, content_preview: captionTexts.contentPreview, languages: captionTexts.texts, ...captionApprover } : null,
body_copy: bodyCopy ? { id: bodyCopy.Id, title: bodyCopy.title, status: bodyCopy.status, content_preview: bodyTexts.contentPreview, languages: bodyTexts.texts, ...bodyApprover } : 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, ...videoApprover } : null,
platforms,