feat: version 2.10.4 -- tarjetas de Notas arrastrables para reordenar

Nueva columna sort_order en notes (migracion 020, mismo patron que 012_sort_order.sql de
activities/tasks). Se reusa el hook useDragReorder ya existente (drag & drop nativo, sin
libreria nueva) -- el icono de agarre va solo en una esquina de la tarjeta, nunca en la
tarjeta completa, para no pisar el click que abre la edicion (mismo truco que ya usa
ListView.jsx: stopPropagation en el handle).

Solo las notas propias son arrastrables/reordenables; las compartidas conmigo quedan
siempre despues, ordenadas por fecha (el orden manual de otro usuario no tiene sentido para
mi vista). Se desactiva mientras hay un filtro de busqueda activo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-12 16:17:51 -05:00
parent 2d1bfc0bfd
commit 0964ffb8a8
12 changed files with 100 additions and 17 deletions
+43 -9
View File
@@ -1,9 +1,10 @@
import { useState } from 'react';
import { Plus, Save, Trash2, Share2, UserPlus } from 'lucide-react';
import { Plus, Save, Trash2, Share2, UserPlus, GripVertical } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { useDragReorder } from '../../hooks/useDragReorder.js';
import {
listNotes, createNote, updateNote, deleteNote, listNoteShares, addNoteShare, removeNoteShare,
listNotes, createNote, updateNote, deleteNote, reorderNotes, listNoteShares, addNoteShare, removeNoteShare,
} from '../../services/notes.js';
import { listUsers } from '../../services/users.js';
import { Button } from '../../components/Button.jsx';
@@ -101,10 +102,20 @@ export default function NotasPage() {
});
}
if (!notes) return <div className="text-muted">{t('common_loading')}</div>;
const query = search.trim().toLowerCase();
const filteredNotes = query ? notes.filter((n) => n.content.toLowerCase().includes(query)) : notes;
const filteredNotes = query ? (notes || []).filter((n) => n.content.toLowerCase().includes(query)) : (notes || []);
const ownNotes = filteredNotes.filter((n) => n.is_owner);
const sharedNotes = filteredNotes.filter((n) => !n.is_owner);
// Solo se puede arrastrar lo propio, y no mientras hay un filtro de busqueda activo (el orden
// de un subconjunto filtrado no tiene un significado claro que persistir).
const dragEnabled = !query;
const { order: orderedOwnNotes, getHandleProps, getRowProps, isDragging } = useDragReorder(ownNotes, async (ids) => {
await reorderNotes(ids);
await reload();
});
const displayedOwnNotes = dragEnabled ? orderedOwnNotes : ownNotes;
if (!notes) return <div className="text-muted">{t('common_loading')}</div>;
return (
<div>
@@ -118,17 +129,40 @@ export default function NotasPage() {
<Plus size={28} />
<span>{t('notes_new')}</span>
</button>
{filteredNotes.map((note) => (
<div key={note.id} className="notes-card" onClick={() => openNote(note)}>
{displayedOwnNotes.map((note) => (
<div
key={note.id}
className={`notes-card ${dragEnabled && isDragging(note.id) ? 'dragging' : ''}`}
onClick={() => openNote(note)}
{...(dragEnabled ? getRowProps(note) : {})}
>
{dragEnabled && (
<span className="notes-card-handle" onClick={(e) => e.stopPropagation()} {...getHandleProps(note)}>
<GripVertical size={14} />
</span>
)}
<div className="notes-card-preview">{note.content}</div>
<div className="notes-card-meta">
<span className="notes-card-meta-row">
{(note.is_owner ? note.share_count > 0 : true) && <Share2 size={13} strokeWidth={2.5} title={t('notes_share_title')} />}
{note.share_count > 0 && <Share2 size={13} strokeWidth={2.5} title={t('notes_share_title')} />}
{note.created_at === note.updated_at
? formatDateTime(note.created_at)
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
</span>
{!note.is_owner && <span className="notes-card-shared">{t('notes_shared_by')} {note.owner_name}</span>}
</div>
</div>
))}
{sharedNotes.map((note) => (
<div key={note.id} className="notes-card" onClick={() => openNote(note)}>
<div className="notes-card-preview">{note.content}</div>
<div className="notes-card-meta">
<span className="notes-card-meta-row">
<Share2 size={13} strokeWidth={2.5} title={t('notes_share_title')} />
{note.created_at === note.updated_at
? formatDateTime(note.created_at)
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
</span>
<span className="notes-card-shared">{t('notes_shared_by')} {note.owner_name}</span>
</div>
</div>
))}