feat: bulk delete, team dispatch, calendar views, timeline colors
All checks were successful
Deploy / deploy (push) Successful in 11s
All checks were successful
Deploy / deploy (push) Successful in 11s
- Multi-select bulk delete in all 5 list views (Artefacts, Posts, Tasks, Issues, Assets) with cascade deletes and confirmation modals - Team-based issue dispatch: team picker on public issue form, team filter on Issues page, copy public link from Team page and Issues header, team assignment in IssueDetailPanel - Month/Week toggle on PostCalendar and TaskCalendarView - Month/Week/Day zoom on project and campaign timelines (InteractiveTimeline) and ProjectDetail GanttView, with Month as default - Custom timeline bar colors: clickable color dot with 12-color palette popover on project, campaign, and task timeline bars - Artefacts default view changed to list - BulkSelectBar reusable component - i18n keys for all new features (en + ar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
30
server/config.js
Normal file
30
server/config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// server/config.js
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 3001;
|
||||
const UPLOADS_DIR = path.join(__dirname, 'uploads');
|
||||
const SETTINGS_PATH = path.join(__dirname, 'app-settings.json');
|
||||
|
||||
const DEFAULTS = {
|
||||
uploadMaxSizeMB: 50,
|
||||
cacheTTLMs: 60000,
|
||||
sessionMaxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
tokenExpiryDays: 7,
|
||||
};
|
||||
|
||||
const QUERY_LIMITS = {
|
||||
small: 200,
|
||||
medium: 500,
|
||||
large: 1000,
|
||||
max: 5000,
|
||||
};
|
||||
|
||||
const ALL_MODULES = ['marketing', 'projects', 'finance'];
|
||||
|
||||
// NocoDB table name mapping for ownership checks
|
||||
const TABLE_NAME_MAP = { posts: 'Posts', tasks: 'Tasks', campaigns: 'Campaigns', projects: 'Projects' };
|
||||
|
||||
// Entity types allowed for comments
|
||||
const COMMENT_ENTITY_TYPES = new Set(['post', 'task', 'project', 'campaign', 'asset']);
|
||||
|
||||
module.exports = { PORT, UPLOADS_DIR, SETTINGS_PATH, DEFAULTS, QUERY_LIMITS, ALL_MODULES, TABLE_NAME_MAP, COMMENT_ENTITY_TYPES };
|
||||
99
server/helpers.js
Normal file
99
server/helpers.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// server/helpers.js
|
||||
const nocodb = require('./nocodb');
|
||||
const { DEFAULTS } = require('./config');
|
||||
|
||||
// Name lookup cache
|
||||
const _nameCache = {};
|
||||
|
||||
// Clear cache periodically
|
||||
setInterval(() => { Object.keys(_nameCache).forEach(k => delete _nameCache[k]); }, DEFAULTS.cacheTTLMs);
|
||||
|
||||
// Get a single record's display name
|
||||
async function getRecordName(table, id) {
|
||||
if (!id) return null;
|
||||
const key = `${table}:${id}`;
|
||||
if (_nameCache[key] !== undefined) return _nameCache[key];
|
||||
try {
|
||||
const r = await nocodb.get(table, id);
|
||||
const name = r?.name || r?.title || r?.Name || null;
|
||||
_nameCache[key] = name;
|
||||
return name;
|
||||
} catch {
|
||||
_nameCache[key] = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch resolve names for multiple IDs across tables
|
||||
// Usage: await batchResolveNames({ brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } })
|
||||
// Returns: { 'brand:1': 'BrandA', 'user:4': 'Alice', ... }
|
||||
async function batchResolveNames(groups) {
|
||||
// groups is an object like: { brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } }
|
||||
const names = {};
|
||||
const fetches = [];
|
||||
|
||||
for (const [prefix, { table, ids }] of Object.entries(groups)) {
|
||||
const uniqueIds = [...new Set(ids.filter(Boolean))];
|
||||
for (const id of uniqueIds) {
|
||||
const key = `${prefix}:${id}`;
|
||||
if (_nameCache[`${table}:${id}`] !== undefined) {
|
||||
names[key] = _nameCache[`${table}:${id}`];
|
||||
} else {
|
||||
fetches.push(
|
||||
getRecordName(table, id).then(name => { names[key] = name; })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(fetches);
|
||||
return names;
|
||||
}
|
||||
|
||||
// Parse comma-separated approver IDs
|
||||
function parseApproverIds(str) {
|
||||
if (!str) return [];
|
||||
return str.split(',').map(s => s.trim()).filter(Boolean).map(Number);
|
||||
}
|
||||
|
||||
// Safely parse JSON with fallback
|
||||
function safeJsonParse(str, fallback = null) {
|
||||
if (!str || typeof str !== 'string') return fallback;
|
||||
try { return JSON.parse(str); } catch { return fallback; }
|
||||
}
|
||||
|
||||
// Pick allowed fields from request body
|
||||
function pickBodyFields(body, fields) {
|
||||
const data = {};
|
||||
for (const f of fields) {
|
||||
if (body[f] !== undefined) data[f] = body[f];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Sanitize a value for use in NocoDB WHERE clauses
|
||||
// Prevents injection by removing NocoDB query operators
|
||||
function sanitizeWhereValue(val) {
|
||||
if (val === null || val === undefined) return '';
|
||||
const str = String(val);
|
||||
// Remove characters that could manipulate NocoDB query syntax
|
||||
return str.replace(/[~(),$]/g, '');
|
||||
}
|
||||
|
||||
// Build user modules list from user record
|
||||
function getUserModules(user, allModules) {
|
||||
if (user.role === 'superadmin') return allModules;
|
||||
if (user.modules) return safeJsonParse(user.modules, allModules);
|
||||
return allModules;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRecordName,
|
||||
batchResolveNames,
|
||||
parseApproverIds,
|
||||
safeJsonParse,
|
||||
pickBodyFields,
|
||||
sanitizeWhereValue,
|
||||
getUserModules,
|
||||
_nameCache,
|
||||
};
|
||||
599
server/server.js
599
server/server.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user