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:
+17
-15
@@ -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,
|
||||
|
||||
+38
-18
@@ -521,6 +521,7 @@ const TEXT_COLUMNS = {
|
||||
Artefacts: [
|
||||
{ name: 'approver_ids', uidt: 'SingleLineText' },
|
||||
{ name: 'thumbnail_url', uidt: 'SingleLineText' },
|
||||
{ name: 'copy_type', uidt: 'SingleLineText' },
|
||||
],
|
||||
Posts: [
|
||||
{ name: 'approver_ids', uidt: 'SingleLineText' },
|
||||
@@ -4019,7 +4020,7 @@ app.get('/api/artefacts/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids } = req.body;
|
||||
const { title, description, type, brand_id, content, project_id, campaign_id, approver_ids, copy_type } = req.body;
|
||||
if (!title) return res.status(400).json({ error: 'Title is required' });
|
||||
|
||||
try {
|
||||
@@ -4033,6 +4034,7 @@ app.post('/api/artefacts', requireAuth, async (req, res) => {
|
||||
project_id: project_id ? Number(project_id) : null,
|
||||
campaign_id: campaign_id ? Number(campaign_id) : null,
|
||||
approver_ids: approver_ids || null,
|
||||
copy_type: copy_type || null,
|
||||
created_by_user_id: req.session.userId,
|
||||
current_version: 1,
|
||||
};
|
||||
@@ -4112,7 +4114,7 @@ app.patch('/api/artefacts/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const data = {};
|
||||
for (const f of ['title', 'description', 'type', 'status', 'content', 'feedback']) {
|
||||
for (const f of ['title', 'description', 'type', 'status', 'content', 'feedback', 'copy_type']) {
|
||||
if (req.body[f] !== undefined) data[f] = req.body[f];
|
||||
}
|
||||
if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
|
||||
@@ -4311,12 +4313,45 @@ app.post('/api/artefacts/:id/versions', requireAuth, async (req, res) => {
|
||||
return res.status(403).json({ error: 'You can only create versions for your own artefacts' });
|
||||
}
|
||||
|
||||
// Get current max version number
|
||||
// Get current max version
|
||||
const versions = await nocodb.list('ArtefactVersions', {
|
||||
where: `(artefact_id,eq,${sanitizeWhereValue(req.params.id)})`,
|
||||
sort: '-version_number',
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
// Validate previous version before allowing new one
|
||||
if (versions.length > 0) {
|
||||
const prevVersion = versions[0];
|
||||
|
||||
// Check if previous version has content
|
||||
if (artefact.type === 'design' || artefact.type === 'video') {
|
||||
const attachments = await nocodb.list('ArtefactAttachments', {
|
||||
where: `(version_id,eq,${prevVersion.Id})`, limit: 1,
|
||||
});
|
||||
if (attachments.length === 0) {
|
||||
return res.status(400).json({ error: 'Upload content to the current version before creating a new one' });
|
||||
}
|
||||
} else if (artefact.type === 'copy') {
|
||||
const texts = await nocodb.list('ArtefactVersionTexts', {
|
||||
where: `(version_id,eq,${prevVersion.Id})`, limit: 1,
|
||||
});
|
||||
if (texts.length === 0) {
|
||||
return res.status(400).json({ error: 'Add text to the current version before creating a new one' });
|
||||
}
|
||||
}
|
||||
|
||||
// Can't create new version if artefact hasn't been submitted for review yet
|
||||
if (artefact.status === 'draft') {
|
||||
return res.status(400).json({ error: 'Submit the current version for review before creating a new one' });
|
||||
}
|
||||
|
||||
// If revision_requested, user should edit the current version, not create a new one
|
||||
if (artefact.status === 'revision_requested') {
|
||||
return res.status(400).json({ error: 'Edit the current version and resubmit instead of creating a new one' });
|
||||
}
|
||||
}
|
||||
|
||||
const newVersionNumber = versions.length > 0 ? versions[0].version_number + 1 : 1;
|
||||
|
||||
const created = await nocodb.create('ArtefactVersions', {
|
||||
@@ -5084,21 +5119,6 @@ app.patch('/api/translations/:id', requireAuth, async (req, res) => {
|
||||
|
||||
await nocodb.update('Translations', req.params.id, data);
|
||||
|
||||
// Auto-update linked post stage (both old and new post if post_id changed)
|
||||
const oldTransPostId = existing.post_id ? Number(existing.post_id) : null;
|
||||
const updated = await nocodb.get('Translations', Number(req.params.id));
|
||||
const newTransPostId = updated?.post_id ? Number(updated.post_id) : null;
|
||||
const transPostIds = [...new Set([oldTransPostId, newTransPostId].filter(Boolean))];
|
||||
for (const pid of transPostIds) {
|
||||
try {
|
||||
const { getPostComposition, computeStage } = require('./post-composition');
|
||||
const composition = await getPostComposition(pid);
|
||||
if (composition) {
|
||||
await nocodb.update('Posts', pid, { stage: computeStage(composition) });
|
||||
}
|
||||
} catch (e) { console.error('Post stage update error:', e); }
|
||||
}
|
||||
|
||||
const record = await nocodb.get('Translations', req.params.id);
|
||||
const approverIdList = record.approver_ids ? record.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
const approvers = [];
|
||||
|
||||
Reference in New Issue
Block a user