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:
fahed
2026-02-19 11:35:42 +03:00
parent e76be78498
commit 4522edeea8
2207 changed files with 3767 additions and 831225 deletions

View File

@@ -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() {