From c9f7fbb5222bbecaf5403f56a90b001e16fba908 Mon Sep 17 00:00:00 2001 From: Antonis Date: Mon, 29 Sep 2025 16:03:46 +0300 Subject: [PATCH] Fix notes.js & areas.js UID usage and remove IDs. (#355) * Add logging placeholder functions, fix notes.js uids * Fix areas.js uids and remove ids * Add UIDs to inbox items. Includes migration. * id -> uid for task-events.js --------- Co-authored-by: antanst <> --- backend/app.js | 10 +- .../20250924000001-add-uid-to-inbox-items.js | 83 +++++++++++ backend/models/inbox_item.js | 7 + backend/routes/areas.js | 61 +++----- backend/routes/inbox.js | 130 ++++++++++------- backend/routes/notes.js | 138 +++++++----------- backend/routes/tags.js | 11 +- backend/routes/task-events.js | 72 ++++++--- backend/services/logService.js | 9 ++ backend/tests/integration/areas.test.js | 70 ++++++--- .../tests/integration/inbox-no-tags.test.js | 62 ++++---- backend/tests/integration/inbox.test.js | 91 ++++++++---- backend/tests/integration/notes.test.js | 28 ++-- e2e/bin/run-e2e.sh | 2 +- frontend/Layout.tsx | 12 +- frontend/components/Area/AreaModal.tsx | 34 ++--- frontend/components/Areas.tsx | 34 ++--- frontend/components/Inbox/InboxItemDetail.tsx | 38 ++--- frontend/components/Inbox/InboxItems.tsx | 63 ++++---- frontend/components/Note/NoteDetails.tsx | 10 +- frontend/components/Note/NoteModal.tsx | 24 +-- frontend/components/Notes.tsx | 18 +-- .../components/Project/ProjectDetails.tsx | 9 +- frontend/components/Shared/AreaDropdown.tsx | 4 +- frontend/components/Shared/NoteCard.tsx | 21 +-- frontend/components/Tag/TagDetails.tsx | 14 +- frontend/components/Task/TaskDetails.tsx | 2 +- frontend/components/Task/TaskTimeline.tsx | 12 +- frontend/components/Task/TimelinePanel.tsx | 6 +- frontend/entities/InboxItem.ts | 1 + frontend/entities/Note.ts | 3 +- frontend/entities/Project.ts | 1 + frontend/store/useStore.ts | 22 ++- frontend/utils/areasService.ts | 8 +- frontend/utils/inboxService.ts | 36 ++--- frontend/utils/noteDeleteUtils.ts | 18 ++- frontend/utils/notesService.ts | 27 +++- frontend/utils/taskEventService.ts | 23 +-- 38 files changed, 730 insertions(+), 484 deletions(-) create mode 100644 backend/migrations/20250924000001-add-uid-to-inbox-items.js create mode 100644 backend/services/logService.js diff --git a/backend/app.js b/backend/app.js index 949f3bc..d3e6fce 100644 --- a/backend/app.js +++ b/backend/app.js @@ -90,6 +90,7 @@ app.use('/api/uploads', express.static(config.uploadPath)); // Authentication middleware const { requireAuth } = require('./middleware/auth'); +const { logError } = require('./services/logService'); // Health check (before auth middleware) - ensure it's completely bypassed app.get('/api/health', (req, res) => { @@ -134,12 +135,15 @@ app.get('*', (req, res) => { } }); -// Error handling +// Error handling fallback. +// We shouldn't be here normally! +// Each route should properly handle +// and log its own errors. app.use((err, req, res, next) => { - console.error(err.stack); + logError(err); res.status(500).json({ error: 'Internal Server Error', - message: err.message, + // message: err.message, }); }); diff --git a/backend/migrations/20250924000001-add-uid-to-inbox-items.js b/backend/migrations/20250924000001-add-uid-to-inbox-items.js new file mode 100644 index 0000000..17d5c39 --- /dev/null +++ b/backend/migrations/20250924000001-add-uid-to-inbox-items.js @@ -0,0 +1,83 @@ +'use strict'; + +const { uid } = require('../utils/uid'); +const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils'); + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + // Temporarily disable foreign key constraints for SQLite + await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF'); + + try { + // 1. Add uid column to inbox_items table + await safeAddColumns(queryInterface, 'inbox_items', [ + { + name: 'uid', + definition: { + type: Sequelize.STRING, + allowNull: true, // Initially allow null during population + }, + }, + ]); + + // 2. Populate uid values for existing inbox items + const records = await queryInterface.sequelize.query( + 'SELECT id FROM inbox_items WHERE uid IS NULL', + { type: Sequelize.QueryTypes.SELECT } + ); + + // Generate uid for each record + for (const record of records) { + const uniqueId = uid(); + await queryInterface.sequelize.query( + 'UPDATE inbox_items SET uid = ? WHERE id = ?', + { + replacements: [uniqueId, record.id], + type: Sequelize.QueryTypes.UPDATE, + } + ); + } + + // 3. Make uid column not null and unique + await queryInterface.changeColumn('inbox_items', 'uid', { + type: Sequelize.STRING, + allowNull: false, + unique: true, + }); + + // 4. Add unique index for performance + await safeAddIndex(queryInterface, 'inbox_items', ['uid'], { + unique: true, + name: 'inbox_items_uid_unique_index', + }); + } finally { + // Re-enable foreign key constraints + await queryInterface.sequelize.query('PRAGMA foreign_keys = ON'); + } + }, + + async down(queryInterface, Sequelize) { + try { + // Remove unique index + await queryInterface.removeIndex( + 'inbox_items', + 'inbox_items_uid_unique_index' + ); + } catch (error) { + console.log( + 'inbox_items_uid_unique_index not found, skipping removal' + ); + } + + try { + // Remove uid column + await queryInterface.removeColumn('inbox_items', 'uid'); + } catch (error) { + console.log( + 'Error removing uid column from inbox_items:', + error.message + ); + } + }, +}; diff --git a/backend/models/inbox_item.js b/backend/models/inbox_item.js index dfa82f7..18475ff 100644 --- a/backend/models/inbox_item.js +++ b/backend/models/inbox_item.js @@ -1,4 +1,5 @@ const { DataTypes } = require('sequelize'); +const { uid } = require('../utils/uid'); module.exports = (sequelize) => { const InboxItem = sequelize.define( @@ -9,6 +10,12 @@ module.exports = (sequelize) => { primaryKey: true, autoIncrement: true, }, + uid: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + defaultValue: uid, + }, content: { type: DataTypes.STRING, allowNull: false, diff --git a/backend/routes/areas.js b/backend/routes/areas.js index fd1d13c..2ed6c82 100644 --- a/backend/routes/areas.js +++ b/backend/routes/areas.js @@ -1,17 +1,15 @@ const express = require('express'); const { Area } = require('../models'); +const { isValidUid } = require('../utils/slug-utils'); +const _ = require('lodash'); const router = express.Router(); // GET /api/areas router.get('/areas', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const areas = await Area.findAll({ where: { user_id: req.session.userId }, - attributes: ['id', 'uid', 'name', 'description'], + attributes: ['uid', 'name', 'description'], order: [['name', 'ASC']], }); @@ -22,18 +20,17 @@ router.get('/areas', async (req, res) => { } }); -// GET /api/areas/:id -router.get('/areas/:id', async (req, res) => { +// GET /api/areas/:uid +router.get('/areas/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); const area = await Area.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, + attributes: ['uid', 'name', 'description'], }); - if (!area) { + if (_.isEmpty(area)) { return res.status(404).json({ error: "Area not found or doesn't belong to the current user.", }); @@ -49,13 +46,9 @@ router.get('/areas/:id', async (req, res) => { // POST /api/areas router.post('/areas', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const { name, description } = req.body; - if (!name || !name.trim()) { + if (!name || _.isEmpty(name.trim())) { return res.status(400).json({ error: 'Area name is required.' }); } @@ -65,10 +58,7 @@ router.post('/areas', async (req, res) => { user_id: req.session.userId, }); - res.status(201).json({ - ...area.toJSON(), - uid: area.uid, // Explicitly include uid - }); + res.status(201).json(_.pick(area, ['uid', 'name', 'description'])); } catch (error) { console.error('Error creating area:', error); res.status(400).json({ @@ -80,15 +70,13 @@ router.post('/areas', async (req, res) => { } }); -// PATCH /api/areas/:id -router.patch('/areas/:id', async (req, res) => { +// PATCH /api/areas/:uid +router.patch('/areas/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); const area = await Area.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!area) { @@ -102,7 +90,7 @@ router.patch('/areas/:id', async (req, res) => { if (description !== undefined) updateData.description = description; await area.update(updateData); - res.json(area); + res.json(_.pick(area, ['uid', 'name', 'description'])); } catch (error) { console.error('Error updating area:', error); res.status(400).json({ @@ -114,15 +102,14 @@ router.patch('/areas/:id', async (req, res) => { } }); -// DELETE /api/areas/:id -router.delete('/areas/:id', async (req, res) => { +// DELETE /api/areas/:uid +router.delete('/areas/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); const area = await Area.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!area) { @@ -130,7 +117,7 @@ router.delete('/areas/:id', async (req, res) => { } await area.destroy(); - res.status(204).send(); + return res.status(204).send(); } catch (error) { console.error('Error deleting area:', error); res.status(400).json({ diff --git a/backend/routes/inbox.js b/backend/routes/inbox.js index ee6fdc2..ab1d349 100644 --- a/backend/routes/inbox.js +++ b/backend/routes/inbox.js @@ -1,23 +1,22 @@ const express = require('express'); const { InboxItem } = require('../models'); const { processInboxItem } = require('../services/inboxProcessingService'); +const { isValidUid } = require('../utils/slug-utils'); +const _ = require('lodash'); +const { logError } = require('../services/logService'); const router = express.Router(); // GET /api/inbox router.get('/inbox', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - // Check if pagination parameters are provided const hasPagination = - req.query.limit !== undefined || req.query.offset !== undefined; + !_.isEmpty(req.query.limit) || !_.isEmpty(req.query.offset); if (hasPagination) { // Parse pagination parameters - const limit = parseInt(req.query.limit) || 20; // Default to 20 items - const offset = parseInt(req.query.offset) || 0; + const limit = parseInt(req.query.limit, 10) || 20; // Default to 20 items + const offset = parseInt(req.query.offset, 10) || 0; // Get total count for pagination info const totalCount = await InboxItem.count({ @@ -59,7 +58,7 @@ router.get('/inbox', async (req, res) => { res.json(items); } } catch (error) { - console.error('Error fetching inbox items:', error); + logError('Error fetching inbox items:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -67,13 +66,9 @@ router.get('/inbox', async (req, res) => { // POST /api/inbox router.post('/inbox', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const { content, source } = req.body; - if (!content || !content.trim()) { + if (!content || _.isEmpty(content.trim())) { return res.status(400).json({ error: 'Content is required' }); } @@ -86,9 +81,18 @@ router.post('/inbox', async (req, res) => { user_id: req.session.userId, }); - res.status(201).json(item); + res.status(201).json( + _.pick(item, [ + 'uid', + 'content', + 'status', + 'source', + 'created_at', + 'updated_at', + ]) + ); } catch (error) { - console.error('Error creating inbox item:', error); + logError('Error creating inbox item:', error); res.status(400).json({ error: 'There was a problem creating the inbox item.', details: error.errors @@ -98,53 +102,70 @@ router.post('/inbox', async (req, res) => { } }); -// GET /api/inbox/:id -router.get('/inbox/:id', async (req, res) => { +// GET /api/inbox/:uid +router.get('/inbox/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); + if (!isValidUid(req.params.uid)) { + return res.status(400).json({ error: 'Invalid UID' }); } const item = await InboxItem.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, + attributes: [ + 'uid', + 'content', + 'status', + 'source', + 'created_at', + 'updated_at', + ], }); - if (!item) { + if (_.isEmpty(item)) { return res.status(404).json({ error: 'Inbox item not found.' }); } res.json(item); } catch (error) { - console.error('Error fetching inbox item:', error); + logError('Error fetching inbox item:', error); res.status(500).json({ error: 'Internal server error' }); } }); -// PATCH /api/inbox/:id -router.patch('/inbox/:id', async (req, res) => { +// PATCH /api/inbox/:uid +router.patch('/inbox/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); + if (!isValidUid(req.params.uid)) { + return res.status(400).json({ error: 'Invalid UID' }); } const item = await InboxItem.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); - if (!item) { + if (_.isEmpty(item)) { return res.status(404).json({ error: 'Inbox item not found.' }); } const { content, status } = req.body; const updateData = {}; - if (content !== undefined) updateData.content = content; - if (status !== undefined) updateData.status = status; + if (content != null) updateData.content = content; + if (status != null) updateData.status = status; await item.update(updateData); - res.json(item); + res.json( + _.pick(item, [ + 'uid', + 'content', + 'status', + 'source', + 'created_at', + 'updated_at', + ]) + ); } catch (error) { - console.error('Error updating inbox item:', error); + logError('Error updating inbox item:', error); res.status(400).json({ error: 'There was a problem updating the inbox item.', details: error.errors @@ -154,18 +175,18 @@ router.patch('/inbox/:id', async (req, res) => { } }); -// DELETE /api/inbox/:id -router.delete('/inbox/:id', async (req, res) => { +// DELETE /api/inbox/:uid +router.delete('/inbox/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); + if (!isValidUid(req.params.uid)) { + return res.status(400).json({ error: 'Invalid UID' }); } const item = await InboxItem.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); - if (!item) { + if (_.isEmpty(item)) { return res.status(404).json({ error: 'Inbox item not found.' }); } @@ -173,32 +194,41 @@ router.delete('/inbox/:id', async (req, res) => { await item.update({ status: 'deleted' }); res.json({ message: 'Inbox item successfully deleted' }); } catch (error) { - console.error('Error deleting inbox item:', error); + logError('Error deleting inbox item:', error); res.status(400).json({ error: 'There was a problem deleting the inbox item.', }); } }); -// PATCH /api/inbox/:id/process -router.patch('/inbox/:id/process', async (req, res) => { +// PATCH /api/inbox/:uid/process +router.patch('/inbox/:uid/process', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); + if (!isValidUid(req.params.uid)) { + return res.status(400).json({ error: 'Invalid UID' }); } const item = await InboxItem.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); - if (!item) { + if (_.isEmpty(item)) { return res.status(404).json({ error: 'Inbox item not found.' }); } await item.update({ status: 'processed' }); - res.json(item); + res.json( + _.pick(item, [ + 'uid', + 'content', + 'status', + 'source', + 'created_at', + 'updated_at', + ]) + ); } catch (error) { - console.error('Error processing inbox item:', error); + logError('Error processing inbox item:', error); res.status(400).json({ error: 'There was a problem processing the inbox item.', }); @@ -208,10 +238,6 @@ router.patch('/inbox/:id/process', async (req, res) => { // POST /api/inbox/analyze-text router.post('/inbox/analyze-text', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const { content } = req.body; if (!content || typeof content !== 'string') { @@ -225,7 +251,7 @@ router.post('/inbox/analyze-text', async (req, res) => { res.json(result); } catch (error) { - console.error('Error analyzing inbox text:', error); + logError('Error analyzing inbox text:', error); res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/backend/routes/notes.js b/backend/routes/notes.js index f6b7fd3..d58df82 100644 --- a/backend/routes/notes.js +++ b/backend/routes/notes.js @@ -1,13 +1,15 @@ const express = require('express'); const { Note, Tag, Project, sequelize } = require('../models'); const { Op } = require('sequelize'); -const { extractUidFromSlug } = require('../utils/slug-utils'); +const { extractUidFromSlug, isValidUid } = require('../utils/slug-utils'); const { validateTagName } = require('../services/tagsService'); const router = express.Router(); +const _ = require('lodash'); +const { logError } = require('../services/logService'); // Helper function to update note tags async function updateNoteTags(note, tagsArray, userId) { - if (!tagsArray || tagsArray.length === 0) { + if (_.isEmpty(tagsArray)) { await note.setTags([]); return; } @@ -29,7 +31,6 @@ async function updateNoteTags(note, tagsArray, userId) { } } - // If there are invalid tags, throw an error if (invalidTags.length > 0) { throw new Error( `Invalid tag names: ${invalidTags.map((t) => `"${t.name}" (${t.error})`).join(', ')}` @@ -47,7 +48,7 @@ async function updateNoteTags(note, tagsArray, userId) { ); await note.setTags(tags); } catch (error) { - console.error('Failed to update tags:', error.message); + logError('Failed to update tags:', error.message); throw error; // Re-throw to handle at route level } } @@ -55,10 +56,6 @@ async function updateNoteTags(note, tagsArray, userId) { // GET /api/notes router.get('/notes', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - const orderBy = req.query.order_by || 'title:asc'; const [orderColumn, orderDirection] = orderBy.split(':'); @@ -66,13 +63,13 @@ router.get('/notes', async (req, res) => { let includeClause = [ { model: Tag, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], through: { attributes: [] }, }, { model: Project, required: false, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], }, ]; @@ -91,62 +88,42 @@ router.get('/notes', async (req, res) => { res.json(notes); } catch (error) { - console.error('Error fetching notes:', error); + logError('Error fetching notes:', error); res.status(500).json({ error: 'Internal server error' }); } }); -// GET /api/note/:id (supports both numeric ID and uid-slug) -router.get('/note/:id', async (req, res) => { +// GET /api/note/:uidSlug +router.get('/note/:uidSlug', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - - const identifier = req.params.id; - let whereClause; - - // Check if identifier is numeric (regular ID) or uid-slug - if (/^\d+$/.test(identifier)) { - // It's a numeric ID - whereClause = { - id: parseInt(identifier), - user_id: req.session.userId, - }; - } else { - // It's a uid-slug, extract the uid - const uid = extractUidFromSlug(identifier); - if (!uid) { - return res - .status(400) - .json({ error: 'Invalid note identifier' }); - } - whereClause = { uid: uid, user_id: req.session.userId }; + const uid = extractUidFromSlug(req.params.uidSlug); + if (_.isEmpty(uid)) { + return res.status(400).json({ error: 'Invalid note identifier' }); } const note = await Note.findOne({ - where: whereClause, + where: { uid, user_id: req.session.userId }, include: [ { model: Tag, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], through: { attributes: [] }, }, { model: Project, required: false, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], }, ], }); - if (!note) { + if (_.isEmpty(note)) { return res.status(404).json({ error: 'Note not found.' }); } res.json(note); } catch (error) { - console.error('Error fetching note:', error); + logError('Error fetching note:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -154,11 +131,7 @@ router.get('/note/:id', async (req, res) => { // POST /api/note router.post('/note', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } - - const { title, content, project_id, tags } = req.body; + const { title, content, project_uid, tags } = req.body; const noteAttributes = { title, @@ -167,19 +140,23 @@ router.post('/note', async (req, res) => { }; // Handle project assignment - if (project_id && project_id.toString().trim()) { - const project = await Project.findOne({ - where: { id: project_id, user_id: req.session.userId }, - }); - if (!project) { - return res.status(400).json({ error: 'Invalid project.' }); + if (project_uid !== undefined) { + const projectUid = project_uid.toString().trim(); + if (!_.isEmpty(projectUid)) { + const project = await Project.findOne({ + where: { uid: projectUid, user_id: req.session.userId }, + }); + if (_.isEmpty(project)) { + return res.status(400).json({ error: 'Invalid project.' }); + } + noteAttributes.project_id = project.id; } - noteAttributes.project_id = project_id; } const note = await Note.create(noteAttributes); - // Handle tags - can be array of strings or array of objects with name property + // Handle tags - can be an array of strings + // or array of objects with name property let tagNames = []; if (Array.isArray(tags)) { if (tags.every((t) => typeof t === 'string')) { @@ -196,23 +173,23 @@ router.post('/note', async (req, res) => { include: [ { model: Tag, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], through: { attributes: [] }, }, { model: Project, required: false, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], }, ], }); res.status(201).json({ ...noteWithAssociations.toJSON(), - uid: noteWithAssociations.uid, // Explicitly include uid + uid: noteWithAssociations.uid, }); } catch (error) { - console.error('Error creating note:', error); + logError('Error creating note:', error); res.status(400).json({ error: 'There was a problem creating the note.', details: error.errors @@ -222,37 +199,37 @@ router.post('/note', async (req, res) => { } }); -// PATCH /api/note/:id -router.patch('/note/:id', async (req, res) => { +// PATCH /api/note/:uid +router.patch('/note/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); const note = await Note.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!note) { return res.status(404).json({ error: 'Note not found.' }); } - const { title, content, project_id, tags } = req.body; + const { title, content, project_uid, tags } = req.body; const updateData = {}; if (title !== undefined) updateData.title = title; if (content !== undefined) updateData.content = content; // Handle project assignment - if (project_id !== undefined) { - if (project_id && project_id.toString().trim()) { + if (project_uid !== undefined) { + const projectUid = project_uid.toString().trim(); + if (!_.isEmpty(projectUid)) { const project = await Project.findOne({ - where: { id: project_id, user_id: req.session.userId }, + where: { uid: projectUid, user_id: req.session.userId }, }); if (!project) { return res.status(400).json({ error: 'Invalid project.' }); } - updateData.project_id = project_id; + updateData.project_id = project.id; } else { updateData.project_id = null; } @@ -278,20 +255,20 @@ router.patch('/note/:id', async (req, res) => { include: [ { model: Tag, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], through: { attributes: [] }, }, { model: Project, required: false, - attributes: ['id', 'name', 'uid'], + attributes: ['name', 'uid'], }, ], }); res.json(noteWithAssociations); } catch (error) { - console.error('Error updating note:', error); + logError('Error updating note:', error); res.status(400).json({ error: 'There was a problem updating the note.', details: error.errors @@ -301,15 +278,14 @@ router.patch('/note/:id', async (req, res) => { } }); -// DELETE /api/note/:id -router.delete('/note/:id', async (req, res) => { +// DELETE /api/note/:uid +router.delete('/note/:uid', async (req, res) => { try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Authentication required' }); - } + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); const note = await Note.findOne({ - where: { id: req.params.id, user_id: req.session.userId }, + where: { uid: req.params.uid, user_id: req.session.userId }, }); if (!note) { @@ -319,10 +295,8 @@ router.delete('/note/:id', async (req, res) => { await note.destroy(); res.json({ message: 'Note deleted successfully.' }); } catch (error) { - console.error('Error deleting note:', error); - res.status(400).json({ - error: 'There was a problem deleting the note.', - }); + logError('Error deleting note:', error); + res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/backend/routes/tags.js b/backend/routes/tags.js index 778b0e0..32cce6a 100644 --- a/backend/routes/tags.js +++ b/backend/routes/tags.js @@ -5,6 +5,7 @@ const { validateTagName } = require('../services/tagsService'); const router = express.Router(); const _ = require('lodash'); const { Op } = require('sequelize'); +const { logError } = require('../services/logService'); // GET /api/tags router.get('/tags', async (req, res) => { @@ -16,7 +17,7 @@ router.get('/tags', async (req, res) => { }); res.json(tags); } catch (error) { - console.error('Error fetching tags:', error); + logError('Error fetching tags:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -47,7 +48,7 @@ router.get('/tag', async (req, res) => { res.json(tag); } catch (error) { - console.error('Error fetching tag:', error); + logError('Error fetching tag:', error); res.status(500).json({ error: 'Internal server error' }); } }); @@ -72,7 +73,7 @@ router.post('/tag', async (req, res) => { name: tag.name, }); } catch (error) { - console.error('Error creating tag:', error); + logError('Error creating tag:', error); res.status(400).json({ error: 'There was a problem creating the tag.', }); @@ -111,7 +112,7 @@ router.patch('/tag/:identifier', async (req, res) => { name: tag.name, }); } catch (error) { - console.error('Error updating tag:', error); + logError('Error updating tag:', error); res.status(400).json({ error: 'There was a problem updating the tag.', }); @@ -166,7 +167,7 @@ router.delete('/tag/:identifier', async (req, res) => { res.json({ message: 'Tag successfully deleted' }); } catch (error) { await transaction.rollback(); - console.error('Error deleting tag:', error); + logError('Error deleting tag:', error); res.status(400).json({ error: 'There was a problem deleting the tag.', }); diff --git a/backend/routes/task-events.js b/backend/routes/task-events.js index 839da11..16a26df 100644 --- a/backend/routes/task-events.js +++ b/backend/routes/task-events.js @@ -1,34 +1,53 @@ const express = require('express'); -const { TaskEvent } = require('../models'); +const { Task, TaskEvent } = require('../models'); +const { isValidUid } = require('../utils/slug-utils'); const { getTaskTimeline, getTaskCompletionTime, getUserProductivityMetrics, getTaskActivitySummary, } = require('../services/taskEventService'); +const { logError } = require('../services/logService'); const router = express.Router(); -// GET /api/task/:id/timeline - Get task event timeline -router.get('/task/:id/timeline', async (req, res) => { +// GET /api/task/:uid/timeline - Get task event timeline +router.get('/task/:uid/timeline', async (req, res) => { try { - const timeline = await getTaskTimeline(req.params.id); + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); - // Filter to only show events for tasks owned by the current user - const userTimeline = timeline.filter( - (event) => event.user_id === req.currentUser.id - ); + const task = await Task.findOne({ + where: { uid: req.params.uid, user_id: req.currentUser.id }, + }); - res.json(userTimeline); + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + const timeline = await getTaskTimeline(task.id); + + res.json(timeline); } catch (error) { - console.error('Error fetching task timeline:', error); + logError('Error fetching task timeline:', error); res.status(500).json({ error: 'Failed to fetch task timeline' }); } }); -// GET /api/task/:id/completion-time - Get task completion analytics -router.get('/task/:id/completion-time', async (req, res) => { +// GET /api/task/:uid/completion-time - Get task completion analytics +router.get('/task/:uid/completion-time', async (req, res) => { try { - const completionTime = await getTaskCompletionTime(req.params.id); + if (!isValidUid(req.params.uid)) + return res.status(400).json({ error: 'Invalid UID' }); + + const task = await Task.findOne({ + where: { uid: req.params.uid, user_id: req.currentUser.id }, + }); + + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + const completionTime = await getTaskCompletionTime(task.id); if (!completionTime) { return res @@ -38,7 +57,7 @@ router.get('/task/:id/completion-time', async (req, res) => { res.json(completionTime); } catch (error) { - console.error('Error fetching task completion time:', error); + logError('Error fetching task completion time:', error); res.status(500).json({ error: 'Failed to fetch task completion time' }); } }); @@ -56,7 +75,7 @@ router.get('/user/productivity-metrics', async (req, res) => { res.json(metrics); } catch (error) { - console.error('Error fetching productivity metrics:', error); + logError('Error fetching productivity metrics:', error); res.status(500).json({ error: 'Failed to fetch productivity metrics' }); } }); @@ -80,7 +99,7 @@ router.get('/user/activity-summary', async (req, res) => { res.json(activitySummary); } catch (error) { - console.error('Error fetching activity summary:', error); + logError('Error fetching activity summary:', error); res.status(500).json({ error: 'Failed to fetch activity summary' }); } }); @@ -88,7 +107,7 @@ router.get('/user/activity-summary', async (req, res) => { // GET /api/tasks/completion-analytics - Get completion time analytics for multiple tasks router.get('/tasks/completion-analytics', async (req, res) => { try { - const { limit = 50, offset = 0, projectId } = req.query; + const { limit = 50, offset = 0, projectUid } = req.query; // Get completed tasks for the user const { Task, Project } = require('../models'); @@ -99,8 +118,21 @@ router.get('/tasks/completion-analytics', async (req, res) => { status: 2, // completed }; - if (projectId) { - whereClause.project_id = projectId; + // If projectUid is provided, find the project and filter by its ID + if (projectUid) { + if (!isValidUid(projectUid)) { + return res.status(400).json({ error: 'Invalid project UID' }); + } + + const project = await Project.findOne({ + where: { uid: projectUid, user_id: req.currentUser.id }, + }); + + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + + whereClause.project_id = project.id; } const completedTasks = await Task.findAll({ @@ -163,7 +195,7 @@ router.get('/tasks/completion-analytics', async (req, res) => { summary, }); } catch (error) { - console.error('Error fetching completion analytics:', error); + logError('Error fetching completion analytics:', error); res.status(500).json({ error: 'Failed to fetch completion analytics' }); } }); diff --git a/backend/services/logService.js b/backend/services/logService.js new file mode 100644 index 0000000..69d7696 --- /dev/null +++ b/backend/services/logService.js @@ -0,0 +1,9 @@ +const logError = console.error; +const logInfo = console.log; +const logDebug = console.log; + +module.exports = { + logError, + logInfo, + logDebug, +}; diff --git a/backend/tests/integration/areas.test.js b/backend/tests/integration/areas.test.js index 4741124..c0de85b 100644 --- a/backend/tests/integration/areas.test.js +++ b/backend/tests/integration/areas.test.js @@ -31,7 +31,8 @@ describe('Areas Routes', () => { expect(response.status).toBe(201); expect(response.body.name).toBe(areaData.name); expect(response.body.description).toBe(areaData.description); - expect(response.body.user_id).toBe(user.id); + expect(response.body.uid).toBeDefined(); + expect(typeof response.body.uid).toBe('string'); }); it('should require authentication', async () => { @@ -81,8 +82,8 @@ describe('Areas Routes', () => { expect(response.status).toBe(200); expect(response.body).toHaveLength(2); - expect(response.body.map((a) => a.id)).toContain(area1.id); - expect(response.body.map((a) => a.id)).toContain(area2.id); + expect(response.body.map((a) => a.uid)).toContain(area1.uid); + expect(response.body.map((a) => a.uid)).toContain(area2.uid); }); it('should order areas by name', async () => { @@ -101,7 +102,7 @@ describe('Areas Routes', () => { }); }); - describe('GET /api/areas/:id', () => { + describe('GET /api/areas/:uid', () => { let area; beforeEach(async () => { @@ -112,17 +113,24 @@ describe('Areas Routes', () => { }); }); - it('should get area by id', async () => { - const response = await agent.get(`/api/areas/${area.id}`); + it('should get area by uid', async () => { + const response = await agent.get(`/api/areas/${area.uid}`); expect(response.status).toBe(200); - expect(response.body.id).toBe(area.id); + expect(response.body.uid).toBe(area.uid); expect(response.body.name).toBe(area.name); expect(response.body.description).toBe(area.description); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent.get('/api/areas/invalid-uid'); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent area', async () => { - const response = await agent.get('/api/areas/999999'); + const response = await agent.get('/api/areas/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe( @@ -142,7 +150,7 @@ describe('Areas Routes', () => { user_id: otherUser.id, }); - const response = await agent.get(`/api/areas/${otherArea.id}`); + const response = await agent.get(`/api/areas/${otherArea.uid}`); expect(response.status).toBe(404); expect(response.body.error).toBe( @@ -151,14 +159,14 @@ describe('Areas Routes', () => { }); it('should require authentication', async () => { - const response = await request(app).get(`/api/areas/${area.id}`); + const response = await request(app).get(`/api/areas/${area.uid}`); expect(response.status).toBe(401); expect(response.body.error).toBe('Authentication required'); }); }); - describe('PATCH /api/areas/:id', () => { + describe('PATCH /api/areas/:uid', () => { let area; beforeEach(async () => { @@ -176,7 +184,7 @@ describe('Areas Routes', () => { }; const response = await agent - .patch(`/api/areas/${area.id}`) + .patch(`/api/areas/${area.uid}`) .send(updateData); expect(response.status).toBe(200); @@ -184,9 +192,18 @@ describe('Areas Routes', () => { expect(response.body.description).toBe(updateData.description); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent + .patch('/api/areas/invalid-uid') + .send({ name: 'Updated' }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent area', async () => { const response = await agent - .patch('/api/areas/999999') + .patch('/api/areas/abcd1234efghijk') .send({ name: 'Updated' }); expect(response.status).toBe(404); @@ -206,7 +223,7 @@ describe('Areas Routes', () => { }); const response = await agent - .patch(`/api/areas/${otherArea.id}`) + .patch(`/api/areas/${otherArea.uid}`) .send({ name: 'Updated' }); expect(response.status).toBe(404); @@ -215,7 +232,7 @@ describe('Areas Routes', () => { it('should require authentication', async () => { const response = await request(app) - .patch(`/api/areas/${area.id}`) + .patch(`/api/areas/${area.uid}`) .send({ name: 'Updated' }); expect(response.status).toBe(401); @@ -223,7 +240,7 @@ describe('Areas Routes', () => { }); }); - describe('DELETE /api/areas/:id', () => { + describe('DELETE /api/areas/:uid', () => { let area; beforeEach(async () => { @@ -234,17 +251,26 @@ describe('Areas Routes', () => { }); it('should delete area', async () => { - const response = await agent.delete(`/api/areas/${area.id}`); + const response = await agent.delete(`/api/areas/${area.uid}`); expect(response.status).toBe(204); // Verify area is deleted - const deletedArea = await Area.findByPk(area.id); + const deletedArea = await Area.findOne({ + where: { uid: area.uid }, + }); expect(deletedArea).toBeNull(); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent.delete('/api/areas/invalid-uid'); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent area', async () => { - const response = await agent.delete('/api/areas/999999'); + const response = await agent.delete('/api/areas/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe('Area not found.'); @@ -262,14 +288,16 @@ describe('Areas Routes', () => { user_id: otherUser.id, }); - const response = await agent.delete(`/api/areas/${otherArea.id}`); + const response = await agent.delete(`/api/areas/${otherArea.uid}`); expect(response.status).toBe(404); expect(response.body.error).toBe('Area not found.'); }); it('should require authentication', async () => { - const response = await request(app).delete(`/api/areas/${area.id}`); + const response = await request(app).delete( + `/api/areas/${area.uid}` + ); expect(response.status).toBe(401); expect(response.body.error).toBe('Authentication required'); diff --git a/backend/tests/integration/inbox-no-tags.test.js b/backend/tests/integration/inbox-no-tags.test.js index ada0b32..7e53483 100644 --- a/backend/tests/integration/inbox-no-tags.test.js +++ b/backend/tests/integration/inbox-no-tags.test.js @@ -52,15 +52,16 @@ describe('Inbox Routes - No Tags Scenario', () => { expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBe(2); - expect(response.body.map((item) => item.id)).toContain( - inboxItem1.id + expect(response.body.map((item) => item.uid)).toContain( + inboxItem1.uid ); - expect(response.body.map((item) => item.id)).toContain( - inboxItem2.id + expect(response.body.map((item) => item.uid)).toContain( + inboxItem2.uid ); expect(response.body[0].content).toBeDefined(); expect(response.body[0].status).toBe('added'); - expect(response.body[0].user_id).toBe(user.id); + expect(response.body[0].uid).toBeDefined(); + expect(typeof response.body[0].uid).toBe('string'); }); it('should handle mixed inbox items when no tags exist', async () => { @@ -90,7 +91,7 @@ describe('Inbox Routes - No Tags Scenario', () => { expect(response.status).toBe(200); expect(response.body.length).toBe(1); // Only 'added' items should be returned - expect(response.body[0].id).toBe(addedItem.id); + expect(response.body[0].uid).toBe(addedItem.uid); expect(response.body[0].status).toBe('added'); }); }); @@ -139,7 +140,8 @@ describe('Inbox Routes - No Tags Scenario', () => { expect(response.body.content).toBe(inboxData.content); expect(response.body.source).toBe(inboxData.source); expect(response.body.status).toBe('added'); - expect(response.body.user_id).toBe(user.id); + expect(response.body.uid).toBeDefined(); + expect(typeof response.body.uid).toBe('string'); }); it('should handle multiple inbox items creation when no tags exist', async () => { @@ -164,7 +166,7 @@ describe('Inbox Routes - No Tags Scenario', () => { }); }); - describe('PATCH /api/inbox/:id - No Tags Scenario', () => { + describe('PATCH /api/inbox/:uid - No Tags Scenario', () => { let inboxItem; beforeEach(async () => { @@ -183,7 +185,7 @@ describe('Inbox Routes - No Tags Scenario', () => { }; const response = await agent - .patch(`/api/inbox/${inboxItem.id}`) + .patch(`/api/inbox/${inboxItem.uid}`) .send(updateData); expect(response.status).toBe(200); @@ -192,7 +194,7 @@ describe('Inbox Routes - No Tags Scenario', () => { }); }); - describe('PATCH /api/inbox/:id/process - No Tags Scenario', () => { + describe('PATCH /api/inbox/:uid/process - No Tags Scenario', () => { let inboxItem; beforeEach(async () => { @@ -206,7 +208,7 @@ describe('Inbox Routes - No Tags Scenario', () => { it('should process inbox items when no tags exist', async () => { const response = await agent.patch( - `/api/inbox/${inboxItem.id}/process` + `/api/inbox/${inboxItem.uid}/process` ); expect(response.status).toBe(200); @@ -214,7 +216,7 @@ describe('Inbox Routes - No Tags Scenario', () => { }); }); - describe('DELETE /api/inbox/:id - No Tags Scenario', () => { + describe('DELETE /api/inbox/:uid - No Tags Scenario', () => { let inboxItem; beforeEach(async () => { @@ -227,7 +229,7 @@ describe('Inbox Routes - No Tags Scenario', () => { }); it('should delete inbox items when no tags exist', async () => { - const response = await agent.delete(`/api/inbox/${inboxItem.id}`); + const response = await agent.delete(`/api/inbox/${inboxItem.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe( @@ -235,7 +237,9 @@ describe('Inbox Routes - No Tags Scenario', () => { ); // Verify item status is updated to deleted - const deletedItem = await InboxItem.findByPk(inboxItem.id); + const deletedItem = await InboxItem.findOne({ + where: { uid: inboxItem.uid }, + }); expect(deletedItem).not.toBeNull(); expect(deletedItem.status).toBe('deleted'); }); @@ -253,24 +257,24 @@ describe('Inbox Routes - No Tags Scenario', () => { .post('/api/inbox') .send({ content: 'Complete workflow test', source: 'web' }); expect(createResponse.status).toBe(201); - const itemId = createResponse.body.id; + const itemUid = createResponse.body.uid; // Step 3: Retrieve inbox items const getResponse = await agent.get('/api/inbox'); expect(getResponse.status).toBe(200); expect(getResponse.body.length).toBe(1); - expect(getResponse.body[0].id).toBe(itemId); + expect(getResponse.body[0].uid).toBe(itemUid); // Step 4: Update inbox item const updateResponse = await agent - .patch(`/api/inbox/${itemId}`) + .patch(`/api/inbox/${itemUid}`) .send({ content: 'Updated workflow test' }); expect(updateResponse.status).toBe(200); expect(updateResponse.body.content).toBe('Updated workflow test'); // Step 5: Process inbox item const processResponse = await agent.patch( - `/api/inbox/${itemId}/process` + `/api/inbox/${itemUid}/process` ); expect(processResponse.status).toBe(200); expect(processResponse.body.status).toBe('processed'); @@ -303,9 +307,11 @@ describe('Inbox Routes - No Tags Scenario', () => { expect(getResponse.body.length).toBe(5); // Process all items concurrently - const itemIds = createResponses.map((response) => response.body.id); - const processPromises = itemIds.map((id) => - agent.patch(`/api/inbox/${id}/process`) + const itemUids = createResponses.map( + (response) => response.body.uid + ); + const processPromises = itemUids.map((uid) => + agent.patch(`/api/inbox/${uid}/process`) ); const processResponses = await Promise.all(processPromises); @@ -326,26 +332,28 @@ describe('Inbox Routes - No Tags Scenario', () => { describe('Error Handling - No Tags Scenario', () => { it('should handle invalid inbox item operations gracefully when no tags exist', async () => { // Try to get non-existent item - const getResponse = await agent.get('/api/inbox/999999'); - expect(getResponse.status).toBe(404); - expect(getResponse.body.error).toBe('Inbox item not found.'); + const getResponse = await agent.get('/api/inbox/invalid-uid'); + expect(getResponse.status).toBe(400); + expect(getResponse.body.error).toBe('Invalid UID'); // Try to update non-existent item const updateResponse = await agent - .patch('/api/inbox/999999') + .patch('/api/inbox/abcd1234efghijk') .send({ content: 'Updated' }); expect(updateResponse.status).toBe(404); expect(updateResponse.body.error).toBe('Inbox item not found.'); // Try to process non-existent item const processResponse = await agent.patch( - '/api/inbox/999999/process' + '/api/inbox/abcd1234efghijk/process' ); expect(processResponse.status).toBe(404); expect(processResponse.body.error).toBe('Inbox item not found.'); // Try to delete non-existent item - const deleteResponse = await agent.delete('/api/inbox/999999'); + const deleteResponse = await agent.delete( + '/api/inbox/abcd1234efghijk' + ); expect(deleteResponse.status).toBe(404); expect(deleteResponse.body.error).toBe('Inbox item not found.'); }); diff --git a/backend/tests/integration/inbox.test.js b/backend/tests/integration/inbox.test.js index 3f00166..3956daf 100644 --- a/backend/tests/integration/inbox.test.js +++ b/backend/tests/integration/inbox.test.js @@ -32,7 +32,8 @@ describe('Inbox Routes', () => { expect(response.body.content).toBe(inboxData.content); expect(response.body.source).toBe(inboxData.source); expect(response.body.status).toBe('added'); - expect(response.body.user_id).toBe(user.id); + expect(response.body.uid).toBeDefined(); + expect(typeof response.body.uid).toBe('string'); }); it('should require authentication', async () => { @@ -83,7 +84,7 @@ describe('Inbox Routes', () => { expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBe(1); // Only items with status 'added' are returned - expect(response.body.map((i) => i.id)).toContain(inboxItem1.id); + expect(response.body.map((i) => i.uid)).toContain(inboxItem1.uid); }); it('should only return items with added status', async () => { @@ -91,7 +92,7 @@ describe('Inbox Routes', () => { expect(response.status).toBe(200); expect(response.body.length).toBe(1); - expect(response.body[0].id).toBe(inboxItem1.id); + expect(response.body[0].uid).toBe(inboxItem1.uid); expect(response.body[0].status).toBe('added'); }); @@ -129,9 +130,9 @@ describe('Inbox Routes', () => { expect(response.body.length).toBe(4); // Including the item from beforeEach // Check that items are ordered by newest first - expect(response.body[0].id).toBe(item3.id); - expect(response.body[1].id).toBe(item2.id); - expect(response.body[2].id).toBe(item1.id); + expect(response.body[0].uid).toBe(item3.uid); + expect(response.body[1].uid).toBe(item2.uid); + expect(response.body[2].uid).toBe(item1.uid); // Verify the content matches expected order expect(response.body[0].content).toBe('Third item (newest)'); @@ -147,7 +148,7 @@ describe('Inbox Routes', () => { }); }); - describe('GET /api/inbox/:id', () => { + describe('GET /api/inbox/:uid', () => { let inboxItem; beforeEach(async () => { @@ -158,16 +159,23 @@ describe('Inbox Routes', () => { }); }); - it('should get inbox item by id', async () => { - const response = await agent.get(`/api/inbox/${inboxItem.id}`); + it('should get inbox item by uid', async () => { + const response = await agent.get(`/api/inbox/${inboxItem.uid}`); expect(response.status).toBe(200); - expect(response.body.id).toBe(inboxItem.id); + expect(response.body.uid).toBe(inboxItem.uid); expect(response.body.content).toBe(inboxItem.content); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent.get('/api/inbox/invalid-uid'); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent inbox item', async () => { - const response = await agent.get('/api/inbox/999999'); + const response = await agent.get('/api/inbox/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe('Inbox item not found.'); @@ -186,7 +194,9 @@ describe('Inbox Routes', () => { user_id: otherUser.id, }); - const response = await agent.get(`/api/inbox/${otherInboxItem.id}`); + const response = await agent.get( + `/api/inbox/${otherInboxItem.uid}` + ); expect(response.status).toBe(404); expect(response.body.error).toBe('Inbox item not found.'); @@ -194,7 +204,7 @@ describe('Inbox Routes', () => { it('should require authentication', async () => { const response = await request(app).get( - `/api/inbox/${inboxItem.id}` + `/api/inbox/${inboxItem.uid}` ); expect(response.status).toBe(401); @@ -202,7 +212,7 @@ describe('Inbox Routes', () => { }); }); - describe('PATCH /api/inbox/:id', () => { + describe('PATCH /api/inbox/:uid', () => { let inboxItem; beforeEach(async () => { @@ -221,7 +231,7 @@ describe('Inbox Routes', () => { }; const response = await agent - .patch(`/api/inbox/${inboxItem.id}`) + .patch(`/api/inbox/${inboxItem.uid}`) .send(updateData); expect(response.status).toBe(200); @@ -229,9 +239,18 @@ describe('Inbox Routes', () => { expect(response.body.status).toBe(updateData.status); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent + .patch('/api/inbox/invalid-uid') + .send({ content: 'Updated' }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent inbox item', async () => { const response = await agent - .patch('/api/inbox/999999') + .patch('/api/inbox/abcd1234efghijk') .send({ content: 'Updated' }); expect(response.status).toBe(404); @@ -240,7 +259,7 @@ describe('Inbox Routes', () => { it('should require authentication', async () => { const response = await request(app) - .patch(`/api/inbox/${inboxItem.id}`) + .patch(`/api/inbox/${inboxItem.uid}`) .send({ content: 'Updated' }); expect(response.status).toBe(401); @@ -248,7 +267,7 @@ describe('Inbox Routes', () => { }); }); - describe('DELETE /api/inbox/:id', () => { + describe('DELETE /api/inbox/:uid', () => { let inboxItem; beforeEach(async () => { @@ -260,7 +279,7 @@ describe('Inbox Routes', () => { }); it('should delete inbox item', async () => { - const response = await agent.delete(`/api/inbox/${inboxItem.id}`); + const response = await agent.delete(`/api/inbox/${inboxItem.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe( @@ -268,13 +287,22 @@ describe('Inbox Routes', () => { ); // Verify inbox item status is updated to deleted - const deletedItem = await InboxItem.findByPk(inboxItem.id); + const deletedItem = await InboxItem.findOne({ + where: { uid: inboxItem.uid }, + }); expect(deletedItem).not.toBeNull(); expect(deletedItem.status).toBe('deleted'); }); + it('should return 400 for invalid uid format', async () => { + const response = await agent.delete('/api/inbox/invalid-uid'); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + it('should return 404 for non-existent inbox item', async () => { - const response = await agent.delete('/api/inbox/999999'); + const response = await agent.delete('/api/inbox/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe('Inbox item not found.'); @@ -282,7 +310,7 @@ describe('Inbox Routes', () => { it('should require authentication', async () => { const response = await request(app).delete( - `/api/inbox/${inboxItem.id}` + `/api/inbox/${inboxItem.uid}` ); expect(response.status).toBe(401); @@ -290,7 +318,7 @@ describe('Inbox Routes', () => { }); }); - describe('PATCH /api/inbox/:id/process', () => { + describe('PATCH /api/inbox/:uid/process', () => { let inboxItem; beforeEach(async () => { @@ -304,7 +332,7 @@ describe('Inbox Routes', () => { it('should process inbox item', async () => { const response = await agent.patch( - `/api/inbox/${inboxItem.id}/process` + `/api/inbox/${inboxItem.uid}/process` ); expect(response.status).toBe(200); @@ -312,7 +340,18 @@ describe('Inbox Routes', () => { }); it('should return 404 for non-existent inbox item', async () => { - const response = await agent.patch('/api/inbox/999999/process'); + const response = await agent.patch( + '/api/inbox/invalid-uid/process' + ); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Invalid UID'); + }); + + it('should return 404 for non-existent item', async () => { + const response = await agent.patch( + '/api/inbox/abcd1234efghijk/process' + ); expect(response.status).toBe(404); expect(response.body.error).toBe('Inbox item not found.'); @@ -320,7 +359,7 @@ describe('Inbox Routes', () => { it('should require authentication', async () => { const response = await request(app).patch( - `/api/inbox/${inboxItem.id}/process` + `/api/inbox/${inboxItem.uid}/process` ); expect(response.status).toBe(401); diff --git a/backend/tests/integration/notes.test.js b/backend/tests/integration/notes.test.js index a3896f0..804de49 100644 --- a/backend/tests/integration/notes.test.js +++ b/backend/tests/integration/notes.test.js @@ -29,7 +29,7 @@ describe('Notes Routes', () => { const noteData = { title: 'Test Note', content: 'This is a test note content', - project_id: project.id, + project_uid: project.uid, }; const response = await agent.post('/api/note').send(noteData); @@ -140,7 +140,7 @@ describe('Notes Routes', () => { }); it('should get note by id', async () => { - const response = await agent.get(`/api/note/${note.id}`); + const response = await agent.get(`/api/note/${note.uid}`); expect(response.status).toBe(200); expect(response.body.id).toBe(note.id); @@ -149,7 +149,7 @@ describe('Notes Routes', () => { }); it('should return 404 for non-existent note', async () => { - const response = await agent.get('/api/note/999999'); + const response = await agent.get('/api/note/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe('Note not found.'); @@ -167,14 +167,14 @@ describe('Notes Routes', () => { user_id: otherUser.id, }); - const response = await agent.get(`/api/note/${otherNote.id}`); + const response = await agent.get(`/api/note/${otherNote.uid}`); expect(response.status).toBe(404); expect(response.body.error).toBe('Note not found.'); }); it('should require authentication', async () => { - const response = await request(app).get(`/api/note/${note.id}`); + const response = await request(app).get(`/api/note/${note.uid}`); expect(response.status).toBe(401); expect(response.body.error).toBe('Authentication required'); @@ -196,11 +196,11 @@ describe('Notes Routes', () => { const updateData = { title: 'Updated Note', content: 'Updated content', - project_id: project.id, + project_uid: project.uid, }; const response = await agent - .patch(`/api/note/${note.id}`) + .patch(`/api/note/${note.uid}`) .send(updateData); expect(response.status).toBe(200); @@ -211,7 +211,7 @@ describe('Notes Routes', () => { it('should return 404 for non-existent note', async () => { const response = await agent - .patch('/api/note/999999') + .patch('/api/note/abcd1234efghijk') .send({ title: 'Updated' }); expect(response.status).toBe(404); @@ -231,7 +231,7 @@ describe('Notes Routes', () => { }); const response = await agent - .patch(`/api/note/${otherNote.id}`) + .patch(`/api/note/${otherNote.uid}`) .send({ title: 'Updated' }); expect(response.status).toBe(404); @@ -240,7 +240,7 @@ describe('Notes Routes', () => { it('should require authentication', async () => { const response = await request(app) - .patch(`/api/note/${note.id}`) + .patch(`/api/note/${note.uid}`) .send({ title: 'Updated' }); expect(response.status).toBe(401); @@ -259,7 +259,7 @@ describe('Notes Routes', () => { }); it('should delete note', async () => { - const response = await agent.delete(`/api/note/${note.id}`); + const response = await agent.delete(`/api/note/${note.uid}`); expect(response.status).toBe(200); expect(response.body.message).toBe('Note deleted successfully.'); @@ -270,7 +270,7 @@ describe('Notes Routes', () => { }); it('should return 404 for non-existent note', async () => { - const response = await agent.delete('/api/note/999999'); + const response = await agent.delete('/api/note/abcd1234efghijk'); expect(response.status).toBe(404); expect(response.body.error).toBe('Note not found.'); @@ -288,14 +288,14 @@ describe('Notes Routes', () => { user_id: otherUser.id, }); - const response = await agent.delete(`/api/note/${otherNote.id}`); + const response = await agent.delete(`/api/note/${otherNote.uid}`); expect(response.status).toBe(404); expect(response.body.error).toBe('Note not found.'); }); it('should require authentication', async () => { - const response = await request(app).delete(`/api/note/${note.id}`); + const response = await request(app).delete(`/api/note/${note.uid}`); expect(response.status).toBe(401); expect(response.body.error).toBe('Authentication required'); diff --git a/e2e/bin/run-e2e.sh b/e2e/bin/run-e2e.sh index e66b7c4..28667f0 100755 --- a/e2e/bin/run-e2e.sh +++ b/e2e/bin/run-e2e.sh @@ -110,6 +110,6 @@ bash -c ' # Respect E2E_SLOWMO and run only Firefox sequentially npx playwright test --headed --project=Firefox --workers=1 else - npm test + npx playwright test --workers=10 fi ' diff --git a/frontend/Layout.tsx b/frontend/Layout.tsx index f8d318b..293eca0 100644 --- a/frontend/Layout.tsx +++ b/frontend/Layout.tsx @@ -149,15 +149,15 @@ const Layout: React.FC = ({ const handleSaveNote = async (noteData: Note) => { try { let result: Note; - if (noteData.id) { - result = await updateNote(noteData.id, noteData); + if (noteData.uid) { + result = await updateNote(noteData.uid, noteData); // Update existing note in global store const currentNotes = useStore.getState().notesStore.notes; useStore .getState() .notesStore.setNotes( currentNotes.map((note) => - note.id === result.id ? result : note + note.uid === result.uid ? result : note ) ); } else { @@ -255,15 +255,15 @@ const Layout: React.FC = ({ const handleSaveArea = async (areaData: Partial) => { try { let result: Area; - if (areaData.id) { - result = await updateArea(areaData.id, areaData); + if (areaData.uid) { + result = await updateArea(areaData.uid, areaData); // Update existing area in global store const currentAreas = useStore.getState().areasStore.areas; useStore .getState() .areasStore.setAreas( currentAreas.map((area) => - area.id === result.id ? result : area + area.uid === result.uid ? result : area ) ); } else { diff --git a/frontend/components/Area/AreaModal.tsx b/frontend/components/Area/AreaModal.tsx index 1909f77..080c824 100644 --- a/frontend/components/Area/AreaModal.tsx +++ b/frontend/components/Area/AreaModal.tsx @@ -22,6 +22,7 @@ const AreaModal: React.FC = ({ const { t } = useTranslation(); const [formData, setFormData] = useState({ id: area?.id || 0, + uid: area?.uid || '', name: area?.name || '', description: area?.description || '', }); @@ -38,6 +39,7 @@ const AreaModal: React.FC = ({ if (isOpen) { setFormData({ id: area?.id || 0, + uid: area?.uid || '', name: area?.name || '', description: area?.description || '', }); @@ -109,7 +111,7 @@ const AreaModal: React.FC = ({ try { await onSave(formData); showSuccessToast( - formData.id + formData.uid ? t('success.areaUpdated') : t('success.areaCreated') ); @@ -131,9 +133,9 @@ const AreaModal: React.FC = ({ }; const handleDeleteArea = async () => { - if (formData.id && formData.id !== 0 && onDelete) { + if (formData.uid && onDelete) { try { - await onDelete(formData.id); + await onDelete(formData.uid); showSuccessToast( t('success.areaDeleted', 'Area deleted successfully!') ); @@ -225,22 +227,16 @@ const AreaModal: React.FC = ({
{/* Left side: Delete and Cancel */}
- {area && - area.id && - area.id !== 0 && - onDelete && ( - - )} + {area && area.uid && onDelete && ( + + )} - {dropdownOpen === area.id && ( + {dropdownOpen === area.uid && (
@@ -255,7 +255,7 @@ const Areas: React.FC = () => { setDropdownOpen(null); }} className="block px-4 py-2 text-sm text-red-500 dark:text-red-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left rounded-b-md" - data-testid={`area-delete-${area.id}`} + data-testid={`area-delete-${area.uid}`} > {t('areas.delete', 'Delete')} @@ -273,9 +273,9 @@ const Areas: React.FC = () => { isOpen={isAreaModalOpen} onClose={() => setIsAreaModalOpen(false)} onSave={handleSaveArea} - onDelete={async (areaId) => { + onDelete={async (areaUid) => { try { - await deleteArea(areaId); + await deleteArea(areaUid); const updatedAreas = await fetchAreas(); useStore .getState() diff --git a/frontend/components/Inbox/InboxItemDetail.tsx b/frontend/components/Inbox/InboxItemDetail.tsx index a31bb37..99ec29f 100644 --- a/frontend/components/Inbox/InboxItemDetail.tsx +++ b/frontend/components/Inbox/InboxItemDetail.tsx @@ -19,12 +19,12 @@ import { useStore } from '../../store/useStore'; interface InboxItemDetailProps { item: InboxItem; - onProcess: (id: number) => void; - onDelete: (id: number) => void; - onUpdate?: (id: number) => Promise; - openTaskModal: (task: Task, inboxItemId?: number) => void; - openProjectModal: (project: Project | null, inboxItemId?: number) => void; - openNoteModal: (note: Note | null, inboxItemId?: number) => void; + onProcess: (uid: string) => void; + onDelete: (uid: string) => void; + onUpdate?: (uid: string) => Promise; + openTaskModal: (task: Task, inboxItemUid?: string) => void; + openProjectModal: (project: Project | null, inboxItemUid?: string) => void; + openNoteModal: (note: Note | null, inboxItemUid?: string) => void; projects: Project[]; } @@ -297,8 +297,8 @@ const InboxItemDetail: React.FC = ({ completed_at: null, }; - if (item.id !== undefined) { - openTaskModal(newTask, item.id); + if (item.uid !== undefined) { + openTaskModal(newTask, item.uid); } else { openTaskModal(newTask); } @@ -326,8 +326,8 @@ const InboxItemDetail: React.FC = ({ tags: projectTags, }; - if (item.id !== undefined) { - openProjectModal(newProject, item.id); + if (item.uid !== undefined) { + openProjectModal(newProject, item.uid); } else { openProjectModal(newProject); } @@ -419,11 +419,11 @@ const InboxItemDetail: React.FC = ({ title: finalTitle, content: finalContent, tags: tagObjects, - project_id: projectId, + project_uid: projectId, }; - if (item.id !== undefined) { - openNoteModal(newNote, item.id); + if (item.uid !== undefined) { + openNoteModal(newNote, item.uid); } else { openNoteModal(newNote); } @@ -434,8 +434,8 @@ const InboxItemDetail: React.FC = ({ }; const confirmDelete = () => { - if (item.id !== undefined) { - onDelete(item.id); + if (item.uid !== undefined) { + onDelete(item.uid); } setShowConfirmDialog(false); }; @@ -450,8 +450,8 @@ const InboxItemDetail: React.FC = ({
@@ -227,7 +222,7 @@ const NoteCard: React.FC = ({ setDropdownOpen(false); }} className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left rounded-t-md" - data-testid={`note-edit-${note.id}`} + data-testid={`note-edit-${note.uid}`} > {t('notes.edit', 'Edit')} @@ -241,7 +236,7 @@ const NoteCard: React.FC = ({ setDropdownOpen(false); }} className="block px-4 py-2 text-sm text-red-500 dark:text-red-300 hover:bg-gray-100 dark:hover:bg-gray-600 w-full text-left rounded-b-md" - data-testid={`note-delete-${note.id}`} + data-testid={`note-delete-${note.uid}`} > {t('notes.delete', 'Delete')} diff --git a/frontend/components/Tag/TagDetails.tsx b/frontend/components/Tag/TagDetails.tsx index 243db84..332f203 100644 --- a/frontend/components/Tag/TagDetails.tsx +++ b/frontend/components/Tag/TagDetails.tsx @@ -39,7 +39,7 @@ const TagDetails: React.FC = () => { // State for ProjectItem components const [activeDropdown, setActiveDropdown] = useState(null); - const [hoveredNoteId, setHoveredNoteId] = useState(null); + const [hoveredNoteId, setHoveredNoteId] = useState(null); const [, setProjectToDelete] = useState(null); const [, setIsConfirmDialogOpen] = useState(false); const navigate = useNavigate(); @@ -260,10 +260,10 @@ const TagDetails: React.FC = () => {
    {notes.map((note) => (
  • - setHoveredNoteId(note.id || null) + setHoveredNoteId(note.uid || null) } onMouseLeave={() => setHoveredNoteId(null)} > @@ -282,7 +282,9 @@ const TagDetails: React.FC = () => { /^-|-$/g, '' )}` - : `/note/${note.id}` + : note.uid + ? `/note/${note.uid}` + : '#' } className="text-md font-semibold text-gray-900 dark:text-gray-100 hover:underline" > @@ -324,7 +326,7 @@ const TagDetails: React.FC = () => { onClick={ () => {} // Edit functionality not implemented yet } - className={`text-gray-500 hover:text-blue-700 dark:hover:text-blue-300 focus:outline-none transition-opacity ${hoveredNoteId === note.id ? 'opacity-100' : 'opacity-0'}`} + className={`text-gray-500 hover:text-blue-700 dark:hover:text-blue-300 focus:outline-none transition-opacity ${hoveredNoteId === note.uid ? 'opacity-100' : 'opacity-0'}`} aria-label={`Edit ${note.title}`} title={`Edit ${note.title}`} > @@ -334,7 +336,7 @@ const TagDetails: React.FC = () => { onClick={ () => {} // Delete functionality not implemented yet } - className={`text-gray-500 hover:text-red-700 dark:hover:text-red-300 focus:outline-none transition-opacity ${hoveredNoteId === note.id ? 'opacity-100' : 'opacity-0'}`} + className={`text-gray-500 hover:text-red-700 dark:hover:text-red-300 focus:outline-none transition-opacity ${hoveredNoteId === note.uid ? 'opacity-100' : 'opacity-0'}`} aria-label={`Delete ${note.title}`} title={`Delete ${note.title}`} > diff --git a/frontend/components/Task/TaskDetails.tsx b/frontend/components/Task/TaskDetails.tsx index 5e947ac..f40526f 100644 --- a/frontend/components/Task/TaskDetails.tsx +++ b/frontend/components/Task/TaskDetails.tsx @@ -1015,7 +1015,7 @@ const TaskDetails: React.FC = () => {
    diff --git a/frontend/components/Task/TaskTimeline.tsx b/frontend/components/Task/TaskTimeline.tsx index 423cb0c..135d90d 100644 --- a/frontend/components/Task/TaskTimeline.tsx +++ b/frontend/components/Task/TaskTimeline.tsx @@ -13,11 +13,11 @@ import { } from '@heroicons/react/24/outline'; interface TaskTimelineProps { - taskId: number | undefined; + taskUid: string | undefined; refreshKey?: number; } -const TaskTimeline: React.FC = ({ taskId, refreshKey }) => { +const TaskTimeline: React.FC = ({ taskUid, refreshKey }) => { const { t } = useTranslation(); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(true); @@ -25,7 +25,7 @@ const TaskTimeline: React.FC = ({ taskId, refreshKey }) => { useEffect(() => { const fetchTimeline = async () => { - if (!taskId || taskId === undefined) { + if (!taskUid || taskUid === undefined) { setLoading(false); setEvents([]); return; @@ -35,7 +35,7 @@ const TaskTimeline: React.FC = ({ taskId, refreshKey }) => { setError(null); try { - const timeline = await getTaskTimeline(taskId); + const timeline = await getTaskTimeline(taskUid); // Sort events by created_at in descending order (most recent first) const sortedTimeline = timeline.sort( (a, b) => @@ -53,7 +53,7 @@ const TaskTimeline: React.FC = ({ taskId, refreshKey }) => { }; fetchTimeline(); - }, [taskId, refreshKey]); + }, [taskUid, refreshKey]); const getTranslatedStatusLabel = (status: number | string): string => { // Handle both numeric and string status values @@ -196,7 +196,7 @@ const TaskTimeline: React.FC = ({ taskId, refreshKey }) => { ); } - if (!taskId) { + if (!taskUid) { return (
    diff --git a/frontend/components/Task/TimelinePanel.tsx b/frontend/components/Task/TimelinePanel.tsx index b694e83..61f5594 100644 --- a/frontend/components/Task/TimelinePanel.tsx +++ b/frontend/components/Task/TimelinePanel.tsx @@ -4,13 +4,13 @@ import TaskTimeline from './TaskTimeline'; import { ClockIcon, XMarkIcon } from '@heroicons/react/24/outline'; interface TimelinePanelProps { - taskId: number | undefined; + taskUid: string | undefined; isExpanded: boolean; onToggle: () => void; } const TimelinePanel: React.FC = ({ - taskId, + taskUid, isExpanded, onToggle, }) => { @@ -52,7 +52,7 @@ const TimelinePanel: React.FC = ({
- +
)} diff --git a/frontend/entities/InboxItem.ts b/frontend/entities/InboxItem.ts index de5f87b..0f3736b 100644 --- a/frontend/entities/InboxItem.ts +++ b/frontend/entities/InboxItem.ts @@ -1,5 +1,6 @@ export interface InboxItem { id?: number; + uid?: string; content: string; status?: string; // 'added' | 'processed' | 'deleted' source?: string; // 'telegram' diff --git a/frontend/entities/Note.ts b/frontend/entities/Note.ts index bc261c6..c1d7432 100644 --- a/frontend/entities/Note.ts +++ b/frontend/entities/Note.ts @@ -7,7 +7,8 @@ export interface Note { content: string; created_at?: string; updated_at?: string; - project_id?: number; // Foreign key for project + project_id?: number; // Foreign key for project (deprecated, use project_uid) + project_uid?: string; // Foreign key for project by uid tags?: Tag[]; Tags?: Tag[]; // Sequelize association naming (capitalized) project?: { diff --git a/frontend/entities/Project.ts b/frontend/entities/Project.ts index 4e2b819..b571898 100644 --- a/frontend/entities/Project.ts +++ b/frontend/entities/Project.ts @@ -12,6 +12,7 @@ export interface Project { pin_to_sidebar?: boolean; area?: Area; area_id?: number | null; + area_uid?: string | null; tags?: Tag[]; priority?: PriorityType; tasks?: Task[]; diff --git a/frontend/store/useStore.ts b/frontend/store/useStore.ts index 024da40..6769d6b 100644 --- a/frontend/store/useStore.ts +++ b/frontend/store/useStore.ts @@ -95,6 +95,7 @@ interface InboxStore { addInboxItem: (inboxItem: InboxItem) => void; updateInboxItem: (inboxItem: InboxItem) => void; removeInboxItem: (id: number) => void; + removeInboxItemByUid: (uid: string) => void; setLoading: (isLoading: boolean) => void; setError: (isError: boolean) => void; resetPagination: () => void; @@ -629,7 +630,10 @@ export const useStore = create((set: any) => ({ inboxStore: { ...state.inboxStore, inboxItems: state.inboxStore.inboxItems.map((item) => - item.id === inboxItem.id ? inboxItem : item + (inboxItem.uid && item.uid === inboxItem.uid) || + (inboxItem.id && item.id === inboxItem.id) + ? inboxItem + : item ), }, })), @@ -649,6 +653,22 @@ export const useStore = create((set: any) => ({ }, }, })), + removeInboxItemByUid: (uid) => + set((state) => ({ + inboxStore: { + ...state.inboxStore, + inboxItems: state.inboxStore.inboxItems.filter( + (item) => item.uid !== uid + ), + pagination: { + ...state.inboxStore.pagination, + total: Math.max( + 0, + state.inboxStore.pagination.total - 1 + ), + }, + }, + })), setLoading: (isLoading) => set((state) => ({ inboxStore: { ...state.inboxStore, isLoading }, diff --git a/frontend/utils/areasService.ts b/frontend/utils/areasService.ts index 1953e87..a1d3f2b 100644 --- a/frontend/utils/areasService.ts +++ b/frontend/utils/areasService.ts @@ -28,10 +28,10 @@ export const createArea = async (areaData: Partial): Promise => { }; export const updateArea = async ( - areaId: number, + areaUid: string, areaData: Partial ): Promise => { - const response = await fetch(`/api/areas/${areaId}`, { + const response = await fetch(`/api/areas/${areaUid}`, { method: 'PATCH', credentials: 'include', headers: { @@ -45,8 +45,8 @@ export const updateArea = async ( return await response.json(); }; -export const deleteArea = async (areaId: number): Promise => { - const response = await fetch(`/api/areas/${areaId}`, { +export const deleteArea = async (areaUid: string): Promise => { + const response = await fetch(`/api/areas/${areaUid}`, { method: 'DELETE', credentials: 'include', headers: { diff --git a/frontend/utils/inboxService.ts b/frontend/utils/inboxService.ts index 2f4d675..215d6ca 100644 --- a/frontend/utils/inboxService.ts +++ b/frontend/utils/inboxService.ts @@ -70,10 +70,10 @@ export const createInboxItem = async ( }; export const updateInboxItem = async ( - itemId: number, + itemUid: string, content: string ): Promise => { - const response = await fetch(`/api/inbox/${itemId}`, { + const response = await fetch(`/api/inbox/${itemUid}`, { method: 'PATCH', credentials: 'include', headers: { @@ -87,8 +87,8 @@ export const updateInboxItem = async ( return await response.json(); }; -export const processInboxItem = async (itemId: number): Promise => { - const response = await fetch(`/api/inbox/${itemId}/process`, { +export const processInboxItem = async (itemUid: string): Promise => { + const response = await fetch(`/api/inbox/${itemUid}/process`, { method: 'PATCH', credentials: 'include', headers: { @@ -100,8 +100,8 @@ export const processInboxItem = async (itemId: number): Promise => { return await response.json(); }; -export const deleteInboxItem = async (itemId: number): Promise => { - const response = await fetch(`/api/inbox/${itemId}`, { +export const deleteInboxItem = async (itemUid: string): Promise => { + const response = await fetch(`/api/inbox/${itemUid}`, { method: 'DELETE', credentials: 'include', headers: { @@ -132,15 +132,15 @@ export const loadInboxItemsToStore = async ( // Check for new items since last check (only for non-initial loads) if (!isInitialLoad) { - const currentItemIds = new Set( - inboxStore.inboxItems.map((item) => item.id) + const currentItemUids = new Set( + inboxStore.inboxItems.map((item) => item.uid).filter(Boolean) ); // New telegram items const newTelegramItems = items.filter( (item) => - item.id && - !currentItemIds.has(item.id) && + item.uid && + !currentItemUids.has(item.uid) && item.source === 'telegram' ); @@ -224,13 +224,13 @@ export const createInboxItemWithStore = async ( }; export const updateInboxItemWithStore = async ( - itemId: number, + itemUid: string, content: string ): Promise => { const inboxStore = useStore.getState().inboxStore; try { - const updatedItem = await updateInboxItem(itemId, content); + const updatedItem = await updateInboxItem(itemUid, content); inboxStore.updateInboxItem(updatedItem); return updatedItem; } catch (error) { @@ -240,13 +240,13 @@ export const updateInboxItemWithStore = async ( }; export const processInboxItemWithStore = async ( - itemId: number + itemUid: string ): Promise => { const inboxStore = useStore.getState().inboxStore; try { - const processedItem = await processInboxItem(itemId); - inboxStore.removeInboxItem(itemId); + const processedItem = await processInboxItem(itemUid); + inboxStore.removeInboxItemByUid(itemUid); return processedItem; } catch (error) { console.error('Failed to process inbox item:', error); @@ -255,13 +255,13 @@ export const processInboxItemWithStore = async ( }; export const deleteInboxItemWithStore = async ( - itemId: number + itemUid: string ): Promise => { const inboxStore = useStore.getState().inboxStore; try { - await deleteInboxItem(itemId); - inboxStore.removeInboxItem(itemId); + await deleteInboxItem(itemUid); + inboxStore.removeInboxItemByUid(itemUid); } catch (error) { console.error('Failed to delete inbox item:', error); throw error; diff --git a/frontend/utils/noteDeleteUtils.ts b/frontend/utils/noteDeleteUtils.ts index fcf8be9..4b5aa1e 100644 --- a/frontend/utils/noteDeleteUtils.ts +++ b/frontend/utils/noteDeleteUtils.ts @@ -4,24 +4,34 @@ import { Note } from '../entities/Note'; /** * Shared utility function to delete a note and update the global store - * @param noteId - The ID of the note to delete + * @param noteOrUid - The note object or UID of the note to delete * @param showSuccessToast - Function to show success toast * @param t - Translation function * @returns Promise */ export const deleteNoteWithStoreUpdate = async ( - noteId: number, + noteOrUid: Note | string, showSuccessToast: (message: string) => void, t: (key: string) => string ): Promise => { - await apiDeleteNote(noteId); + let noteUid: string; + + if (typeof noteOrUid === 'object') { + // It's a Note object + noteUid = noteOrUid.uid!; + } else { + // It's a UID string + noteUid = noteOrUid; + } + + await apiDeleteNote(noteUid); // Remove note from global store const currentNotes = useStore.getState().notesStore.notes; useStore .getState() .notesStore.setNotes( - currentNotes.filter((note: Note) => note.id !== noteId) + currentNotes.filter((note: Note) => note.uid !== noteUid) ); // Show success toast diff --git a/frontend/utils/notesService.ts b/frontend/utils/notesService.ts index 4211cb4..0a26655 100644 --- a/frontend/utils/notesService.ts +++ b/frontend/utils/notesService.ts @@ -28,22 +28,39 @@ export const createNote = async (noteData: Note): Promise => { }; export const updateNote = async ( - noteId: number, + noteUid: string, noteData: Note ): Promise => { - const response = await fetch(`/api/note/${noteId}`, { + // Transform project_id to project_uid if needed + const requestData = { ...noteData }; + if (noteData.project && noteData.project.uid) { + requestData.project_uid = noteData.project.uid; + } else if (noteData.project_uid) { + // project_uid is already set, use it as-is + } else if (noteData.project_id && !noteData.project_uid) { + // Legacy: if only project_id is provided, we can't convert it to uid here + // This should not happen with the new implementation, but keeping for safety + console.warn( + 'Note update with project_id but no project_uid - this may fail' + ); + } + + // Use the provided noteUid + const noteIdentifier = noteUid; + + const response = await fetch(`/api/note/${noteIdentifier}`, { method: 'PATCH', credentials: 'include', headers: getPostHeaders(), - body: JSON.stringify(noteData), + body: JSON.stringify(requestData), }); await handleAuthResponse(response, 'Failed to update note.'); return await response.json(); }; -export const deleteNote = async (noteId: number): Promise => { - const response = await fetch(`/api/note/${noteId}`, { +export const deleteNote = async (noteUid: string): Promise => { + const response = await fetch(`/api/note/${noteUid}`, { method: 'DELETE', credentials: 'include', headers: getDefaultHeaders(), diff --git a/frontend/utils/taskEventService.ts b/frontend/utils/taskEventService.ts index ac036b6..38ca3e6 100644 --- a/frontend/utils/taskEventService.ts +++ b/frontend/utils/taskEventService.ts @@ -11,8 +11,10 @@ const API_BASE = '/api'; /** * Get task timeline (all events for a specific task) */ -export const getTaskTimeline = async (taskId: number): Promise => { - const response = await fetch(`${API_BASE}/task/${taskId}/timeline`, { +export const getTaskTimeline = async ( + taskUid: string +): Promise => { + const response = await fetch(`${API_BASE}/task/${taskUid}/timeline`, { credentials: 'include', }); @@ -29,11 +31,14 @@ export const getTaskTimeline = async (taskId: number): Promise => { * Get task completion time analytics */ export const getTaskCompletionTime = async ( - taskId: number + taskUid: string ): Promise => { - const response = await fetch(`${API_BASE}/task/${taskId}/completion-time`, { - credentials: 'include', - }); + const response = await fetch( + `${API_BASE}/task/${taskUid}/completion-time`, + { + credentials: 'include', + } + ); if (response.status === 404) { return null; @@ -111,15 +116,15 @@ export const getCompletionAnalytics = async ( options: { limit?: number; offset?: number; - projectId?: number; + projectUid?: string; } = {} ): Promise => { const params = new URLSearchParams(); if (options.limit) params.append('limit', options.limit.toString()); if (options.offset) params.append('offset', options.offset.toString()); - if (options.projectId) - params.append('projectId', options.projectId.toString()); + if (options.projectUid) + params.append('projectUid', options.projectUid); const response = await fetch( `${API_BASE}/tasks/completion-analytics?${params}`,