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
+38 -18
View File
@@ -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 = [];