mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 15:48:16 +02:00
Feature: Affichage tags et icône priorité dans les cartes notes
Ajout de deux nouvelles fonctionnalités pour les notes: 1. Affichage des tags dans les cartes repliées: - Les tags sont maintenant visibles dans les cartes de notes sans avoir à les ouvrir - Badge avec icône TagIcon et nom du tag - Chargement automatique des tags depuis la base de données 2. Icône de priorité cliquable: - Bouton "!" en haut à droite de chaque carte de note - Rouge (!) quand la note est prioritaire (toujours visible) - Gris clair (!) quand non prioritaire (visible au hover) - Toggle au clic pour marquer/démarquer comme prioritaire - Les notes prioritaires sont triées en premier Backend: - Route PATCH /api/notes/:id/priority pour toggle la priorité - Modification GET /api/notes pour charger tags et priority - Tri par priority DESC, updated_at DESC - Migration automatique pour ajouter colonne priority - Index pour performance des requêtes de tri Frontend: - Interface Note étendue avec tags et priority - Méthode togglePriority dans NotesService - Affichage conditionnel de l'icône (toujours visible si prioritaire) - Empêche le clic sur l'icône d'ouvrir la note (stopPropagation) Migration: - Ajout automatique de la colonne priority (INTEGER DEFAULT 0) - Création d'index idx_notes_priority pour performance - Fichier de migration 002_add_priority.sql créé pour référence
This commit is contained in:
5 files changed
+117
-5
No files matched your search
@@ -242,6 +242,7 @@ function initDatabase() {
|
|||||||
|
|
||||||
const hasImageFilename = columns.some(col => col.name === 'image_filename');
|
const hasImageFilename = columns.some(col => col.name === 'image_filename');
|
||||||
const hasArchived = columns.some(col => col.name === 'archived');
|
const hasArchived = columns.some(col => col.name === 'archived');
|
||||||
|
const hasPriority = columns.some(col => col.name === 'priority');
|
||||||
|
|
||||||
if (!hasImageFilename) {
|
if (!hasImageFilename) {
|
||||||
logger.info('Migration: Ajout de la colonne image_filename...');
|
logger.info('Migration: Ajout de la colonne image_filename...');
|
||||||
@@ -264,6 +265,25 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasPriority) {
|
||||||
|
logger.info('Migration: Ajout de la colonne priority...');
|
||||||
|
db.run('ALTER TABLE notes ADD COLUMN priority INTEGER DEFAULT 0', (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Erreur lors de l\'ajout de la colonne priority:', err);
|
||||||
|
} else {
|
||||||
|
logger.info('✓ Colonne priority ajoutée avec succès');
|
||||||
|
// Créer l'index pour la performance
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_notes_priority ON notes(priority DESC, updated_at DESC)', (indexErr) => {
|
||||||
|
if (indexErr) {
|
||||||
|
logger.error('Erreur lors de la création de l\'index priority:', indexErr);
|
||||||
|
} else {
|
||||||
|
logger.info('✓ Index priority créé avec succès');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Créer l'utilisateur admin par défaut si la table est vide
|
// Créer l'utilisateur admin par défaut si la table est vide
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- Migration: Add priority column to notes table
|
||||||
|
-- Date: 2025-01-12
|
||||||
|
|
||||||
|
ALTER TABLE notes ADD COLUMN priority INTEGER DEFAULT 0;
|
||||||
|
|
||||||
|
-- Create index for better performance when sorting by priority
|
||||||
|
CREATE INDEX idx_notes_priority ON notes(priority DESC, updated_at DESC);
|
||||||
+36
-3
@@ -49,16 +49,16 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const notes = await getAll(`
|
const notes = await getAll(`
|
||||||
SELECT
|
SELECT
|
||||||
n.id, n.title, n.content, n.image_filename, n.archived,
|
n.id, n.title, n.content, n.image_filename, n.archived, n.priority,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id) as todos_count,
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id) as todos_count,
|
||||||
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id AND completed = 1) as todos_completed
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id AND completed = 1) as todos_completed
|
||||||
FROM notes n
|
FROM notes n
|
||||||
WHERE n.user_id = ? AND n.archived = ?
|
WHERE n.user_id = ? AND n.archived = ?
|
||||||
ORDER BY n.updated_at DESC
|
ORDER BY n.priority DESC, n.updated_at DESC
|
||||||
`, [req.user.id, archivedFilter]);
|
`, [req.user.id, archivedFilter]);
|
||||||
|
|
||||||
// Charger les todos, images et fichiers pour chaque note
|
// Charger les todos, images, fichiers et tags pour chaque note
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const todos = await getAll(`
|
const todos = await getAll(`
|
||||||
SELECT id, text, completed, position
|
SELECT id, text, completed, position
|
||||||
@@ -84,6 +84,15 @@ router.get('/', async (req, res) => {
|
|||||||
`, [note.id]);
|
`, [note.id]);
|
||||||
note.files = files || [];
|
note.files = files || [];
|
||||||
note.files_count = files?.length || 0;
|
note.files_count = files?.length || 0;
|
||||||
|
|
||||||
|
const tags = await getAll(`
|
||||||
|
SELECT t.id, t.name
|
||||||
|
FROM tags t
|
||||||
|
INNER JOIN note_tags nt ON t.id = nt.tag_id
|
||||||
|
WHERE nt.note_id = ?
|
||||||
|
ORDER BY t.name ASC
|
||||||
|
`, [note.id]);
|
||||||
|
note.tags = tags || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(notes || []);
|
res.json(notes || []);
|
||||||
@@ -895,5 +904,29 @@ router.delete('/:noteId/tags/:tagId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/notes/:id/priority
|
||||||
|
* Modifier la priorité d'une note
|
||||||
|
*/
|
||||||
|
router.patch('/:id/priority', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { priority } = req.body;
|
||||||
|
|
||||||
|
// Vérifier que la note appartient à l'utilisateur
|
||||||
|
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).json({ error: 'Note non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await runQuery('UPDATE notes SET priority = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [priority ? 1 : 0, req.params.id]);
|
||||||
|
|
||||||
|
logger.info(`Priorité de la note ${req.params.id} modifiée: ${priority}`);
|
||||||
|
res.json({ message: 'Priorité modifiée avec succès', priority });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la modification de la priorité:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
module.exports.searchNotes = searchNotes;
|
module.exports.searchNotes = searchNotes;
|
||||||
+33
-2
@@ -944,11 +944,34 @@ const Index = () => {
|
|||||||
paginatedNotes.map(note => (
|
paginatedNotes.map(note => (
|
||||||
<Card
|
<Card
|
||||||
key={note.id}
|
key={note.id}
|
||||||
className="cursor-pointer hover:shadow-md transition-all group"
|
className="cursor-pointer hover:shadow-md transition-all group relative"
|
||||||
onClick={() => handleOpenNote(note)}
|
onClick={() => handleOpenNote(note)}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<h3 className="font-semibold text-base mb-2 line-clamp-1">
|
{/* Priority icon */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`absolute top-2 right-2 h-6 w-6 transition-opacity ${
|
||||||
|
note.priority ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const newPriority = !note.priority;
|
||||||
|
if (note.id) {
|
||||||
|
NotesService.togglePriority(note.id, newPriority);
|
||||||
|
setNotes(notes.map(n => n.id === note.id ? { ...n, priority: newPriority } : n));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{note.priority ? (
|
||||||
|
<span className="text-red-500 text-lg font-bold">!</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-lg">!</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h3 className="font-semibold text-base mb-2 line-clamp-1 pr-8">
|
||||||
{note.title || "Sans titre"}
|
{note.title || "Sans titre"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
||||||
@@ -973,6 +996,14 @@ const Index = () => {
|
|||||||
{note.files.length}
|
{note.files.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{note.tags && note.tags.length > 0 && (
|
||||||
|
note.tags.map((tag) => (
|
||||||
|
<Badge key={tag.id} variant="outline" className="text-xs">
|
||||||
|
<TagIcon className="h-3 w-3 mr-1" />
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{note.updated_at && (
|
{note.updated_at && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface Note {
|
|||||||
todos: Todo[];
|
todos: Todo[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
files?: NoteFile[];
|
files?: NoteFile[];
|
||||||
|
tags?: Array<{ id: number; name: string }>;
|
||||||
|
priority?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotesService {
|
class NotesService {
|
||||||
@@ -169,6 +171,25 @@ class NotesService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async togglePriority(noteId: number, priority: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/priority`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify({ priority })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la mise à jour de la priorité");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NotesService();
|
export default new NotesService();
|
||||||
Reference in new issue
Block a user