feat: version 2.10.2 -- icono de compartida + buscador en tarjetas de Notas

Tarjetas de Notas: icono junto a la fecha cuando la nota esta compartida (propia compartida
con alguien, via share_count nuevo en la consulta; o recibida de otro usuario). Buscador
junto al titulo que filtra las tarjetas en vivo por contenido (client-side, sin ida y vuelta
al backend ya que el contenido no esta cifrado como en Contraseñas).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-12 15:37:31 -05:00
parent da4d83e7cc
commit 98d9196791
7 changed files with 31 additions and 9 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog - project.linkedcorp.net
## [2.10.2] - 2026-10-12
### Added
- Tarjetas de Notas: ícono junto a la fecha cuando la nota está compartida (ya sea que la
compartiste con alguien, o que te la compartieron a ti).
- Buscador junto al título "Notas" que filtra las tarjetas en vivo por contenido.
## [2.10.1] - 2026-10-12
### Fixed
- "Probar correo" en Configuración → Correo mostraba "Error interno" en vez de un mensaje
+1 -1
View File
@@ -1 +1 @@
2.10.1
2.10.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
"version": "2.10.1",
"version": "2.10.2",
"private": true,
"type": "module",
"main": "src/server.js",
+3 -2
View File
@@ -5,14 +5,15 @@ import { pool } from '../../config/db.js';
export async function findVisibleToUser(userId) {
const [rows] = await pool.query(
`SELECT n.id, n.user_id, n.content, n.created_at, n.updated_at,
(n.user_id = ?) AS is_owner, u.name AS owner_name
(n.user_id = ?) AS is_owner, u.name AS owner_name,
(SELECT COUNT(*) FROM note_shares ns WHERE ns.note_id = n.id) AS share_count
FROM notes n
JOIN users u ON u.id = n.user_id
WHERE n.user_id = ? OR n.id IN (SELECT note_id FROM note_shares WHERE user_id = ?)
ORDER BY n.updated_at DESC`,
[userId, userId, userId]
);
return rows.map((r) => ({ ...r, is_owner: !!r.is_owner }));
return rows.map((r) => ({ ...r, is_owner: !!r.is_owner, share_count: Number(r.share_count) }));
}
export async function findById(id) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "project-linkedcorp-frontend",
"private": true,
"version": "2.10.1",
"version": "2.10.2",
"type": "module",
"scripts": {
"dev": "vite",
+17 -4
View File
@@ -10,6 +10,7 @@ import { Button } from '../../components/Button.jsx';
import { Field } from '../../components/Field.jsx';
import { ComboBox } from '../../components/ComboBox.jsx';
import { DetailPopup } from '../../components/DetailPopup.jsx';
import { SearchInput } from '../../components/SearchInput.jsx';
function formatDateTime(value) {
return String(value).slice(0, 16).replace('T', ' ');
@@ -26,6 +27,7 @@ export default function NotasPage() {
const [content, setContent] = useState('');
const [shares, setShares] = useState([]);
const [sharingWith, setSharingWith] = useState('');
const [search, setSearch] = useState('');
function openCreate() {
setSelected(null);
@@ -101,20 +103,27 @@ 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;
return (
<div>
<h2 className="page-title mt-0" style={{ marginBottom: 16 }}>{t('notes_title')}</h2>
<div className="toolbar" style={{ marginBottom: 16 }}>
<h2 className="page-title mt-0" style={{ marginBottom: 0 }}>{t('notes_title')}</h2>
<SearchInput value={search} onChange={setSearch} />
</div>
<div className="notes-grid">
<button type="button" className="notes-card notes-card-new" onClick={openCreate}>
<Plus size={28} />
<span>{t('notes_new')}</span>
</button>
{notes.map((note) => (
{filteredNotes.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>
<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.created_at === note.updated_at
? formatDateTime(note.created_at)
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
@@ -125,7 +134,11 @@ export default function NotasPage() {
))}
</div>
{notes.length === 0 && <div className="text-muted" style={{ marginTop: 12 }}>{t('notes_empty')}</div>}
{filteredNotes.length === 0 && (
<div className="text-muted" style={{ marginTop: 12 }}>
{query ? t('common_search_no_results') : t('notes_empty')}
</div>
)}
<DetailPopup
open={creating || !!selected}
+2
View File
@@ -378,6 +378,8 @@ table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
}
.notes-card-meta { display: flex; flex-direction: column; gap: 2px; font-size: .68rem; color: rgb(var(--muted)); }
.notes-card-meta-row { display: flex; align-items: center; gap: 5px; }
.notes-card-meta-row svg { color: rgb(var(--accent)); flex: none; }
.notes-card-shared { color: rgb(var(--accent)); }
.notes-card-new {
border: 1px dashed rgb(var(--line)); background: transparent; color: rgb(var(--muted));