feat: slide panels, task calendar, team management, project editing, collapsible sections
- Add SlidePanel, TaskDetailPanel, PostDetailPanel, TeamPanel, TeamMemberPanel - Add ProjectEditPanel, CollapsibleSection, DatePresetPicker, TaskCalendarView - Update App, AuthContext, i18n (ar/en), PostProduction, ProjectDetail, Projects - Update Settings, Tasks, Team pages - Update InteractiveTimeline, MemberCard, ProjectCard, TaskCard components - Update server API utilities - Remove tracked server/node_modules (now properly gitignored)
This commit is contained in:
@@ -64,7 +64,17 @@ app.use(session({
|
||||
// Serve uploaded files
|
||||
app.use('/api/uploads', express.static(uploadsDir));
|
||||
|
||||
// Multer config
|
||||
// ─── APP SETTINGS (persisted to JSON) ────────────────────────────
|
||||
const settingsPath = path.join(__dirname, 'app-settings.json');
|
||||
const defaultSettings = { uploadMaxSizeMB: 50 };
|
||||
function loadSettings() {
|
||||
try { return { ...defaultSettings, ...JSON.parse(fs.readFileSync(settingsPath, 'utf8')) }; }
|
||||
catch { return { ...defaultSettings }; }
|
||||
}
|
||||
function saveSettings(s) { fs.writeFileSync(settingsPath, JSON.stringify(s, null, 2)); }
|
||||
let appSettings = loadSettings();
|
||||
|
||||
// Multer config — dynamic size limit
|
||||
const decodeOriginalName = (name) => Buffer.from(name, 'latin1').toString('utf8');
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, uploadsDir),
|
||||
@@ -74,7 +84,22 @@ const storage = multer.diskStorage({
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } });
|
||||
// Create upload middleware dynamically so it reads current limit
|
||||
function getUpload() {
|
||||
return multer({ storage, limits: { fileSize: appSettings.uploadMaxSizeMB * 1024 * 1024 } });
|
||||
}
|
||||
// Wrapper that creates a fresh multer instance per request
|
||||
const dynamicUpload = (fieldName) => (req, res, next) => {
|
||||
getUpload().single(fieldName)(req, res, (err) => {
|
||||
if (err) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(413).json({ error: `File too large. Maximum size is ${appSettings.uploadMaxSizeMB} MB.`, code: 'FILE_TOO_LARGE', maxSizeMB: appSettings.uploadMaxSizeMB });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// ─── AUTH MIDDLEWARE ─────────────────────────────────────────────
|
||||
|
||||
@@ -745,7 +770,7 @@ app.patch('/api/brands/:id', requireAuth, requireRole('superadmin', 'manager'),
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/brands/:id/logo', requireAuth, requireRole('superadmin', 'manager'), upload.single('file'), async (req, res) => {
|
||||
app.post('/api/brands/:id/logo', requireAuth, requireRole('superadmin', 'manager'), dynamicUpload('file'), async (req, res) => {
|
||||
try {
|
||||
const existing = await nocodb.get('Brands', req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Brand not found' });
|
||||
@@ -978,7 +1003,7 @@ app.get('/api/posts/:id/attachments', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/posts/:id/attachments', requireAuth, upload.single('file'), async (req, res) => {
|
||||
app.post('/api/posts/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
|
||||
try {
|
||||
@@ -1121,7 +1146,7 @@ app.get('/api/assets', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/assets/upload', requireAuth, upload.single('file'), async (req, res) => {
|
||||
app.post('/api/assets/upload', requireAuth, dynamicUpload('file'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
|
||||
try {
|
||||
@@ -1851,7 +1876,7 @@ app.delete('/api/projects/:id', requireAuth, requireRole('superadmin', 'manager'
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/projects/:id/thumbnail', requireAuth, requireRole('superadmin', 'manager'), upload.single('file'), async (req, res) => {
|
||||
app.post('/api/projects/:id/thumbnail', requireAuth, requireRole('superadmin', 'manager'), dynamicUpload('file'), async (req, res) => {
|
||||
try {
|
||||
const existing = await nocodb.get('Projects', req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Project not found' });
|
||||
@@ -2088,7 +2113,7 @@ app.get('/api/tasks/:id/attachments', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/tasks/:id/attachments', requireAuth, upload.single('file'), async (req, res) => {
|
||||
app.post('/api/tasks/:id/attachments', requireAuth, dynamicUpload('file'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
|
||||
try {
|
||||
@@ -2515,6 +2540,25 @@ process.on('unhandledRejection', (err) => {
|
||||
console.error('[UNHANDLED REJECTION]', err);
|
||||
});
|
||||
|
||||
// ─── APP SETTINGS API ───────────────────────────────────────────
|
||||
|
||||
app.get('/api/settings/app', requireAuth, (req, res) => {
|
||||
res.json(appSettings);
|
||||
});
|
||||
|
||||
app.patch('/api/settings/app', requireAuth, requireRole('superadmin'), (req, res) => {
|
||||
const { uploadMaxSizeMB } = req.body;
|
||||
if (uploadMaxSizeMB !== undefined) {
|
||||
const val = Number(uploadMaxSizeMB);
|
||||
if (isNaN(val) || val < 1 || val > 500) {
|
||||
return res.status(400).json({ error: 'uploadMaxSizeMB must be between 1 and 500' });
|
||||
}
|
||||
appSettings.uploadMaxSizeMB = val;
|
||||
}
|
||||
saveSettings(appSettings);
|
||||
res.json(appSettings);
|
||||
});
|
||||
|
||||
// ─── START SERVER ───────────────────────────────────────────────
|
||||
|
||||
async function startServer() {
|
||||
|
||||
Reference in New Issue
Block a user