Cuaderno · Multiusuario y 7 nuevas funciones (PR#56)
Cuaderno · Multiusuario y 7 nuevas funciones (PR#56)
PR#56 mergeado el 2026-05-20 por @Esquembri. Extiende el módulo Cuaderno con sharing colaborativo entre los miembros del staff y añade 6 herramientas de edición adicionales pedidas por Edu. Trabaja sobre la tabla notebook_notes ya existente.
1. Sharing multiusuario
Modelo de datos (migration 0032)
ALTER TABLE notebook_notes ADD COLUMN shared_with TEXT NOT NULL DEFAULT '';
ALTER TABLE notebook_notes ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
Valor de shared_with | Semántica |
|---|---|
'' (vacío) | Nota privada — solo el owner la ve |
'ALL' | Compartida con todo el staff |
'edu@esfericlabs.com,dani@esfericlabs.com' | CSV de emails específicos |
La lógica de acceso en todos los handlers sigue el mismo patrón:
const canAccess =
row.owner_id === me ||
row.shared_with === 'ALL' ||
(row.shared_with !== '' && row.shared_with.includes(me));
Nota de seguridad: el LIKE
%email%se considera seguro porque los emails@esfericlabs.comson únicos y sin solapes de substring con el staff actual (3 personas). Documentado en el comentario de0032_notebook_sharing.sql.
Permisos de operación
| Acción | Owner | Collaborator |
|---|---|---|
| GET nota | ✅ | ✅ |
| PUT contenido/título/pin/archivado | ✅ | ✅ |
PUT shared_with | ✅ | ❌ (403) |
| DELETE nota | ✅ | ❌ (404) |
| POST imagen | ✅ | ✅ |
UX en frontend
- Dropdown “Compartir” en toolbar: opciones Privada / Todo equipo / Edu / Dani / Txell.
- Badge en sidebar:
👤 Danisi la nota es ajena,🔗si es propia y compartida. - Banner en header del editor: “Compartida por X” (nota ajena) · “Compartida · Edu, Dani” (propia compartida).
Detección de ediciones concurrentes
- Polling cada 30 segundos al endpoint
GET /api/notebook/:id/version. - Si el
versiondevuelto es mayor al que el editor tiene en memoria → banner amarillo de aviso (“Otro miembro guardó cambios”). - Modelo last-write-wins: no hay merge automático; la última escritura gana.
2. Undo / Redo — botones visibles
Botones ↶ ↷ en la toolbar del editor. Invocan undo/redo de @codemirror/commands. CodeMirror ya tenía Ctrl+Z ilimitado; los botones añaden descubribilidad para usuarios no técnicos.
3. Toggle autocorrector
- Botón
Aaen toolbar. - Activa/desactiva los atributos de contenido
spellcheck,autocorrectyautocapitalizeviaCompartment+EditorView.contentAttributesde CodeMirror. - Estado persistido en
localStoragepor usuario/dispositivo.
4. Tachado + marca Urgente
| Función | Sintaxis Markdown | Renderizado |
|---|---|---|
| Tachado | ~~texto~~ | |
| Urgente | !!texto!! | <mark class="urgent">⚠ texto</mark> (rojo translúcido + borde izquierdo) |
- El tachado ya existía; se mejoró su tooltip (“ideal para tareas hechas”).
!!texto!!es una extensión custom demarkedañadida en este PR.
5. Emoji picker
- Componente
EmojiPicker.tsx(~180 LOC, ~3 KB, sin dependencias externas). - ~100 emojis en 6 categorías: Caras / Manos / Estados / Trabajo / Tech / Comida.
- Buscador por nombre de categoría. Cierra con
Escapeo click fuera. - Accesible:
role="dialog",aria-label,autoFocusen el input de búsqueda.
6. Fix layout — botones header desbordando
Ajuste CSS puntual en .cuaderno-header-compact .tools-btn:
/* Antes */ padding: 0.5rem 1rem; font-size: 0.82rem;
/* Ahora */ padding: 0.32rem 0.7rem; font-size: 0.74rem;
Los botones de la toolbar ahora encajan en una sola línea en viewports compactos.
7. Insertar imágenes embebidas
Flujo completo de subida e inserción de imágenes en notas:
Botón 🖼 → file input → POST /api/notebook/:id/images
→ validación MIME + tamaño (5MB max)
→ PUT en R2 bucket TASK_ATTACHMENTS (prefix notebook/)
→ INSERT en notebook_attachments
→ inserta  en el editor
MIME whitelist: image/png, image/jpeg, image/webp, image/gif.
Borrado en cascada: al eliminar una nota, el handler DELETE recupera primero todas las r2_key de notebook_attachments y las borra de R2, antes de ejecutar el DELETE SQL (la FK ON DELETE CASCADE limpia las rows pero R2 quedaría huérfano sin este paso).
Endpoints nuevos / modificados
| Método | Ruta | Handler | Cambio |
|---|---|---|---|
| GET | /api/notebook | functions/api/notebook/index.ts | Modificado: lista propias + compartidas |
| POST | /api/notebook | functions/api/notebook/index.ts | Modificado: acepta shared_with |
| GET/PUT/DELETE | /api/notebook/:id | functions/api/notebook/[id]/index.ts | Reemplaza [id].ts eliminado; añade shared_with, version |
| GET | /api/notebook/:id/version | functions/api/notebook/[id]/version.ts | Nuevo — polling de versión |
| POST | /api/notebook/:id/images | functions/api/notebook/[id]/images.ts | Nuevo — subida de imágenes |
| GET | /api/notebook-images/:id | functions/api/notebook-images/[id].ts | Nuevo — stream de imagen con check de acceso |
El fichero
functions/api/notebook/[id].tsfue eliminado y sustituido porfunctions/api/notebook/[id]/index.tspara soportar subrutas (images, version).
Dependencias de infraestructura
| Recurso | Uso |
|---|---|
D1 notebook_notes | Tabla principal — extendida con shared_with + version |
D1 notebook_attachments | Nueva tabla — ver [[entity—notebook—table—notebook-attachments]] |
R2 TASK_ATTACHMENTS | Bucket compartido con tareas; prefix notebook/ para las imágenes de notas |
Véase también
- [[entity—notebook—table—notebook-attachments]]
- [[entity—notebook—endpoint—notebook-images]]