feat: post approval workflow, i18n completion, and multiple fixes
All checks were successful
Deploy / deploy (push) Successful in 11s

- Add approval process to posts (approver multi-select, rejected status column)
- Reorganize PostDetailPanel into Content, Scheduling, Approval sections
- Fix save button visibility: move to fixed footer via SlidePanel footer prop
- Change date picker from datetime-local to date-only
- Complete Arabic translations across all panels (Header, Issues, Artefacts)
- Fix artefact versioning to start empty (copyFromPrevious defaults to false)
- Separate media uploads by type (image, audio, video) in PostDetailPanel
- Fix team membership save when editing own profile as superadmin
- Server: add approver_ids column to Posts, enrich GET/POST/PATCH responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-05 14:17:16 +03:00
parent daf2404bda
commit 82236ecffa
12 changed files with 882 additions and 309 deletions

View File

@@ -462,6 +462,7 @@ const TEXT_COLUMNS = {
Comments: [{ name: 'version_number', uidt: 'Number' }],
Issues: [{ name: 'thumbnail', uidt: 'SingleLineText' }],
Artefacts: [{ name: 'approver_ids', uidt: 'SingleLineText' }],
Posts: [{ name: 'approver_ids', uidt: 'SingleLineText' }],
};
async function ensureTextColumns() {
@@ -1161,6 +1162,11 @@ app.get('/api/posts', requireAuth, async (req, res) => {
if (p.assigned_to_id) userIds.add(p.assigned_to_id);
if (p.created_by_user_id) userIds.add(p.created_by_user_id);
if (p.campaign_id) campaignIds.add(p.campaign_id);
if (p.approver_ids) {
for (const id of p.approver_ids.split(',').map(s => s.trim()).filter(Boolean)) {
userIds.add(Number(id));
}
}
}
const names = await batchResolveNames({
brand: { table: 'Brands', ids: [...brandIds] },
@@ -1168,18 +1174,22 @@ app.get('/api/posts', requireAuth, async (req, res) => {
campaign: { table: 'Campaigns', ids: [...campaignIds] },
});
res.json(filtered.map(p => ({
...p,
brand_id: p.brand_id,
assigned_to: p.assigned_to_id,
campaign_id: p.campaign_id,
created_by_user_id: p.created_by_user_id,
brand_name: names[`brand:${p.brand_id}`] || null,
assigned_name: names[`user:${p.assigned_to_id}`] || null,
campaign_name: names[`campaign:${p.campaign_id}`] || null,
creator_user_name: names[`user:${p.created_by_user_id}`] || null,
thumbnail_url: thumbMap[p.Id] || null,
})));
res.json(filtered.map(p => {
const approverIdList = p.approver_ids ? p.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
return {
...p,
brand_id: p.brand_id,
assigned_to: p.assigned_to_id,
campaign_id: p.campaign_id,
created_by_user_id: p.created_by_user_id,
brand_name: names[`brand:${p.brand_id}`] || null,
assigned_name: names[`user:${p.assigned_to_id}`] || null,
campaign_name: names[`campaign:${p.campaign_id}`] || null,
creator_user_name: names[`user:${p.created_by_user_id}`] || null,
thumbnail_url: thumbMap[p.Id] || null,
approvers: approverIdList.map(id => ({ id: Number(id), name: names[`user:${Number(id)}`] || null })),
};
}));
} catch (err) {
console.error('GET /posts error:', err);
res.status(500).json({ error: 'Failed to load posts' });
@@ -1187,7 +1197,7 @@ app.get('/api/posts', requireAuth, async (req, res) => {
});
app.post('/api/posts', requireAuth, async (req, res) => {
const { title, description, brand_id, assigned_to, status, platform, platforms, content_type, scheduled_date, notes, campaign_id } = req.body;
const { title, description, brand_id, assigned_to, status, platform, platforms, content_type, scheduled_date, notes, campaign_id, approver_ids } = req.body;
if (!title) return res.status(400).json({ error: 'Title is required' });
const platformsArr = platforms || (platform ? [platform] : []);
@@ -1204,10 +1214,16 @@ app.post('/api/posts', requireAuth, async (req, res) => {
brand_id: brand_id ? Number(brand_id) : null,
assigned_to_id: assigned_to ? Number(assigned_to) : null,
campaign_id: campaign_id ? Number(campaign_id) : null,
approver_ids: approver_ids || null,
created_by_user_id: req.session.userId,
});
const post = await nocodb.get('Posts', created.Id);
const approverIdList = post.approver_ids ? post.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
const approverNames = {};
for (const id of approverIdList) {
approverNames[id] = await getRecordName('Users', Number(id));
}
res.status(201).json({
...post,
assigned_to: post.assigned_to_id,
@@ -1215,6 +1231,7 @@ app.post('/api/posts', requireAuth, async (req, res) => {
assigned_name: await getRecordName('Users', post.assigned_to_id),
campaign_name: await getRecordName('Campaigns', post.campaign_id),
creator_user_name: await getRecordName('Users', post.created_by_user_id),
approvers: approverIdList.map(id => ({ id: Number(id), name: approverNames[id] || null })),
});
} catch (err) {
console.error('Create post error:', err);
@@ -1260,6 +1277,7 @@ app.patch('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin
if (req.body.brand_id !== undefined) data.brand_id = req.body.brand_id ? Number(req.body.brand_id) : null;
if (req.body.assigned_to !== undefined) data.assigned_to_id = req.body.assigned_to ? Number(req.body.assigned_to) : null;
if (req.body.campaign_id !== undefined) data.campaign_id = req.body.campaign_id ? Number(req.body.campaign_id) : null;
if (req.body.approver_ids !== undefined) data.approver_ids = req.body.approver_ids || null;
// Publish validation
if (req.body.status === 'published') {
@@ -1279,6 +1297,11 @@ app.patch('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin
await nocodb.update('Posts', id, data);
const post = await nocodb.get('Posts', id);
const approverIdList = post.approver_ids ? post.approver_ids.split(',').map(s => s.trim()).filter(Boolean) : [];
const approverNames = {};
for (const aid of approverIdList) {
approverNames[aid] = await getRecordName('Users', Number(aid));
}
res.json({
...post,
assigned_to: post.assigned_to_id,
@@ -1286,6 +1309,7 @@ app.patch('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin
assigned_name: await getRecordName('Users', post.assigned_to_id),
campaign_name: await getRecordName('Campaigns', post.campaign_id),
creator_user_name: await getRecordName('Users', post.created_by_user_id),
approvers: approverIdList.map(id => ({ id: Number(id), name: approverNames[id] || null })),
});
} catch (err) {
console.error('Update post error:', err);