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
|
# 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
|
## [2.10.1] - 2026-10-12
|
||||||
### Fixed
|
### Fixed
|
||||||
- "Probar correo" en Configuración → Correo mostraba "Error interno" en vez de un mensaje
|
- "Probar correo" en Configuración → Correo mostraba "Error interno" en vez de un mensaje
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "2.10.1",
|
"version": "2.10.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { pool } from '../../config/db.js';
|
|||||||
export async function findVisibleToUser(userId) {
|
export async function findVisibleToUser(userId) {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT n.id, n.user_id, n.content, n.created_at, n.updated_at,
|
`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
|
FROM notes n
|
||||||
JOIN users u ON u.id = n.user_id
|
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 = ?)
|
WHERE n.user_id = ? OR n.id IN (SELECT note_id FROM note_shares WHERE user_id = ?)
|
||||||
ORDER BY n.updated_at DESC`,
|
ORDER BY n.updated_at DESC`,
|
||||||
[userId, userId, userId]
|
[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) {
|
export async function findById(id) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.10.1",
|
"version": "2.10.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Button } from '../../components/Button.jsx';
|
|||||||
import { Field } from '../../components/Field.jsx';
|
import { Field } from '../../components/Field.jsx';
|
||||||
import { ComboBox } from '../../components/ComboBox.jsx';
|
import { ComboBox } from '../../components/ComboBox.jsx';
|
||||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||||
|
import { SearchInput } from '../../components/SearchInput.jsx';
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
return String(value).slice(0, 16).replace('T', ' ');
|
return String(value).slice(0, 16).replace('T', ' ');
|
||||||
@@ -26,6 +27,7 @@ export default function NotasPage() {
|
|||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [shares, setShares] = useState([]);
|
const [shares, setShares] = useState([]);
|
||||||
const [sharingWith, setSharingWith] = useState('');
|
const [sharingWith, setSharingWith] = useState('');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -101,20 +103,27 @@ export default function NotasPage() {
|
|||||||
|
|
||||||
if (!notes) return <div className="text-muted">{t('common_loading')}</div>;
|
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 (
|
return (
|
||||||
<div>
|
<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">
|
<div className="notes-grid">
|
||||||
<button type="button" className="notes-card notes-card-new" onClick={openCreate}>
|
<button type="button" className="notes-card notes-card-new" onClick={openCreate}>
|
||||||
<Plus size={28} />
|
<Plus size={28} />
|
||||||
<span>{t('notes_new')}</span>
|
<span>{t('notes_new')}</span>
|
||||||
</button>
|
</button>
|
||||||
{notes.map((note) => (
|
{filteredNotes.map((note) => (
|
||||||
<div key={note.id} className="notes-card" onClick={() => openNote(note)}>
|
<div key={note.id} className="notes-card" onClick={() => openNote(note)}>
|
||||||
<div className="notes-card-preview">{note.content}</div>
|
<div className="notes-card-preview">{note.content}</div>
|
||||||
<div className="notes-card-meta">
|
<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
|
{note.created_at === note.updated_at
|
||||||
? formatDateTime(note.created_at)
|
? formatDateTime(note.created_at)
|
||||||
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
|
: `${t('notes_updated')} ${formatDateTime(note.updated_at)}`}
|
||||||
@@ -125,7 +134,11 @@ export default function NotasPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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
|
<DetailPopup
|
||||||
open={creating || !!selected}
|
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;
|
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 { 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-shared { color: rgb(var(--accent)); }
|
||||||
.notes-card-new {
|
.notes-card-new {
|
||||||
border: 1px dashed rgb(var(--line)); background: transparent; color: rgb(var(--muted));
|
border: 1px dashed rgb(var(--line)); background: transparent; color: rgb(var(--muted));
|
||||||
|
|||||||
Reference in New Issue
Block a user