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:
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "project-linkedcorp-backend",
|
||||
"version": "2.10.1",
|
||||
"version": "2.10.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "2.10.1",
|
||||
"version": "2.10.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user