eb23931ce0
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>
98 lines
4.6 KiB
JavaScript
98 lines
4.6 KiB
JavaScript
const nocodb = require('./nocodb');
|
|
|
|
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 design = artefacts.find(a => (a.type || 'design') === 'design') || null;
|
|
const video = artefacts.find(a => a.type === 'video') || null;
|
|
|
|
let platforms = [];
|
|
try { platforms = JSON.parse(post.platforms || '[]'); } catch { platforms = post.platform ? [post.platform] : []; }
|
|
|
|
const waitingOn = [];
|
|
if (caption && caption.status !== 'approved') waitingOn.push('Caption');
|
|
if (bodyCopy && bodyCopy.status !== 'approved') waitingOn.push('Copy');
|
|
if (design && design.status !== 'approved') waitingOn.push('Design');
|
|
if (video && video.status !== 'approved') waitingOn.push('Video');
|
|
|
|
const hasPieces = caption || bodyCopy || design || video;
|
|
const piecesReady = hasPieces && waitingOn.length === 0;
|
|
|
|
// Get translation texts for languages preview
|
|
const getTexts = async (translationId) => {
|
|
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 [captionTexts, bodyTexts] = await Promise.all([
|
|
caption ? getTexts(caption.Id) : [],
|
|
bodyCopy ? getTexts(bodyCopy.Id) : [],
|
|
]);
|
|
|
|
// Get first attachment for design/video thumbnail
|
|
const getFirstAttachment = async (artefactId) => {
|
|
try {
|
|
const versions = await nocodb.list('ArtefactVersions', { where: `(artefact_id,eq,${artefactId})`, sort: '-version_number', limit: 1 });
|
|
if (versions.length === 0) return null;
|
|
const attachments = await nocodb.list('ArtefactAttachments', { where: `(version_id,eq,${versions[0].Id})`, limit: 1 });
|
|
if (attachments.length === 0) return null;
|
|
const att = attachments[0];
|
|
return att.drive_url || (att.filename ? `/api/uploads/${att.filename}` : null);
|
|
} catch { return null; }
|
|
};
|
|
const [designThumb, videoThumb] = await Promise.all([
|
|
design ? (design.thumbnail_url || getFirstAttachment(design.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 {
|
|
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,
|
|
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,
|
|
pieces_ready: piecesReady,
|
|
waiting_on: waitingOn,
|
|
stage: post.stage || 'copy',
|
|
};
|
|
}
|
|
|
|
function computeStage(composition) {
|
|
const { caption, body_copy, design, video, pieces_ready } = composition;
|
|
if (pieces_ready) return 'post';
|
|
if (design || video) return 'design';
|
|
if (caption || body_copy) return 'translate';
|
|
return 'copy';
|
|
}
|
|
|
|
module.exports = { getPostComposition, computeStage };
|