mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
vboxes fix
This commit is contained in:
@@ -135,6 +135,9 @@ venv.bak/
|
|||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
|
# NodeModules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
"""
|
||||||
|
Router tag (Label).
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
- GET /labels – elenca i tag del tenant
|
||||||
|
- POST /labels – crea un nuovo tag (admin)
|
||||||
|
- PATCH /labels/{id} – modifica un tag (admin)
|
||||||
|
- DELETE /labels/{id} – elimina un tag (admin)
|
||||||
|
|
||||||
|
- GET /messages/{id}/labels – tag di un messaggio
|
||||||
|
- PUT /messages/{id}/labels – imposta i tag di un messaggio (sostituisce)
|
||||||
|
- POST /messages/{id}/labels/add – aggiunge tag a un messaggio
|
||||||
|
- POST /messages/{id}/labels/remove – rimuove tag da un messaggio
|
||||||
|
|
||||||
|
- POST /messages/bulk-labels – aggiunge/rimuove tag in blocco
|
||||||
|
|
||||||
|
Permessi:
|
||||||
|
- GET /labels: tutti gli utenti autenticati
|
||||||
|
- POST/PATCH/DELETE: solo admin
|
||||||
|
- Operazioni su messaggi: utenti con accesso alla casella del messaggio
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, status
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.core.exceptions import ForbiddenError, NotFoundError
|
||||||
|
from app.dependencies import AdminUser, CurrentUser, DB
|
||||||
|
from app.models.message import Message
|
||||||
|
from app.schemas.label import (
|
||||||
|
LabelCreate,
|
||||||
|
LabelResponse,
|
||||||
|
LabelUpdate,
|
||||||
|
MessageBulkLabelRequest,
|
||||||
|
MessageBulkLabelResponse,
|
||||||
|
MessageLabelAddRequest,
|
||||||
|
MessageLabelRemoveRequest,
|
||||||
|
MessageLabelSetRequest,
|
||||||
|
)
|
||||||
|
from app.services.label_service import LabelService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["Labels"])
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _check_message_access(
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
current_user,
|
||||||
|
db,
|
||||||
|
) -> Message:
|
||||||
|
"""Verifica che il messaggio esista e l'utente vi abbia accesso."""
|
||||||
|
result = await db.execute(
|
||||||
|
select(Message).where(
|
||||||
|
Message.id == message_id,
|
||||||
|
Message.tenant_id == current_user.tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
message = result.scalar_one_or_none()
|
||||||
|
if not message:
|
||||||
|
raise NotFoundError(f"Messaggio {message_id} non trovato")
|
||||||
|
|
||||||
|
if not current_user.is_admin:
|
||||||
|
from app.services.permission_service import PermissionService
|
||||||
|
perm_svc = PermissionService(db)
|
||||||
|
if not await perm_svc.check_can_read(current_user, message.mailbox_id):
|
||||||
|
raise ForbiddenError("Accesso al messaggio non autorizzato")
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
# ─── CRUD Label ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/labels", response_model=list[LabelResponse])
|
||||||
|
async def list_labels(
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> list[LabelResponse]:
|
||||||
|
"""Elenca tutti i tag del tenant corrente."""
|
||||||
|
svc = LabelService(db)
|
||||||
|
labels = await svc.list_labels(current_user.tenant_id)
|
||||||
|
return [LabelResponse.model_validate(l) for l in labels]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/labels", response_model=LabelResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_label(
|
||||||
|
data: LabelCreate,
|
||||||
|
current_user: AdminUser,
|
||||||
|
db: DB,
|
||||||
|
) -> LabelResponse:
|
||||||
|
"""Crea un nuovo tag (solo admin)."""
|
||||||
|
svc = LabelService(db)
|
||||||
|
label = await svc.create_label(current_user.tenant_id, data)
|
||||||
|
return LabelResponse.model_validate(label)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/labels/{label_id}", response_model=LabelResponse)
|
||||||
|
async def update_label(
|
||||||
|
label_id: uuid.UUID,
|
||||||
|
data: LabelUpdate,
|
||||||
|
current_user: AdminUser,
|
||||||
|
db: DB,
|
||||||
|
) -> LabelResponse:
|
||||||
|
"""Modifica un tag esistente (solo admin)."""
|
||||||
|
svc = LabelService(db)
|
||||||
|
label = await svc.update_label(current_user.tenant_id, label_id, data)
|
||||||
|
return LabelResponse.model_validate(label)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/labels/{label_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_label(
|
||||||
|
label_id: uuid.UUID,
|
||||||
|
current_user: AdminUser,
|
||||||
|
db: DB,
|
||||||
|
) -> None:
|
||||||
|
"""Elimina un tag (solo admin). Viene rimosso automaticamente da tutti i messaggi."""
|
||||||
|
svc = LabelService(db)
|
||||||
|
await svc.delete_label(current_user.tenant_id, label_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Tag su singolo messaggio ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/messages/{message_id}/labels", response_model=list[LabelResponse])
|
||||||
|
async def get_message_labels(
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> list[LabelResponse]:
|
||||||
|
"""Elenca i tag assegnati a un messaggio."""
|
||||||
|
await _check_message_access(message_id, current_user, db)
|
||||||
|
svc = LabelService(db)
|
||||||
|
labels = await svc.get_message_labels(message_id, current_user.tenant_id)
|
||||||
|
return [LabelResponse.model_validate(l) for l in labels]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/messages/{message_id}/labels", response_model=list[LabelResponse])
|
||||||
|
async def set_message_labels(
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
data: MessageLabelSetRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> list[LabelResponse]:
|
||||||
|
"""
|
||||||
|
Sostituisce tutti i tag di un messaggio.
|
||||||
|
Passare una lista vuota per rimuovere tutti i tag.
|
||||||
|
"""
|
||||||
|
await _check_message_access(message_id, current_user, db)
|
||||||
|
svc = LabelService(db)
|
||||||
|
labels = await svc.set_message_labels(
|
||||||
|
message_id, current_user.tenant_id, data.label_ids
|
||||||
|
)
|
||||||
|
return [LabelResponse.model_validate(l) for l in labels]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/messages/{message_id}/labels/add", response_model=list[LabelResponse])
|
||||||
|
async def add_message_labels(
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
data: MessageLabelAddRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> list[LabelResponse]:
|
||||||
|
"""Aggiunge tag a un messaggio senza rimuovere quelli esistenti."""
|
||||||
|
await _check_message_access(message_id, current_user, db)
|
||||||
|
svc = LabelService(db)
|
||||||
|
labels = await svc.add_message_labels(
|
||||||
|
message_id, current_user.tenant_id, data.label_ids
|
||||||
|
)
|
||||||
|
return [LabelResponse.model_validate(l) for l in labels]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/messages/{message_id}/labels/remove", response_model=list[LabelResponse])
|
||||||
|
async def remove_message_labels(
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
data: MessageLabelRemoveRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> list[LabelResponse]:
|
||||||
|
"""Rimuove specifici tag da un messaggio."""
|
||||||
|
await _check_message_access(message_id, current_user, db)
|
||||||
|
svc = LabelService(db)
|
||||||
|
labels = await svc.remove_message_labels(
|
||||||
|
message_id, current_user.tenant_id, data.label_ids
|
||||||
|
)
|
||||||
|
return [LabelResponse.model_validate(l) for l in labels]
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Bulk labels ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.post("/messages/bulk-labels", response_model=MessageBulkLabelResponse)
|
||||||
|
async def bulk_labels(
|
||||||
|
data: MessageBulkLabelRequest,
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
) -> MessageBulkLabelResponse:
|
||||||
|
"""
|
||||||
|
Aggiunge o rimuove tag da più messaggi in blocco.
|
||||||
|
|
||||||
|
- action="add": aggiunge i tag ai messaggi indicati
|
||||||
|
- action="remove": rimuove i tag dai messaggi indicati
|
||||||
|
|
||||||
|
I messaggi non accessibili all'utente vengono silenziosamente ignorati.
|
||||||
|
"""
|
||||||
|
if not data.message_ids or not data.label_ids:
|
||||||
|
return MessageBulkLabelResponse(updated=0)
|
||||||
|
|
||||||
|
# Per utenti non-admin filtra per caselle accessibili
|
||||||
|
message_ids = [str(mid) for mid in data.message_ids]
|
||||||
|
if not current_user.is_admin:
|
||||||
|
from app.services.permission_service import PermissionService
|
||||||
|
perm_svc = PermissionService(db)
|
||||||
|
visible = await perm_svc.get_visible_mailboxes(current_user)
|
||||||
|
visible_set = set(visible) if visible else set()
|
||||||
|
|
||||||
|
# Filtra i messaggi per caselle visibili
|
||||||
|
result = await db.execute(
|
||||||
|
select(Message.id).where(
|
||||||
|
Message.id.in_(data.message_ids),
|
||||||
|
Message.tenant_id == current_user.tenant_id,
|
||||||
|
Message.mailbox_id.in_(visible_set),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
filtered_ids = list(result.scalars().all())
|
||||||
|
else:
|
||||||
|
filtered_ids = data.message_ids
|
||||||
|
|
||||||
|
svc = LabelService(db)
|
||||||
|
if data.action == "add":
|
||||||
|
updated = await svc.bulk_add_labels(
|
||||||
|
filtered_ids, current_user.tenant_id, data.label_ids
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
updated = await svc.bulk_remove_labels(
|
||||||
|
filtered_ids, current_user.tenant_id, data.label_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessageBulkLabelResponse(updated=updated)
|
||||||
@@ -23,11 +23,13 @@ from fastapi import APIRouter, Query, status
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy import func, or_, select
|
from sqlalchemy import func, or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.core.exceptions import ForbiddenError, NotFoundError
|
from app.core.exceptions import ForbiddenError, NotFoundError
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.dependencies import CurrentUser, DB
|
from app.dependencies import CurrentUser, DB
|
||||||
|
from app.models.label import Label
|
||||||
from app.models.message import Attachment, Message
|
from app.models.message import Attachment, Message
|
||||||
from app.schemas.message import (
|
from app.schemas.message import (
|
||||||
AttachmentResponse,
|
AttachmentResponse,
|
||||||
@@ -107,12 +109,20 @@ async def _resolve_message(
|
|||||||
current_user,
|
current_user,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""Carica il messaggio e verifica i permessi di accesso."""
|
"""Carica il messaggio e verifica i permessi di accesso.
|
||||||
|
|
||||||
|
L'accesso è consentito se:
|
||||||
|
1. L'utente è admin del tenant, oppure
|
||||||
|
2. L'utente ha un permesso diretto can_read sulla casella, oppure
|
||||||
|
3. L'utente è assegnato a una Virtual Box attiva che include la casella.
|
||||||
|
"""
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Message).where(
|
select(Message)
|
||||||
|
.where(
|
||||||
Message.id == message_id,
|
Message.id == message_id,
|
||||||
Message.tenant_id == current_user.tenant_id,
|
Message.tenant_id == current_user.tenant_id,
|
||||||
)
|
)
|
||||||
|
.options(selectinload(Message.labels))
|
||||||
)
|
)
|
||||||
message = result.scalar_one_or_none()
|
message = result.scalar_one_or_none()
|
||||||
if not message:
|
if not message:
|
||||||
@@ -121,8 +131,39 @@ async def _resolve_message(
|
|||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
from app.services.permission_service import PermissionService
|
from app.services.permission_service import PermissionService
|
||||||
perm_svc = PermissionService(db)
|
perm_svc = PermissionService(db)
|
||||||
if not await perm_svc.check_can_read(current_user, message.mailbox_id):
|
has_direct_access = await perm_svc.check_can_read(current_user, message.mailbox_id)
|
||||||
raise ForbiddenError("Accesso al messaggio non autorizzato")
|
|
||||||
|
if not has_direct_access:
|
||||||
|
# Verifica accesso tramite Virtual Box:
|
||||||
|
# l'utente deve essere assegnato a una VBox attiva
|
||||||
|
# che include la casella del messaggio.
|
||||||
|
from app.models.virtual_box import (
|
||||||
|
VirtualBox,
|
||||||
|
VirtualBoxAssignment,
|
||||||
|
virtual_box_mailboxes,
|
||||||
|
)
|
||||||
|
vbox_result = await db.execute(
|
||||||
|
select(VirtualBox.id)
|
||||||
|
.join(
|
||||||
|
VirtualBoxAssignment,
|
||||||
|
VirtualBox.id == VirtualBoxAssignment.virtual_box_id,
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
virtual_box_mailboxes,
|
||||||
|
VirtualBox.id == virtual_box_mailboxes.c.virtual_box_id,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
VirtualBoxAssignment.user_id == current_user.id,
|
||||||
|
virtual_box_mailboxes.c.mailbox_id == message.mailbox_id,
|
||||||
|
VirtualBox.tenant_id == current_user.tenant_id,
|
||||||
|
VirtualBox.is_active == True,
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
has_vbox_access = vbox_result.scalar_one_or_none() is not None
|
||||||
|
|
||||||
|
if not has_vbox_access:
|
||||||
|
raise ForbiddenError("Accesso al messaggio non autorizzato")
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
@@ -160,7 +201,6 @@ async def list_messages(
|
|||||||
# ── Filtro Virtual Box ────────────────────────────────────────────────────
|
# ── Filtro Virtual Box ────────────────────────────────────────────────────
|
||||||
vbox_rules: list = []
|
vbox_rules: list = []
|
||||||
if vbox_id is not None:
|
if vbox_id is not None:
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from app.models.virtual_box import VirtualBox, VirtualBoxAssignment
|
from app.models.virtual_box import VirtualBox, VirtualBoxAssignment
|
||||||
|
|
||||||
vbox_result = await db.execute(
|
vbox_result = await db.execute(
|
||||||
@@ -258,7 +298,8 @@ async def list_messages(
|
|||||||
|
|
||||||
# Ordinamento e paginazione
|
# Ordinamento e paginazione
|
||||||
q = (
|
q = (
|
||||||
q.order_by(
|
q.options(selectinload(Message.labels))
|
||||||
|
.order_by(
|
||||||
Message.received_at.desc().nullslast(),
|
Message.received_at.desc().nullslast(),
|
||||||
Message.created_at.desc(),
|
Message.created_at.desc(),
|
||||||
)
|
)
|
||||||
|
|||||||
+94
-30
@@ -1,20 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
Router API – Invio PEC (Fase 4).
|
Router API – Invio PEC.
|
||||||
|
|
||||||
Endpoint:
|
Endpoint:
|
||||||
POST /send – invia una nuova PEC (crea Message + SendJob, accoda job)
|
POST /send – invia una PEC via JSON (body_text, senza allegati)
|
||||||
GET /send/jobs – lista job di invio del tenant (paginata)
|
POST /send/multipart – invia una PEC con allegati (multipart/form-data)
|
||||||
GET /send/jobs/{id} – dettaglio di un singolo job
|
GET /send/jobs – lista job di invio del tenant (paginata)
|
||||||
DELETE /send/jobs/{id} – annulla job se ancora pending/retrying
|
GET /send/jobs/{id} – dettaglio di un singolo job
|
||||||
|
DELETE /send/jobs/{id} – annulla job se ancora pending/retrying
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Query, status
|
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile, status
|
||||||
|
|
||||||
from app.core.exceptions import ForbiddenError
|
from app.core.exceptions import ForbiddenError
|
||||||
from app.dependencies import AdminUser, CurrentUser, DB
|
from app.dependencies import CurrentUser, DB
|
||||||
from app.schemas.send import SendJobListResponse, SendJobResponse, SendPecRequest
|
from app.schemas.send import SendJobListResponse, SendJobResponse, SendPecRequest
|
||||||
from app.services.permission_service import PermissionService
|
from app.services.permission_service import PermissionService
|
||||||
from app.services.send_service import SendService
|
from app.services.send_service import SendService
|
||||||
@@ -32,18 +34,16 @@ def _job_response(job) -> SendJobResponse:
|
|||||||
return SendJobResponse.model_validate(job)
|
return SendJobResponse.model_validate(job)
|
||||||
|
|
||||||
|
|
||||||
# ─── Endpoints ────────────────────────────────────────────────────────────────
|
# ─── POST /send (JSON – retrocompatibile) ────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"",
|
"",
|
||||||
response_model=SendJobResponse,
|
response_model=SendJobResponse,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
summary="Invia una PEC",
|
summary="Invia una PEC (JSON, senza allegati)",
|
||||||
description=(
|
description=(
|
||||||
"Crea un messaggio PEC in uscita e accoda il job di invio SMTP. "
|
"Crea un messaggio PEC in uscita e accoda il job di invio SMTP. "
|
||||||
"Il job viene eseguito in background con retry automatico. "
|
"Per inviare allegati utilizzare `POST /send/multipart`."
|
||||||
"Richiede permesso **can_send** sulla casella (gli admin possono inviare da qualsiasi casella del tenant)."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def create_send_job(
|
async def create_send_job(
|
||||||
@@ -54,11 +54,87 @@ async def create_send_job(
|
|||||||
svc = _svc(db)
|
svc = _svc(db)
|
||||||
job = await svc.create_send_job(current_user=current_user, data=data)
|
job = await svc.create_send_job(current_user=current_user, data=data)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
# Refresh per ottenere tutti i valori default dal DB
|
|
||||||
await db.refresh(job)
|
await db.refresh(job)
|
||||||
return _job_response(job)
|
return _job_response(job)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── POST /send/multipart (Form + files) ─────────────────────────────────────
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/multipart",
|
||||||
|
response_model=SendJobResponse,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
summary="Invia una PEC con allegati (multipart/form-data)",
|
||||||
|
description=(
|
||||||
|
"Accetta un form multipart con:\n"
|
||||||
|
"- **data**: JSON string con i campi della PEC (stessa struttura di `POST /send`, "
|
||||||
|
" più il campo opzionale `body_html`)\n"
|
||||||
|
"- **attachments**: zero o più file allegati (max 20 MB ciascuno)\n\n"
|
||||||
|
"Carica gli allegati su MinIO e crea i record `Attachment` nel DB prima di accodare il job."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def create_send_job_multipart(
|
||||||
|
current_user: CurrentUser,
|
||||||
|
db: DB,
|
||||||
|
data: str = Form(
|
||||||
|
...,
|
||||||
|
description="JSON con i dati della PEC (campi SendPecRequest + body_html opzionale)",
|
||||||
|
),
|
||||||
|
attachments: list[UploadFile] = File(
|
||||||
|
default=[],
|
||||||
|
description="File allegati (0 o più, max 20 MB ciascuno)",
|
||||||
|
),
|
||||||
|
) -> SendJobResponse:
|
||||||
|
# ── Parse del JSON ────────────────────────────────────────────────────────
|
||||||
|
try:
|
||||||
|
raw = json.loads(data)
|
||||||
|
pec_data = SendPecRequest.model_validate(raw)
|
||||||
|
# Estrai body_html (non presente nel modello base)
|
||||||
|
body_html: str | None = raw.get("body_html") or None
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail=f"Dati PEC non validi: {exc}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Leggi file allegati ───────────────────────────────────────────────────
|
||||||
|
MAX_FILE_BYTES = 20 * 1024 * 1024 # 20 MB
|
||||||
|
files_data: list[dict] = []
|
||||||
|
|
||||||
|
for upload in attachments:
|
||||||
|
content = await upload.read()
|
||||||
|
if not content:
|
||||||
|
continue # Salta file vuoti
|
||||||
|
if len(content) > MAX_FILE_BYTES:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail=f"Il file '{upload.filename}' supera il limite di 20 MB",
|
||||||
|
)
|
||||||
|
files_data.append(
|
||||||
|
{
|
||||||
|
"filename": upload.filename or "allegato",
|
||||||
|
"content": content,
|
||||||
|
"content_type": upload.content_type or "application/octet-stream",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Sovrascrive body_html se fornito ──────────────────────────────────────
|
||||||
|
if body_html:
|
||||||
|
pec_data.body_html = body_html # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
svc = _svc(db)
|
||||||
|
job = await svc.create_send_job(
|
||||||
|
current_user=current_user,
|
||||||
|
data=pec_data,
|
||||||
|
attachments=files_data if files_data else None,
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(job)
|
||||||
|
return _job_response(job)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── GET /send/jobs ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/jobs",
|
"/jobs",
|
||||||
response_model=SendJobListResponse,
|
response_model=SendJobListResponse,
|
||||||
@@ -76,15 +152,8 @@ async def list_send_jobs(
|
|||||||
description="Filtra per stato: pending | sending | sent | failed | retrying",
|
description="Filtra per stato: pending | sending | sent | failed | retrying",
|
||||||
),
|
),
|
||||||
) -> SendJobListResponse:
|
) -> SendJobListResponse:
|
||||||
"""
|
|
||||||
Elenca i job di invio del tenant.
|
|
||||||
|
|
||||||
Gli admin vedono tutti i job; gli operatori vedono solo i job
|
|
||||||
delle caselle su cui hanno permesso can_read.
|
|
||||||
"""
|
|
||||||
svc = _svc(db)
|
svc = _svc(db)
|
||||||
|
|
||||||
# Filtro opzionale per casella: verifica accesso se non admin
|
|
||||||
if mailbox_id and not current_user.is_admin:
|
if mailbox_id and not current_user.is_admin:
|
||||||
perm_svc = PermissionService(db)
|
perm_svc = PermissionService(db)
|
||||||
if not await perm_svc.check_can_read(current_user, mailbox_id):
|
if not await perm_svc.check_can_read(current_user, mailbox_id):
|
||||||
@@ -105,6 +174,8 @@ async def list_send_jobs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── GET /send/jobs/{id} ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/jobs/{job_id}",
|
"/jobs/{job_id}",
|
||||||
response_model=SendJobResponse,
|
response_model=SendJobResponse,
|
||||||
@@ -115,11 +186,9 @@ async def get_send_job(
|
|||||||
current_user: CurrentUser,
|
current_user: CurrentUser,
|
||||||
db: DB,
|
db: DB,
|
||||||
) -> SendJobResponse:
|
) -> SendJobResponse:
|
||||||
"""Recupera lo stato di un singolo job di invio."""
|
|
||||||
svc = _svc(db)
|
svc = _svc(db)
|
||||||
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
||||||
|
|
||||||
# Verifica accesso alla casella se non admin
|
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
perm_svc = PermissionService(db)
|
perm_svc = PermissionService(db)
|
||||||
if not await perm_svc.check_can_read(current_user, job.mailbox_id):
|
if not await perm_svc.check_can_read(current_user, job.mailbox_id):
|
||||||
@@ -128,6 +197,8 @@ async def get_send_job(
|
|||||||
return _job_response(job)
|
return _job_response(job)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── DELETE /send/jobs/{id} ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/jobs/{job_id}",
|
"/jobs/{job_id}",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
@@ -138,16 +209,9 @@ async def cancel_send_job(
|
|||||||
current_user: CurrentUser,
|
current_user: CurrentUser,
|
||||||
db: DB,
|
db: DB,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Annulla un job di invio se è ancora in stato **pending** o **retrying**.
|
|
||||||
|
|
||||||
Non è possibile annullare un invio già partito (stato sending) o
|
|
||||||
completato (sent).
|
|
||||||
"""
|
|
||||||
svc = _svc(db)
|
svc = _svc(db)
|
||||||
|
|
||||||
# Verifica che l'utente possa agire su questo job
|
|
||||||
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
job = await svc.get_send_job(job_id, current_user.tenant_id)
|
||||||
|
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
perm_svc = PermissionService(db)
|
perm_svc = PermissionService(db)
|
||||||
if not await perm_svc.check_can_send(current_user, job.mailbox_id):
|
if not await perm_svc.check_can_send(current_user, job.mailbox_id):
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,7 @@ from slowapi.errors import RateLimitExceeded
|
|||||||
from slowapi.middleware import SlowAPIMiddleware
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address
|
||||||
|
|
||||||
from app.api.v1 import auth, mailboxes, messages, notifications, permissions, send, tenants, users, virtual_boxes, ws
|
from app.api.v1 import auth, labels, mailboxes, messages, notifications, permissions, send, tenants, users, virtual_boxes, ws
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.core.logging import get_logger, setup_logging
|
from app.core.logging import get_logger, setup_logging
|
||||||
from app.database import engine
|
from app.database import engine
|
||||||
@@ -93,6 +93,7 @@ app.include_router(send.router, prefix=API_PREFIX)
|
|||||||
app.include_router(ws.router, prefix=API_PREFIX)
|
app.include_router(ws.router, prefix=API_PREFIX)
|
||||||
app.include_router(virtual_boxes.router, prefix=API_PREFIX)
|
app.include_router(virtual_boxes.router, prefix=API_PREFIX)
|
||||||
app.include_router(notifications.router, prefix=API_PREFIX)
|
app.include_router(notifications.router, prefix=API_PREFIX)
|
||||||
|
app.include_router(labels.router, prefix=API_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
# ─── Health check ─────────────────────────────────────────────────────────────
|
# ─── Health check ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ class Message(Base):
|
|||||||
children: Mapped[list["Message"]] = relationship(
|
children: Mapped[list["Message"]] = relationship(
|
||||||
"Message", foreign_keys=[parent_message_id]
|
"Message", foreign_keys=[parent_message_id]
|
||||||
)
|
)
|
||||||
|
labels: Mapped[list["Label"]] = relationship( # type: ignore[name-defined]
|
||||||
|
"Label",
|
||||||
|
secondary="message_labels",
|
||||||
|
lazy="select",
|
||||||
|
)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_messages_tenant", "tenant_id"),
|
Index("idx_messages_tenant", "tenant_id"),
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Schemi Pydantic per Label (tag) e operazioni correlate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class LabelCreate(BaseModel):
|
||||||
|
name: str = Field(..., min_length=1, max_length=100)
|
||||||
|
color: Optional[str] = Field(None, pattern=r'^#[0-9A-Fa-f]{6}$')
|
||||||
|
|
||||||
|
|
||||||
|
class LabelUpdate(BaseModel):
|
||||||
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||||
|
color: Optional[str] = Field(None, pattern=r'^#[0-9A-Fa-f]{6}$')
|
||||||
|
|
||||||
|
|
||||||
|
class LabelResponse(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
tenant_id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
color: Optional[str] = None
|
||||||
|
|
||||||
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Richieste per assegnazione tag a messaggi ────────────────────────────────
|
||||||
|
|
||||||
|
class MessageLabelSetRequest(BaseModel):
|
||||||
|
"""Sostituisce tutti i tag di un messaggio con quelli indicati."""
|
||||||
|
label_ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageLabelAddRequest(BaseModel):
|
||||||
|
"""Aggiunge tag a un messaggio senza rimuovere quelli esistenti."""
|
||||||
|
label_ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageLabelRemoveRequest(BaseModel):
|
||||||
|
"""Rimuove specifici tag da un messaggio."""
|
||||||
|
label_ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class MessageBulkLabelRequest(BaseModel):
|
||||||
|
"""Aggiunge o rimuove tag da più messaggi in blocco."""
|
||||||
|
message_ids: list[uuid.UUID]
|
||||||
|
label_ids: list[uuid.UUID]
|
||||||
|
action: str = Field(..., pattern=r'^(add|remove)$')
|
||||||
|
|
||||||
|
|
||||||
|
class MessageBulkLabelResponse(BaseModel):
|
||||||
|
updated: int
|
||||||
@@ -8,6 +8,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from pydantic import BaseModel, model_validator
|
from pydantic import BaseModel, model_validator
|
||||||
|
|
||||||
|
from app.schemas.label import LabelResponse
|
||||||
|
|
||||||
|
|
||||||
class AttachmentResponse(BaseModel):
|
class AttachmentResponse(BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
@@ -50,6 +52,7 @@ class MessageResponse(BaseModel):
|
|||||||
raw_eml_path: Optional[str] = None
|
raw_eml_path: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
labels: list[LabelResponse] = []
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
Service per la gestione delle Label (tag) e la loro assegnazione ai messaggi.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import delete, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.exceptions import ConflictError, NotFoundError
|
||||||
|
from app.models.label import Label, MessageLabel
|
||||||
|
from app.models.message import Message
|
||||||
|
from app.schemas.label import LabelCreate, LabelUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class LabelService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
# ─── CRUD Label ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def list_labels(self, tenant_id: uuid.UUID) -> list[Label]:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(Label)
|
||||||
|
.where(Label.tenant_id == tenant_id)
|
||||||
|
.order_by(Label.name)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_label(self, tenant_id: uuid.UUID, label_id: uuid.UUID) -> Label:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.id == label_id,
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
label = result.scalar_one_or_none()
|
||||||
|
if not label:
|
||||||
|
raise NotFoundError(f"Tag {label_id} non trovato")
|
||||||
|
return label
|
||||||
|
|
||||||
|
async def create_label(self, tenant_id: uuid.UUID, data: LabelCreate) -> Label:
|
||||||
|
# Verifica unicità
|
||||||
|
existing = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
Label.name == data.name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise ConflictError(f"Tag '{data.name}' già esistente")
|
||||||
|
|
||||||
|
label = Label(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
name=data.name,
|
||||||
|
color=data.color,
|
||||||
|
)
|
||||||
|
self.db.add(label)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(label)
|
||||||
|
return label
|
||||||
|
|
||||||
|
async def update_label(
|
||||||
|
self, tenant_id: uuid.UUID, label_id: uuid.UUID, data: LabelUpdate
|
||||||
|
) -> Label:
|
||||||
|
label = await self.get_label(tenant_id, label_id)
|
||||||
|
|
||||||
|
if data.name is not None:
|
||||||
|
# Verifica unicità del nuovo nome
|
||||||
|
existing = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
Label.name == data.name,
|
||||||
|
Label.id != label_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise ConflictError(f"Tag '{data.name}' già esistente")
|
||||||
|
label.name = data.name
|
||||||
|
|
||||||
|
if data.color is not None:
|
||||||
|
label.color = data.color
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(label)
|
||||||
|
return label
|
||||||
|
|
||||||
|
async def delete_label(self, tenant_id: uuid.UUID, label_id: uuid.UUID) -> None:
|
||||||
|
label = await self.get_label(tenant_id, label_id)
|
||||||
|
await self.db.delete(label)
|
||||||
|
await self.db.commit()
|
||||||
|
|
||||||
|
# ─── Assegnazione tag a singolo messaggio ─────────────────────────────────
|
||||||
|
|
||||||
|
async def get_message_labels(
|
||||||
|
self, message_id: uuid.UUID, tenant_id: uuid.UUID
|
||||||
|
) -> list[Label]:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(Label)
|
||||||
|
.join(MessageLabel, Label.id == MessageLabel.label_id)
|
||||||
|
.where(
|
||||||
|
MessageLabel.message_id == message_id,
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
.order_by(Label.name)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def set_message_labels(
|
||||||
|
self,
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
label_ids: list[uuid.UUID],
|
||||||
|
) -> list[Label]:
|
||||||
|
"""Sostituisce tutti i tag di un messaggio con quelli indicati."""
|
||||||
|
# Verifica che i label appartengano al tenant
|
||||||
|
valid_ids: set[uuid.UUID] = set()
|
||||||
|
if label_ids:
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.id.in_(label_ids),
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_ids = {lbl.id for lbl in result.scalars().all()}
|
||||||
|
|
||||||
|
# Rimuovi tutti i tag esistenti dal messaggio
|
||||||
|
await self.db.execute(
|
||||||
|
delete(MessageLabel).where(MessageLabel.message_id == message_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Aggiungi i nuovi tag validi
|
||||||
|
for lbl_id in valid_ids:
|
||||||
|
self.db.add(MessageLabel(message_id=message_id, label_id=lbl_id))
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
return await self.get_message_labels(message_id, tenant_id)
|
||||||
|
|
||||||
|
async def add_message_labels(
|
||||||
|
self,
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
label_ids: list[uuid.UUID],
|
||||||
|
) -> list[Label]:
|
||||||
|
"""Aggiunge tag a un messaggio senza rimuovere quelli esistenti."""
|
||||||
|
if not label_ids:
|
||||||
|
return await self.get_message_labels(message_id, tenant_id)
|
||||||
|
|
||||||
|
# Verifica appartenenza al tenant
|
||||||
|
result = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.id.in_(label_ids),
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_labels = list(result.scalars().all())
|
||||||
|
|
||||||
|
# Carica tag esistenti per evitare duplicati
|
||||||
|
existing_result = await self.db.execute(
|
||||||
|
select(MessageLabel.label_id).where(
|
||||||
|
MessageLabel.message_id == message_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing_ids = set(existing_result.scalars().all())
|
||||||
|
|
||||||
|
for lbl in valid_labels:
|
||||||
|
if lbl.id not in existing_ids:
|
||||||
|
self.db.add(MessageLabel(message_id=message_id, label_id=lbl.id))
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
return await self.get_message_labels(message_id, tenant_id)
|
||||||
|
|
||||||
|
async def remove_message_labels(
|
||||||
|
self,
|
||||||
|
message_id: uuid.UUID,
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
label_ids: list[uuid.UUID],
|
||||||
|
) -> list[Label]:
|
||||||
|
"""Rimuove specifici tag da un messaggio."""
|
||||||
|
if label_ids:
|
||||||
|
await self.db.execute(
|
||||||
|
delete(MessageLabel).where(
|
||||||
|
MessageLabel.message_id == message_id,
|
||||||
|
MessageLabel.label_id.in_(label_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await self.db.commit()
|
||||||
|
return await self.get_message_labels(message_id, tenant_id)
|
||||||
|
|
||||||
|
# ─── Azioni bulk ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def bulk_add_labels(
|
||||||
|
self,
|
||||||
|
message_ids: list[uuid.UUID],
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
label_ids: list[uuid.UUID],
|
||||||
|
) -> int:
|
||||||
|
"""Aggiunge tag a più messaggi in blocco."""
|
||||||
|
if not label_ids or not message_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Verifica label del tenant
|
||||||
|
lbl_result = await self.db.execute(
|
||||||
|
select(Label).where(
|
||||||
|
Label.id.in_(label_ids),
|
||||||
|
Label.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_label_ids = [lbl.id for lbl in lbl_result.scalars().all()]
|
||||||
|
|
||||||
|
# Verifica messaggi del tenant
|
||||||
|
msg_result = await self.db.execute(
|
||||||
|
select(Message.id).where(
|
||||||
|
Message.id.in_(message_ids),
|
||||||
|
Message.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_message_ids = list(msg_result.scalars().all())
|
||||||
|
|
||||||
|
if not valid_label_ids or not valid_message_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Carica coppie esistenti per evitare duplicati
|
||||||
|
existing_result = await self.db.execute(
|
||||||
|
select(MessageLabel).where(
|
||||||
|
MessageLabel.message_id.in_(valid_message_ids),
|
||||||
|
MessageLabel.label_id.in_(valid_label_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing_pairs = {
|
||||||
|
(ml.message_id, ml.label_id) for ml in existing_result.scalars().all()
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg_id in valid_message_ids:
|
||||||
|
for lbl_id in valid_label_ids:
|
||||||
|
if (msg_id, lbl_id) not in existing_pairs:
|
||||||
|
self.db.add(MessageLabel(message_id=msg_id, label_id=lbl_id))
|
||||||
|
|
||||||
|
await self.db.commit()
|
||||||
|
return len(valid_message_ids)
|
||||||
|
|
||||||
|
async def bulk_remove_labels(
|
||||||
|
self,
|
||||||
|
message_ids: list[uuid.UUID],
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
label_ids: list[uuid.UUID],
|
||||||
|
) -> int:
|
||||||
|
"""Rimuove tag da più messaggi in blocco."""
|
||||||
|
if not label_ids or not message_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Verifica messaggi del tenant
|
||||||
|
msg_result = await self.db.execute(
|
||||||
|
select(Message.id).where(
|
||||||
|
Message.id.in_(message_ids),
|
||||||
|
Message.tenant_id == tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_message_ids = list(msg_result.scalars().all())
|
||||||
|
|
||||||
|
if not valid_message_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
await self.db.execute(
|
||||||
|
delete(MessageLabel).where(
|
||||||
|
MessageLabel.message_id.in_(valid_message_ids),
|
||||||
|
MessageLabel.label_id.in_(label_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await self.db.commit()
|
||||||
|
return len(valid_message_ids)
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
SendService – logica di business per l'invio PEC (Fase 4).
|
SendService – logica di business per l'invio PEC.
|
||||||
|
|
||||||
Responsabilità:
|
Responsabilità:
|
||||||
1. Valida permessi (check_can_send) sulla casella selezionata
|
1. Valida permessi (check_can_send) sulla casella selezionata
|
||||||
2. Crea il record Message (direction=outbound, state=queued)
|
2. Crea il record Message (direction=outbound, state=queued)
|
||||||
3. Crea il record SendJob (status=pending)
|
3. [Opzionale] Carica gli allegati su MinIO e crea i record Attachment
|
||||||
4. Enqueue il job arq 'send_pec' tramite il pool Redis/arq
|
4. Crea il record SendJob (status=pending)
|
||||||
5. Ritorna SendJobResponse
|
5. Enqueue il job arq 'send_pec' tramite il pool Redis/arq
|
||||||
|
6. Ritorna SendJob
|
||||||
|
|
||||||
Il worker (arq) gestisce la connessione SMTP, il retry e la scrittura
|
Il worker (arq) gestisce la connessione SMTP, il retry e la scrittura
|
||||||
del raw EML su MinIO.
|
del raw EML su MinIO.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
@@ -21,13 +23,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.core.exceptions import ForbiddenError, NotFoundError
|
from app.core.exceptions import ForbiddenError, NotFoundError
|
||||||
from app.models.mailbox import Mailbox
|
from app.models.mailbox import Mailbox
|
||||||
from app.models.message import Message, SendJob
|
from app.models.message import Attachment, Message, SendJob
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.send import SendJobResponse, SendPecRequest
|
from app.schemas.send import SendJobResponse, SendPecRequest
|
||||||
from app.services.permission_service import PermissionService
|
from app.services.permission_service import PermissionService
|
||||||
|
|
||||||
# ─── Pool arq (singleton lazy) ────────────────────────────────────────────────
|
# ─── Pool arq (singleton lazy) ────────────────────────────────────────────────
|
||||||
# Usato per fare enqueue dei job send_pec senza avviare un worker nel backend.
|
|
||||||
|
|
||||||
_arq_pool = None
|
_arq_pool = None
|
||||||
_arq_pool_lock = asyncio.Lock()
|
_arq_pool_lock = asyncio.Lock()
|
||||||
@@ -68,6 +69,40 @@ async def close_arq_pool() -> None:
|
|||||||
_arq_pool = None
|
_arq_pool = None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Helper: HTML → testo semplice ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _html_to_plain(html: str) -> str:
|
||||||
|
"""
|
||||||
|
Converte HTML in testo semplice usando html.parser della stdlib.
|
||||||
|
Usato per generare la parte text/plain quando viene fornito solo body_html.
|
||||||
|
"""
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
BLOCK_TAGS = {
|
||||||
|
"p", "div", "br", "li", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"blockquote", "pre", "hr", "tr",
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Extractor(HTMLParser):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._parts: list[str] = []
|
||||||
|
|
||||||
|
def handle_data(self, data: str) -> None:
|
||||||
|
self._parts.append(data)
|
||||||
|
|
||||||
|
def handle_starttag(self, tag: str, attrs) -> None: # noqa: ANN001
|
||||||
|
if tag.lower() in BLOCK_TAGS:
|
||||||
|
self._parts.append("\n")
|
||||||
|
|
||||||
|
def text(self) -> str:
|
||||||
|
return "".join(self._parts).strip()
|
||||||
|
|
||||||
|
extractor = _Extractor()
|
||||||
|
extractor.feed(html)
|
||||||
|
return extractor.text()
|
||||||
|
|
||||||
|
|
||||||
# ─── SendService ──────────────────────────────────────────────────────────────
|
# ─── SendService ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class SendService:
|
class SendService:
|
||||||
@@ -80,20 +115,22 @@ class SendService:
|
|||||||
self,
|
self,
|
||||||
current_user: User,
|
current_user: User,
|
||||||
data: SendPecRequest,
|
data: SendPecRequest,
|
||||||
|
attachments: list[dict] | None = None,
|
||||||
) -> SendJob:
|
) -> SendJob:
|
||||||
"""
|
"""
|
||||||
Crea Message + SendJob e accoda il job di invio.
|
Crea Message + (Attachment*) + SendJob e accoda il job di invio.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
current_user: utente autenticato che richiede l'invio
|
current_user: utente autenticato che richiede l'invio
|
||||||
data: dati della PEC da inviare
|
data: dati della PEC da inviare
|
||||||
|
attachments: lista opzionale di dict {filename, content: bytes, content_type}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SendJob appena creato
|
SendJob appena creato
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotFoundError: casella non trovata o non appartenente al tenant
|
NotFoundError: casella non trovata o non appartenente al tenant
|
||||||
ForbiddenError: utente senza can_send sulla casella
|
ForbiddenError: utente senza can_send sulla casella
|
||||||
"""
|
"""
|
||||||
# ── Verifica casella ──────────────────────────────────────────────────
|
# ── Verifica casella ──────────────────────────────────────────────────
|
||||||
mailbox = await self.db.get(Mailbox, data.mailbox_id)
|
mailbox = await self.db.get(Mailbox, data.mailbox_id)
|
||||||
@@ -105,7 +142,6 @@ class SendService:
|
|||||||
raise NotFoundError("casella PEC mittente")
|
raise NotFoundError("casella PEC mittente")
|
||||||
|
|
||||||
if mailbox.status != "active":
|
if mailbox.status != "active":
|
||||||
from app.core.exceptions import ForbiddenError
|
|
||||||
raise ForbiddenError(
|
raise ForbiddenError(
|
||||||
f"La casella è in stato '{mailbox.status}' e non può inviare"
|
f"La casella è in stato '{mailbox.status}' e non può inviare"
|
||||||
)
|
)
|
||||||
@@ -118,8 +154,18 @@ class SendService:
|
|||||||
"Non hai il permesso di inviare da questa casella"
|
"Non hai il permesso di inviare da questa casella"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Ricava body_text ──────────────────────────────────────────────────
|
||||||
|
body_text = data.body_text or ""
|
||||||
|
body_html: str | None = getattr(data, "body_html", None) or data.body_html # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# Se il body_text è vuoto ma abbiamo HTML, genera il plain text
|
||||||
|
if not body_text and body_html:
|
||||||
|
body_text = _html_to_plain(body_html)
|
||||||
|
|
||||||
# ── Crea il messaggio outbound ────────────────────────────────────────
|
# ── Crea il messaggio outbound ────────────────────────────────────────
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
has_files = bool(attachments)
|
||||||
|
|
||||||
message = Message(
|
message = Message(
|
||||||
tenant_id=current_user.tenant_id,
|
tenant_id=current_user.tenant_id,
|
||||||
mailbox_id=data.mailbox_id,
|
mailbox_id=data.mailbox_id,
|
||||||
@@ -130,9 +176,9 @@ class SendService:
|
|||||||
from_address=mailbox.email_address,
|
from_address=mailbox.email_address,
|
||||||
to_addresses=[str(a) for a in data.to_addresses],
|
to_addresses=[str(a) for a in data.to_addresses],
|
||||||
cc_addresses=[str(a) for a in data.cc_addresses] if data.cc_addresses else [],
|
cc_addresses=[str(a) for a in data.cc_addresses] if data.cc_addresses else [],
|
||||||
body_text=data.body_text or "",
|
body_text=body_text,
|
||||||
body_html=data.body_html,
|
body_html=body_html,
|
||||||
has_attachments=False, # allegati in Fase 5
|
has_attachments=has_files,
|
||||||
sent_at=None,
|
sent_at=None,
|
||||||
received_at=None,
|
received_at=None,
|
||||||
)
|
)
|
||||||
@@ -144,7 +190,16 @@ class SendService:
|
|||||||
message.parent_message_id = data.reply_to_message_id
|
message.parent_message_id = data.reply_to_message_id
|
||||||
|
|
||||||
self.db.add(message)
|
self.db.add(message)
|
||||||
await self.db.flush()
|
await self.db.flush() # Ottieni message.id
|
||||||
|
|
||||||
|
# ── Carica allegati su MinIO e crea record Attachment ─────────────────
|
||||||
|
if has_files:
|
||||||
|
await self._upload_attachments(
|
||||||
|
message=message,
|
||||||
|
attachments=attachments, # type: ignore[arg-type]
|
||||||
|
tenant_id=current_user.tenant_id,
|
||||||
|
mailbox_id=data.mailbox_id,
|
||||||
|
)
|
||||||
|
|
||||||
# ── Crea il SendJob ───────────────────────────────────────────────────
|
# ── Crea il SendJob ───────────────────────────────────────────────────
|
||||||
job = SendJob(
|
job = SendJob(
|
||||||
@@ -171,11 +226,70 @@ class SendService:
|
|||||||
f"[send_service] Impossibile enqueue send_pec job {job.id}: {e}. "
|
f"[send_service] Impossibile enqueue send_pec job {job.id}: {e}. "
|
||||||
"Il job resterà in stato 'pending' per pickup manuale."
|
"Il job resterà in stato 'pending' per pickup manuale."
|
||||||
)
|
)
|
||||||
# Non alziamo eccezione: il job è nel DB e verrà processato
|
|
||||||
# dal cron di polling se disponibile
|
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
# ── Upload allegati ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _upload_attachments(
|
||||||
|
self,
|
||||||
|
message: Message,
|
||||||
|
attachments: list[dict],
|
||||||
|
tenant_id: uuid.UUID,
|
||||||
|
mailbox_id: uuid.UUID,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Carica ogni allegato su MinIO e crea il record Attachment nel DB.
|
||||||
|
|
||||||
|
Gestisce gli errori di upload singolarmente: un allegato che non
|
||||||
|
riesce a caricarsi non blocca gli altri.
|
||||||
|
"""
|
||||||
|
from app.core.logging import get_logger
|
||||||
|
from app.storage.minio_client import upload_attachment as minio_upload
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
for att in attachments:
|
||||||
|
filename: str = att.get("filename") or "allegato"
|
||||||
|
content: bytes = att.get("content") or b""
|
||||||
|
content_type: str = att.get("content_type") or "application/octet-stream"
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_path = await minio_upload(
|
||||||
|
tenant_id=str(tenant_id),
|
||||||
|
mailbox_id=str(mailbox_id),
|
||||||
|
message_id=str(message.id),
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
checksum = hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
|
attachment_record = Attachment(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
message_id=message.id,
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type,
|
||||||
|
size_bytes=len(content),
|
||||||
|
storage_path=storage_path,
|
||||||
|
checksum_sha256=checksum,
|
||||||
|
)
|
||||||
|
self.db.add(attachment_record)
|
||||||
|
logger.debug(
|
||||||
|
f"[send_service] Allegato caricato: {storage_path} "
|
||||||
|
f"({len(content)} bytes)"
|
||||||
|
)
|
||||||
|
except Exception as upload_err:
|
||||||
|
logger.error(
|
||||||
|
f"[send_service] Errore upload allegato '{filename}': {upload_err}"
|
||||||
|
)
|
||||||
|
# Non sollevare: l'invio continuerà senza questo allegato
|
||||||
|
|
||||||
|
await self.db.flush()
|
||||||
|
|
||||||
# ── Lista job di invio ────────────────────────────────────────────────────
|
# ── Lista job di invio ────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def list_send_jobs(
|
async def list_send_jobs(
|
||||||
@@ -226,10 +340,7 @@ class SendService:
|
|||||||
job_id: uuid.UUID,
|
job_id: uuid.UUID,
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
) -> SendJob:
|
) -> SendJob:
|
||||||
"""
|
"""Annulla un job di invio se è ancora in stato 'pending' o 'retrying'."""
|
||||||
Annulla un job di invio se è ancora in stato 'pending'.
|
|
||||||
Non cancella il messaggio associato.
|
|
||||||
"""
|
|
||||||
job = await self.get_send_job(job_id, tenant_id)
|
job = await self.get_send_job(job_id, tenant_id)
|
||||||
|
|
||||||
if job.status not in ("pending", "retrying"):
|
if job.status not in ("pending", "retrying"):
|
||||||
@@ -240,7 +351,6 @@ class SendService:
|
|||||||
job.status = "failed"
|
job.status = "failed"
|
||||||
job.last_error = "Annullato dall'utente"
|
job.last_error = "Annullato dall'utente"
|
||||||
|
|
||||||
# Aggiorna anche il messaggio
|
|
||||||
if job.message_id:
|
if job.message_id:
|
||||||
msg = await self.db.get(Message, job.message_id)
|
msg = await self.db.get(Message, job.message_id)
|
||||||
if msg and msg.state in ("queued", "draft"):
|
if msg and msg.state in ("queued", "draft"):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError
|
from app.core.exceptions import ConflictError, ForbiddenError, NotFoundError
|
||||||
from app.models.mailbox import Mailbox
|
from app.models.mailbox import Mailbox
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.virtual_box import VirtualBox, VirtualBoxAssignment, VirtualBoxRule
|
from app.models.virtual_box import VirtualBox, VirtualBoxAssignment, VirtualBoxRule, virtual_box_mailboxes
|
||||||
from app.schemas.virtual_box import (
|
from app.schemas.virtual_box import (
|
||||||
AssignedUserResponse,
|
AssignedUserResponse,
|
||||||
VirtualBoxCreate,
|
VirtualBoxCreate,
|
||||||
@@ -65,10 +65,15 @@ class VirtualBoxService:
|
|||||||
)
|
)
|
||||||
self.db.add(rule)
|
self.db.add(rule)
|
||||||
|
|
||||||
# Associa le caselle reali
|
# Associa le caselle reali (INSERT diretto sulla tabella di associazione
|
||||||
|
# per evitare MissingGreenlet con SQLAlchemy async)
|
||||||
if data.mailbox_ids:
|
if data.mailbox_ids:
|
||||||
mailboxes = await self._load_mailboxes(data.mailbox_ids, tenant_id)
|
valid_mailboxes = await self._load_mailboxes(data.mailbox_ids, tenant_id)
|
||||||
vbox.mailboxes = mailboxes
|
if valid_mailboxes:
|
||||||
|
await self.db.execute(
|
||||||
|
virtual_box_mailboxes.insert(),
|
||||||
|
[{"virtual_box_id": vbox.id, "mailbox_id": mb.id} for mb in valid_mailboxes],
|
||||||
|
)
|
||||||
|
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
return await self._load_full(vbox.id)
|
return await self._load_full(vbox.id)
|
||||||
@@ -136,10 +141,19 @@ class VirtualBoxService:
|
|||||||
if data.is_active is not None:
|
if data.is_active is not None:
|
||||||
vbox.is_active = data.is_active
|
vbox.is_active = data.is_active
|
||||||
|
|
||||||
# Aggiorna le caselle associate se fornito
|
# Aggiorna le caselle associate se fornito (INSERT/DELETE diretti)
|
||||||
if data.mailbox_ids is not None:
|
if data.mailbox_ids is not None:
|
||||||
mailboxes = await self._load_mailboxes(data.mailbox_ids, tenant_id)
|
await self.db.execute(
|
||||||
vbox.mailboxes = mailboxes
|
virtual_box_mailboxes.delete().where(
|
||||||
|
virtual_box_mailboxes.c.virtual_box_id == vbox_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_mailboxes = await self._load_mailboxes(data.mailbox_ids, tenant_id)
|
||||||
|
if valid_mailboxes:
|
||||||
|
await self.db.execute(
|
||||||
|
virtual_box_mailboxes.insert(),
|
||||||
|
[{"virtual_box_id": vbox_id, "mailbox_id": mb.id} for mb in valid_mailboxes],
|
||||||
|
)
|
||||||
|
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
return await self._load_full(vbox_id)
|
return await self._load_full(vbox_id)
|
||||||
@@ -196,8 +210,18 @@ class VirtualBoxService:
|
|||||||
if not vbox or vbox.tenant_id != tenant_id:
|
if not vbox or vbox.tenant_id != tenant_id:
|
||||||
raise NotFoundError("Virtual Box")
|
raise NotFoundError("Virtual Box")
|
||||||
|
|
||||||
mailboxes = await self._load_mailboxes(mailbox_ids, tenant_id)
|
# Sostituzione completa con INSERT/DELETE diretti
|
||||||
vbox.mailboxes = mailboxes
|
await self.db.execute(
|
||||||
|
virtual_box_mailboxes.delete().where(
|
||||||
|
virtual_box_mailboxes.c.virtual_box_id == vbox_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
valid_mailboxes = await self._load_mailboxes(mailbox_ids, tenant_id)
|
||||||
|
if valid_mailboxes:
|
||||||
|
await self.db.execute(
|
||||||
|
virtual_box_mailboxes.insert(),
|
||||||
|
[{"virtual_box_id": vbox_id, "mailbox_id": mb.id} for mb in valid_mailboxes],
|
||||||
|
)
|
||||||
|
|
||||||
await self.db.flush()
|
await self.db.flush()
|
||||||
return await self._load_full(vbox_id)
|
return await self._load_full(vbox_id)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Storage utilities per il backend (MinIO/S3)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""
|
||||||
|
Client MinIO per il backend – upload allegati outbound.
|
||||||
|
|
||||||
|
Percorso allegati outbound:
|
||||||
|
tenants/{tenant_id}/mailboxes/{mailbox_id}/attachments/{message_id}/{filename}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from miniopy_async import Minio
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_minio_client() -> Minio:
|
||||||
|
"""Restituisce l'istanza singleton del client MinIO."""
|
||||||
|
settings = get_settings()
|
||||||
|
return Minio(
|
||||||
|
endpoint=settings.minio_endpoint,
|
||||||
|
access_key=settings.minio_access_key,
|
||||||
|
secret_key=settings.minio_secret_key,
|
||||||
|
secure=settings.minio_use_ssl,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def upload_attachment(
|
||||||
|
tenant_id: str,
|
||||||
|
mailbox_id: str,
|
||||||
|
message_id: str,
|
||||||
|
filename: str,
|
||||||
|
content: bytes,
|
||||||
|
content_type: str = "application/octet-stream",
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Carica un allegato outbound su MinIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tenant_id: UUID del tenant (stringa)
|
||||||
|
mailbox_id: UUID della casella mittente
|
||||||
|
message_id: UUID del messaggio associato
|
||||||
|
filename: Nome file originale
|
||||||
|
content: Byte del file
|
||||||
|
content_type: MIME type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Percorso oggetto su MinIO (senza nome bucket)
|
||||||
|
"""
|
||||||
|
settings = get_settings()
|
||||||
|
client = get_minio_client()
|
||||||
|
bucket = settings.minio_bucket
|
||||||
|
|
||||||
|
safe_filename = _sanitize_filename(filename)
|
||||||
|
object_path = (
|
||||||
|
f"tenants/{tenant_id}/mailboxes/{mailbox_id}"
|
||||||
|
f"/attachments/{message_id}/{safe_filename}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data_stream = io.BytesIO(content)
|
||||||
|
await client.put_object(
|
||||||
|
bucket_name=bucket,
|
||||||
|
object_name=object_path,
|
||||||
|
data=data_stream,
|
||||||
|
length=len(content),
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Allegato outbound caricato: {object_path} "
|
||||||
|
f"({len(content)} bytes, {content_type})"
|
||||||
|
)
|
||||||
|
return object_path
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_filename(filename: str) -> str:
|
||||||
|
"""Sanitizza il nome file per uso sicuro come path MinIO."""
|
||||||
|
safe = filename.replace("/", "_").replace("\\", "_").replace("\x00", "")
|
||||||
|
safe = re.sub(r"[^\w.\-() ]", "_", safe, flags=re.UNICODE)
|
||||||
|
if len(safe) > 200:
|
||||||
|
parts = safe.rsplit(".", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
safe = parts[0][:196] + "." + parts[1]
|
||||||
|
else:
|
||||||
|
safe = safe[:200]
|
||||||
|
return safe or "attachment"
|
||||||
+858
-3
@@ -2150,6 +2150,12 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@remirror/core-constants": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||||
@@ -2180,6 +2186,20 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.59.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
|
||||||
|
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.91.0",
|
"version": "5.91.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.0.tgz",
|
||||||
@@ -2233,6 +2253,505 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-color": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-+OT9wWEJnqoWmzfqPYt0oWm8LZcH+D44Z3jA2TNzBj4tLGQ2YPxN2SyS12AlRi7MuguVT7utFy7qDXrfir8eUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-font-family": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-u5HjpNVBK7N9glR4Sz/HyVvTTAprEiion0oyyBWPBlgZvLrJta0zNvhfwG9ZUoubvqou3fBRbZwVosfonN2fAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-6ZuRyClIyCimXu+S5LQ54DueEsYg5VOVOmubOVbG+WAjM9svn9Z8gv2sNDah2yEqXrX06B02zYcSyMiD7CHbfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-style": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-collab": "^1.3.1",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
|
"prosemirror-keymap": "^1.2.2",
|
||||||
|
"prosemirror-markdown": "^1.13.1",
|
||||||
|
"prosemirror-menu": "^1.2.4",
|
||||||
|
"prosemirror-model": "^1.24.1",
|
||||||
|
"prosemirror-schema-basic": "^1.2.3",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.3",
|
||||||
|
"prosemirror-tables": "^1.6.4",
|
||||||
|
"prosemirror-trailing-node": "^3.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2",
|
||||||
|
"prosemirror-view": "^1.38.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/react": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"fast-equals": "^5.3.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.20.4",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.20.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/extension-blockquote": "^3.20.4",
|
||||||
|
"@tiptap/extension-bold": "^3.20.4",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-code": "^3.20.4",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.4",
|
||||||
|
"@tiptap/extension-document": "^3.20.4",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.20.4",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.20.4",
|
||||||
|
"@tiptap/extension-hard-break": "^3.20.4",
|
||||||
|
"@tiptap/extension-heading": "^3.20.4",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.20.4",
|
||||||
|
"@tiptap/extension-italic": "^3.20.4",
|
||||||
|
"@tiptap/extension-link": "^3.20.4",
|
||||||
|
"@tiptap/extension-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-list-item": "^3.20.4",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.20.4",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-paragraph": "^3.20.4",
|
||||||
|
"@tiptap/extension-strike": "^3.20.4",
|
||||||
|
"@tiptap/extension-text": "^3.20.4",
|
||||||
|
"@tiptap/extension-underline": "^3.20.4",
|
||||||
|
"@tiptap/extensions": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2285,6 +2804,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.15",
|
"version": "22.19.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||||
@@ -2299,14 +2840,12 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.28",
|
"version": "18.3.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@@ -2317,12 +2856,17 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -2485,6 +3029,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
@@ -2803,6 +3353,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -2910,6 +3466,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -3011,6 +3579,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
@@ -3031,6 +3611,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@@ -3396,6 +3985,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -3444,6 +4048,23 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.0",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -3453,6 +4074,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -3573,6 +4200,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
@@ -3832,12 +4465,216 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-collab": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-menu": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crelt": "^1.0.0",
|
||||||
|
"prosemirror-commands": "^1.0.0",
|
||||||
|
"prosemirror-history": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-basic": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-trailing-node": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remirror/core-constants": "3.0.0",
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prosemirror-model": "^1.22.1",
|
||||||
|
"prosemirror-state": "^1.4.2",
|
||||||
|
"prosemirror-view": "^1.33.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
|
||||||
|
"integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qrcode.react": {
|
"node_modules/qrcode.react": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
@@ -4137,6 +4974,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -4450,6 +5293,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -4696,6 +5545,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/why-is-node-running": {
|
"node_modules/why-is-node-running": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
|||||||
Generated
+854
-3
@@ -21,6 +21,16 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@tanstack/react-query-devtools": "^5.56.2",
|
"@tanstack/react-query-devtools": "^5.56.2",
|
||||||
|
"@tiptap/extension-color": "^3.20.4",
|
||||||
|
"@tiptap/extension-font-family": "^3.20.4",
|
||||||
|
"@tiptap/extension-link": "^3.20.4",
|
||||||
|
"@tiptap/extension-placeholder": "^3.20.4",
|
||||||
|
"@tiptap/extension-text-align": "^3.20.4",
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4",
|
||||||
|
"@tiptap/extension-underline": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4",
|
||||||
|
"@tiptap/react": "^3.20.4",
|
||||||
|
"@tiptap/starter-kit": "^3.20.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -2569,6 +2579,12 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@remirror/core-constants": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||||
@@ -2988,6 +3004,505 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-9sskyyhYj2oKat//lyZVXCp9YrPt4oJAZnGHYWXS0xlskjsLElrfKKlM4vpbhGss3VrhQRoEGqWLnIaJYPF1zw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-Md7/mNAeJCY+VLJc8JRGI+8XkVPKiOGB1NgqQPdh3aYtxXQDChQOZoJEQl6TuudDxZ85bLZB67NjZlx3jo8/0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-EXywPlI8wjPcAb8ozymgVhjtMjFrnhtoyNTy8ZcObdpUi5CdO9j892Y7aPbKe5hLhlDpvJk7rMfir4FFKEmfng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-1RTGrur1EKoxfnLZ3M6xeNj8GITAz74jH2DHGcjLsd2Xr7Q7BozGaIq6GkkvKguMwbI1zCOxTHFCpUETXAIQQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-7j8Hi964bH1SZ9oLdZC1fkqWz27mliSDV7M8lmL/M14+Qw42D/VOAKS4Aw9OCFtHMlTsjLR6qsoVxL8Lpkt6NA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-Zlw3FrXTy01+o1yISeX/LC+iJeHA+ym602bMXGmtA6lyl7QSOSO7WExweJ6xeJGhbCjldwT5al6fkRAs8iGJZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-color": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-+OT9wWEJnqoWmzfqPYt0oWm8LZcH+D44Z3jA2TNzBj4tLGQ2YPxN2SyS12AlRi7MuguVT7utFy7qDXrfir8eUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-zF1CIFVLt8MfSpWWnPwtGyxPOsT0xYM2qJKcXf2yZcTG37wDKmUi6heG53vGigIavbQlLaAFvs+1mNdOu2x/0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-TgMwvZ8myXYdmd6bUV7qkpZXv7ZUiSmX/8eo+iPEzYo2CnDLAGvDKgC50nfq/g87SDvfBgPuAiBfFvsMQQWaTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-AaPTFhoO8DBIElJyd/RTVJjkctvJuL+GHURX0npbtTxXq5HXbebVwf2ARNR7jMd/GThsmBaNJiGxZg4A2oeDqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-font-family": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-u5HjpNVBK7N9glR4Sz/HyVvTTAprEiion0oyyBWPBlgZvLrJta0zNvhfwG9ZUoubvqou3fBRbZwVosfonN2fAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-JJ6f1iQ1e0s4kISgq55U3UYGwWV/N9f0PYMtB6e3L+SBQjXnywaLK0g6vfN6IvTCC2vdIuqeSOX8VlSO97sJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-gJbq58d8zB1gzyqVEopowej5CpW4/Fpg6oGJvlZxaCukqd0gJRWGC89K+jE62YA1Td4sfcKrekKvN7jm2y/ZUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-xsnkmTGggJc5P2iCwS1lv8KFG31xC/GNPJKoi/3UH67j/lKDhA3AdtshsLeyv2FKtTtYDb8oV0IqzHB1MM6a7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-y6joCi49haAA0bo3EGUY+dWUMHH1GPUc84hxrBY/0pMs+Bn+kQ1+DQJErZDTWGJrlHPWU/yekBZT72SNdp0DNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-4ZqiWr7cmqPFux8tj1ZLiYytyWf343IvQemNX6AvVWvscrJcrfj3YX4Le2BA0RW3A3M6RpLQXXozuF8vxYFDeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-JNDSkWrVdb8NSvbQXwHWvK5tCMbTWwOHFOweknQZ1JPK4dei9FJVofYQaHyW4bJBdcCjds3NZSnXE8DM9iAWmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-QoTc5RACXaZF+vIIBBxjGO7D0oWFUDgBKJCpvUZ0CoGGKosnfe4a9I5THFyLj4201cf0oUqgf1oZhTqETGxlVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-RIqXM649+8IP7p/KVfaGlJiwjCylm1m6OPlaoM3K8O7oEOGRQzNeexexECCD2jsXRxew4E+vBNMD2orXqJmu8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-3budNL8BgBon3TcXZ4hjT0YpFvx1Ka3uSIECKDxHgES+OQcR+6cagxSb60gFEccf3Dr0PIwcVTY6g14lC1qKRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-lm6fOScWuZAF/Sfp97igUwFd3L1QHIVLAWP5NVdh0DTLrEIt4rMBmsww+yOpMQRhvz2uTgMbMXynrimhzi/QVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-GB0KWtqm83YHG8cnqBLijvUBm+xvLfQHDfFRRH2fb3EzH3eIsM9jKRC31ADT27RSV1zVpHMFGcP3/pWpdrN1Lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-It1Px9uDGTsVqyyg6cy7DigLoenljpQwqdI0jssM7QclZrHnsrye9fZxBBiiuCzzV1305MxKgHvratkHwqmVNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-jchJcBZixDEO2J66Zx5dchsI2mA6IYsROqF8P1poxL4ienH7RVQRCTsBNnSfIeOtREKKWeOU/tEs5fcpvvGwIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-6ZuRyClIyCimXu+S5LQ54DueEsYg5VOVOmubOVbG+WAjM9svn9Z8gv2sNDah2yEqXrX06B02zYcSyMiD7CHbfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-style": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-0OjMc3FDujX16G+jhvqcY/mLot8SrNtDu8ggUwNLAfiI/QIvMVgk7giFD71DATC/4Nb8i/iwAEegTD8MxBIXCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-collab": "^1.3.1",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
|
"prosemirror-keymap": "^1.2.2",
|
||||||
|
"prosemirror-markdown": "^1.13.1",
|
||||||
|
"prosemirror-menu": "^1.2.4",
|
||||||
|
"prosemirror-model": "^1.24.1",
|
||||||
|
"prosemirror-schema-basic": "^1.2.3",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.3",
|
||||||
|
"prosemirror-tables": "^1.6.4",
|
||||||
|
"prosemirror-trailing-node": "^3.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2",
|
||||||
|
"prosemirror-view": "^1.38.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/react": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-1B8iWsHWwb5TeyVaUs8BRPzwWo4PsLQcl03urHaz0zTJ8DauopqvxzV3+lem1OkzRHn7wnrapDvwmIGoROCaQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"fast-equals": "^5.3.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.20.4",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.20.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.4.tgz",
|
||||||
|
"integrity": "sha512-WcyK6hsTl8eBsQhQ+d9Sq8fYZKOYdL+D45MyH3hz583elXqJlW3h3JPFYb0o87gddGxn8Mm57OA/gA1zEdeDMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.20.4",
|
||||||
|
"@tiptap/extension-blockquote": "^3.20.4",
|
||||||
|
"@tiptap/extension-bold": "^3.20.4",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-code": "^3.20.4",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.4",
|
||||||
|
"@tiptap/extension-document": "^3.20.4",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.20.4",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.20.4",
|
||||||
|
"@tiptap/extension-hard-break": "^3.20.4",
|
||||||
|
"@tiptap/extension-heading": "^3.20.4",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.20.4",
|
||||||
|
"@tiptap/extension-italic": "^3.20.4",
|
||||||
|
"@tiptap/extension-link": "^3.20.4",
|
||||||
|
"@tiptap/extension-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-list-item": "^3.20.4",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.20.4",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.20.4",
|
||||||
|
"@tiptap/extension-paragraph": "^3.20.4",
|
||||||
|
"@tiptap/extension-strike": "^3.20.4",
|
||||||
|
"@tiptap/extension-text": "^3.20.4",
|
||||||
|
"@tiptap/extension-underline": "^3.20.4",
|
||||||
|
"@tiptap/extensions": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -3040,6 +3555,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.15",
|
"version": "22.19.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||||
@@ -3054,14 +3591,12 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.28",
|
"version": "18.3.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@@ -3072,12 +3607,17 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -3240,6 +3780,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
@@ -3558,6 +4104,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -3665,6 +4217,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -3766,6 +4330,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
@@ -3786,6 +4362,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@@ -4166,6 +4751,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/loose-envify": {
|
"node_modules/loose-envify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
@@ -4214,6 +4814,23 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.0",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -4223,6 +4840,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -4343,6 +4966,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
@@ -4602,12 +5231,216 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-collab": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-menu": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crelt": "^1.0.0",
|
||||||
|
"prosemirror-commands": "^1.0.0",
|
||||||
|
"prosemirror-history": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-basic": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-trailing-node": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remirror/core-constants": "3.0.0",
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prosemirror-model": "^1.22.1",
|
||||||
|
"prosemirror-state": "^1.4.2",
|
||||||
|
"prosemirror-view": "^1.33.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
|
||||||
|
"integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qrcode.react": {
|
"node_modules/qrcode.react": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
@@ -4907,6 +5740,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -5220,6 +6059,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -5481,6 +6326,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/why-is-node-running": {
|
"node_modules/why-is-node-running": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
|||||||
+25
-15
@@ -13,7 +13,31 @@
|
|||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
|
"@tanstack/react-query": "^5.56.2",
|
||||||
|
"@tanstack/react-query-devtools": "^5.56.2",
|
||||||
|
"@tiptap/extension-color": "^3.20.4",
|
||||||
|
"@tiptap/extension-font-family": "^3.20.4",
|
||||||
|
"@tiptap/extension-link": "^3.20.4",
|
||||||
|
"@tiptap/extension-placeholder": "^3.20.4",
|
||||||
|
"@tiptap/extension-text-align": "^3.20.4",
|
||||||
|
"@tiptap/extension-text-style": "^3.20.4",
|
||||||
|
"@tiptap/extension-underline": "^3.20.4",
|
||||||
|
"@tiptap/pm": "^3.20.4",
|
||||||
|
"@tiptap/react": "^3.20.4",
|
||||||
|
"@tiptap/starter-kit": "^3.20.4",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"lucide-react": "^0.441.0",
|
"lucide-react": "^0.441.0",
|
||||||
@@ -24,21 +48,7 @@
|
|||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"zustand": "^5.0.0",
|
"zustand": "^5.0.0"
|
||||||
"@tanstack/react-query": "^5.56.2",
|
|
||||||
"@tanstack/react-query-devtools": "^5.56.2",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
|
||||||
"@radix-ui/react-switch": "^1.1.1",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
||||||
"class-variance-authority": "^0.7.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import apiClient from './client'
|
||||||
|
import type {
|
||||||
|
LabelCreate,
|
||||||
|
LabelResponse,
|
||||||
|
LabelUpdate,
|
||||||
|
MessageBulkLabelRequest,
|
||||||
|
MessageBulkLabelResponse,
|
||||||
|
MessageLabelAddRequest,
|
||||||
|
MessageLabelRemoveRequest,
|
||||||
|
MessageLabelSetRequest,
|
||||||
|
} from '@/types/api.types'
|
||||||
|
|
||||||
|
export const labelsApi = {
|
||||||
|
// ─── CRUD Tag ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
list: () =>
|
||||||
|
apiClient.get<LabelResponse[]>('/labels').then((r) => r.data),
|
||||||
|
|
||||||
|
create: (data: LabelCreate) =>
|
||||||
|
apiClient.post<LabelResponse>('/labels', data).then((r) => r.data),
|
||||||
|
|
||||||
|
update: (id: string, data: LabelUpdate) =>
|
||||||
|
apiClient.patch<LabelResponse>(`/labels/${id}`, data).then((r) => r.data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
apiClient.delete(`/labels/${id}`).then((r) => r.data),
|
||||||
|
|
||||||
|
// ─── Tag su singolo messaggio ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
getMessageLabels: (messageId: string) =>
|
||||||
|
apiClient
|
||||||
|
.get<LabelResponse[]>(`/messages/${messageId}/labels`)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
/** Sostituisce tutti i tag di un messaggio. */
|
||||||
|
setMessageLabels: (messageId: string, data: MessageLabelSetRequest) =>
|
||||||
|
apiClient
|
||||||
|
.put<LabelResponse[]>(`/messages/${messageId}/labels`, data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
/** Aggiunge tag a un messaggio senza rimuovere quelli esistenti. */
|
||||||
|
addMessageLabels: (messageId: string, data: MessageLabelAddRequest) =>
|
||||||
|
apiClient
|
||||||
|
.post<LabelResponse[]>(`/messages/${messageId}/labels/add`, data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
/** Rimuove specifici tag da un messaggio. */
|
||||||
|
removeMessageLabels: (messageId: string, data: MessageLabelRemoveRequest) =>
|
||||||
|
apiClient
|
||||||
|
.post<LabelResponse[]>(`/messages/${messageId}/labels/remove`, data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
// ─── Bulk ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bulkLabels: (data: MessageBulkLabelRequest) =>
|
||||||
|
apiClient
|
||||||
|
.post<MessageBulkLabelResponse>('/messages/bulk-labels', data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
}
|
||||||
@@ -69,8 +69,25 @@ export const messagesApi = {
|
|||||||
getAttachments: (id: string) =>
|
getAttachments: (id: string) =>
|
||||||
apiClient.get<AttachmentResponse[]>(`/messages/${id}/attachments`).then((r) => r.data),
|
apiClient.get<AttachmentResponse[]>(`/messages/${id}/attachments`).then((r) => r.data),
|
||||||
|
|
||||||
getAttachmentUrl: (messageId: string, attachmentId: string) =>
|
/**
|
||||||
`/api/v1/messages/${messageId}/attachments/${attachmentId}/download`,
|
* Scarica un allegato autenticato e lo salva localmente.
|
||||||
|
* Utilizza apiClient (con Bearer token) per evitare il 401 che si ottiene
|
||||||
|
* navigando direttamente verso l'URL con un <a href>.
|
||||||
|
*/
|
||||||
|
downloadAttachment: async (messageId: string, attachmentId: string, filename: string): Promise<void> => {
|
||||||
|
const response = await apiClient.get(
|
||||||
|
`/messages/${messageId}/attachments/${attachmentId}/download`,
|
||||||
|
{ responseType: 'blob' },
|
||||||
|
)
|
||||||
|
const blobUrl = window.URL.createObjectURL(new Blob([response.data]))
|
||||||
|
const anchor = document.createElement('a')
|
||||||
|
anchor.href = blobUrl
|
||||||
|
anchor.setAttribute('download', filename)
|
||||||
|
document.body.appendChild(anchor)
|
||||||
|
anchor.click()
|
||||||
|
anchor.remove()
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
},
|
||||||
|
|
||||||
getReceipts: (id: string) =>
|
getReceipts: (id: string) =>
|
||||||
apiClient.get<MessageResponse[]>(`/messages/${id}/receipts`).then((r) => r.data),
|
apiClient.get<MessageResponse[]>(`/messages/${id}/receipts`).then((r) => r.data),
|
||||||
|
|||||||
@@ -12,10 +12,33 @@ export interface SendJobFilters {
|
|||||||
status?: string
|
status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Payload esteso per l'invio multipart (include body_html) */
|
||||||
|
export interface SendPecMultipartRequest extends SendPecRequest {
|
||||||
|
body_html?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const sendApi = {
|
export const sendApi = {
|
||||||
|
/** Invio PEC semplice (JSON, senza allegati) – retrocompatibile */
|
||||||
send: (data: SendPecRequest) =>
|
send: (data: SendPecRequest) =>
|
||||||
apiClient.post<SendJobResponse>('/send', data).then((r) => r.data),
|
apiClient.post<SendJobResponse>('/send', data).then((r) => r.data),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invio PEC con allegati tramite multipart/form-data.
|
||||||
|
*
|
||||||
|
* Il campo `data` viene serializzato come JSON string;
|
||||||
|
* i file vengono appesi come `attachments[]`.
|
||||||
|
*/
|
||||||
|
sendMultipart: (data: SendPecMultipartRequest, files: File[] = []) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('data', JSON.stringify(data))
|
||||||
|
files.forEach((file) => formData.append('attachments', file))
|
||||||
|
return apiClient
|
||||||
|
.post<SendJobResponse>('/send/multipart', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
.then((r) => r.data)
|
||||||
|
},
|
||||||
|
|
||||||
listJobs: (filters: SendJobFilters = {}) =>
|
listJobs: (filters: SendJobFilters = {}) =>
|
||||||
apiClient.get<SendJobListResponse>('/send/jobs', { params: filters }).then((r) => r.data),
|
apiClient.get<SendJobListResponse>('/send/jobs', { params: filters }).then((r) => r.data),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
/**
|
||||||
|
* RichTextEditor – editor di testo ricco in stile Roundcube.
|
||||||
|
*
|
||||||
|
* Usa TipTap (ProseMirror) con estensioni per:
|
||||||
|
* - Formattazione testo: grassetto, corsivo, sottolineato, barrato
|
||||||
|
* - Colore testo (palette + picker custom)
|
||||||
|
* - Titoli H1/H2/H3
|
||||||
|
* - Elenchi puntati, numerati, citazioni
|
||||||
|
* - Allineamento testo
|
||||||
|
* - Collegamento ipertestuale
|
||||||
|
* - Undo/Redo
|
||||||
|
* - Rimuovi formattazione
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import UnderlineExt from '@tiptap/extension-underline'
|
||||||
|
import TextAlign from '@tiptap/extension-text-align'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
|
import { TextStyle } from '@tiptap/extension-text-style'
|
||||||
|
import Color from '@tiptap/extension-color'
|
||||||
|
import { useEffect, useCallback, useState, useRef } from 'react'
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Underline,
|
||||||
|
Strikethrough,
|
||||||
|
AlignLeft,
|
||||||
|
AlignCenter,
|
||||||
|
AlignRight,
|
||||||
|
AlignJustify,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
Quote,
|
||||||
|
Link as LinkIcon,
|
||||||
|
Link2Off,
|
||||||
|
Undo2,
|
||||||
|
Redo2,
|
||||||
|
Baseline,
|
||||||
|
Eraser,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
// ─── Tipi ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface RichTextEditorProps {
|
||||||
|
value: string
|
||||||
|
onChange: (html: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
minHeight?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Palette colori ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const COLOR_PALETTE = [
|
||||||
|
// Grigi
|
||||||
|
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#d9d9d9', '#ffffff',
|
||||||
|
// Vivaci
|
||||||
|
'#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
|
||||||
|
// Chiari
|
||||||
|
'#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#cfe2f3', '#d9d2e9', '#ead1dc',
|
||||||
|
// Standard
|
||||||
|
'#ea4335', '#fbbc04', '#34a853', '#4285f4', '#9334e6', '#c2185b', '#e65100', '#00838f',
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Sub-componenti toolbar ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ToolbarButtonProps {
|
||||||
|
onClick: () => void
|
||||||
|
isActive?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
title: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButton({ onClick, isActive, disabled, title, children }: ToolbarButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
className={[
|
||||||
|
'flex items-center justify-center w-7 h-7 rounded text-sm transition-colors select-none',
|
||||||
|
isActive
|
||||||
|
? 'bg-primary/15 text-primary'
|
||||||
|
: 'text-foreground hover:bg-muted',
|
||||||
|
disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarSeparator() {
|
||||||
|
return <div className="w-px h-5 bg-border mx-0.5 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Color Picker ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ColorPickerProps {
|
||||||
|
currentColor?: string
|
||||||
|
onSelect: (color: string) => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ColorPicker({ currentColor, onSelect, onClose }: ColorPickerProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delay to avoid closing immediately on the button click that opened us
|
||||||
|
const timer = setTimeout(() => document.addEventListener('mousedown', handler), 50)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
document.removeEventListener('mousedown', handler)
|
||||||
|
}
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="absolute z-50 top-full left-0 mt-1 p-2.5 bg-background border rounded-lg shadow-xl min-w-max"
|
||||||
|
>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1.5 font-medium">Colore testo</div>
|
||||||
|
<div className="grid grid-cols-8 gap-1 mb-2">
|
||||||
|
{COLOR_PALETTE.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
type="button"
|
||||||
|
className={[
|
||||||
|
'w-5 h-5 rounded-sm border transition-transform hover:scale-110',
|
||||||
|
currentColor === color ? 'ring-2 ring-primary ring-offset-1' : 'border-border/40',
|
||||||
|
].join(' ')}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={() => { onSelect(color); onClose() }}
|
||||||
|
title={color}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 pt-1.5 border-t">
|
||||||
|
<label className="text-xs text-muted-foreground">Personalizzato:</label>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="w-7 h-6 rounded cursor-pointer border border-border"
|
||||||
|
defaultValue={currentColor || '#000000'}
|
||||||
|
onChange={(e) => onSelect(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground underline ml-auto"
|
||||||
|
onClick={() => { onSelect(''); onClose() }}
|
||||||
|
>
|
||||||
|
Rimuovi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Link Dialog ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface LinkDialogProps {
|
||||||
|
currentUrl?: string
|
||||||
|
onConfirm: (url: string) => void
|
||||||
|
onRemove: () => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkDialog({ currentUrl, onConfirm, onRemove, onClose }: LinkDialogProps) {
|
||||||
|
const [url, setUrl] = useState(currentUrl || 'https://')
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => inputRef.current?.focus(), 50)
|
||||||
|
const handler = (e: MouseEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) onClose()
|
||||||
|
}
|
||||||
|
setTimeout(() => document.addEventListener('mousedown', handler), 50)
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer)
|
||||||
|
document.removeEventListener('mousedown', handler)
|
||||||
|
}
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
const confirm = useCallback(() => {
|
||||||
|
if (url && url !== 'https://') {
|
||||||
|
onConfirm(url.startsWith('http') ? url : `https://${url}`)
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
}, [url, onConfirm, onClose])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="absolute z-50 top-full left-0 mt-1 p-3 bg-background border rounded-lg shadow-xl w-80"
|
||||||
|
>
|
||||||
|
<div className="text-xs text-muted-foreground mb-2 font-medium">
|
||||||
|
{currentUrl ? 'Modifica collegamento' : 'Inserisci collegamento'}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="url"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
placeholder="https://esempio.it"
|
||||||
|
className="flex-1 text-sm border rounded px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-ring bg-background"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') confirm()
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-3 py-1.5 bg-primary text-primary-foreground rounded text-sm font-medium hover:bg-primary/90"
|
||||||
|
onClick={confirm}
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
{currentUrl && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="p-1.5 bg-destructive/10 text-destructive rounded hover:bg-destructive/20"
|
||||||
|
onClick={() => { onRemove(); onClose() }}
|
||||||
|
title="Rimuovi collegamento"
|
||||||
|
>
|
||||||
|
<Link2Off className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Toolbar principale ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface ToolbarProps {
|
||||||
|
editor: ReturnType<typeof useEditor>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toolbar({ editor }: ToolbarProps) {
|
||||||
|
const [showColorPicker, setShowColorPicker] = useState(false)
|
||||||
|
const [showLinkDialog, setShowLinkDialog] = useState(false)
|
||||||
|
const colorRef = useRef<HTMLDivElement>(null)
|
||||||
|
const linkRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
if (!editor) return null
|
||||||
|
|
||||||
|
const currentColor = editor.getAttributes('textStyle').color as string | undefined
|
||||||
|
const currentLinkUrl = editor.getAttributes('link').href as string | undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-0.5 p-1.5 bg-muted/40 border-b overflow-x-auto">
|
||||||
|
|
||||||
|
{/* Undo / Redo */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={!editor.can().undo()}
|
||||||
|
title="Annulla (Ctrl+Z)"
|
||||||
|
>
|
||||||
|
<Undo2 className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={!editor.can().redo()}
|
||||||
|
title="Ripeti (Ctrl+Y)"
|
||||||
|
>
|
||||||
|
<Redo2 className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Formattazione di base */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
isActive={editor.isActive('bold')}
|
||||||
|
title="Grassetto (Ctrl+B)"
|
||||||
|
>
|
||||||
|
<Bold className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
isActive={editor.isActive('italic')}
|
||||||
|
title="Corsivo (Ctrl+I)"
|
||||||
|
>
|
||||||
|
<Italic className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
isActive={editor.isActive('underline')}
|
||||||
|
title="Sottolineato (Ctrl+U)"
|
||||||
|
>
|
||||||
|
<Underline className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
isActive={editor.isActive('strike')}
|
||||||
|
title="Barrato"
|
||||||
|
>
|
||||||
|
<Strikethrough className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Colore testo */}
|
||||||
|
<div className="relative" ref={colorRef}>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => setShowColorPicker((v) => !v)}
|
||||||
|
isActive={showColorPicker}
|
||||||
|
title="Colore testo"
|
||||||
|
>
|
||||||
|
<span className="flex flex-col items-center leading-none">
|
||||||
|
<Baseline className="h-3 w-3" />
|
||||||
|
<span
|
||||||
|
className="block h-[3px] w-4 rounded-sm mt-0.5"
|
||||||
|
style={{ backgroundColor: currentColor || '#000000' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</ToolbarButton>
|
||||||
|
{showColorPicker && (
|
||||||
|
<ColorPicker
|
||||||
|
currentColor={currentColor}
|
||||||
|
onSelect={(color) => {
|
||||||
|
if (color) {
|
||||||
|
editor.chain().focus().setColor(color).run()
|
||||||
|
} else {
|
||||||
|
editor.chain().focus().unsetColor().run()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClose={() => setShowColorPicker(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rimuovi formattazione */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}
|
||||||
|
title="Rimuovi formattazione"
|
||||||
|
>
|
||||||
|
<Eraser className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Titoli */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
isActive={editor.isActive('heading', { level: 1 })}
|
||||||
|
title="Titolo 1"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] font-bold leading-none">H1</span>
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
isActive={editor.isActive('heading', { level: 2 })}
|
||||||
|
title="Titolo 2"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] font-bold leading-none">H2</span>
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||||
|
isActive={editor.isActive('heading', { level: 3 })}
|
||||||
|
title="Titolo 3"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] font-bold leading-none">H3</span>
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Elenchi */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
isActive={editor.isActive('bulletList')}
|
||||||
|
title="Elenco puntato"
|
||||||
|
>
|
||||||
|
<List className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
isActive={editor.isActive('orderedList')}
|
||||||
|
title="Elenco numerato"
|
||||||
|
>
|
||||||
|
<ListOrdered className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
isActive={editor.isActive('blockquote')}
|
||||||
|
title="Citazione"
|
||||||
|
>
|
||||||
|
<Quote className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Allineamento */}
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||||
|
isActive={editor.isActive({ textAlign: 'left' })}
|
||||||
|
title="Allinea a sinistra"
|
||||||
|
>
|
||||||
|
<AlignLeft className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||||
|
isActive={editor.isActive({ textAlign: 'center' })}
|
||||||
|
title="Centra"
|
||||||
|
>
|
||||||
|
<AlignCenter className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
|
isActive={editor.isActive({ textAlign: 'right' })}
|
||||||
|
title="Allinea a destra"
|
||||||
|
>
|
||||||
|
<AlignRight className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||||
|
isActive={editor.isActive({ textAlign: 'justify' })}
|
||||||
|
title="Giustifica"
|
||||||
|
>
|
||||||
|
<AlignJustify className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
|
||||||
|
<ToolbarSeparator />
|
||||||
|
|
||||||
|
{/* Link */}
|
||||||
|
<div className="relative" ref={linkRef}>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => setShowLinkDialog((v) => !v)}
|
||||||
|
isActive={editor.isActive('link') || showLinkDialog}
|
||||||
|
title={editor.isActive('link') ? 'Modifica collegamento' : 'Inserisci collegamento'}
|
||||||
|
>
|
||||||
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
|
</ToolbarButton>
|
||||||
|
{showLinkDialog && (
|
||||||
|
<LinkDialog
|
||||||
|
currentUrl={currentLinkUrl}
|
||||||
|
onConfirm={(url) =>
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||||
|
}
|
||||||
|
onRemove={() => editor.chain().focus().unsetLink().run()}
|
||||||
|
onClose={() => setShowLinkDialog(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Componente principale ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function RichTextEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Scrivi il testo della PEC...',
|
||||||
|
minHeight = '260px',
|
||||||
|
}: RichTextEditorProps) {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [1, 2, 3] },
|
||||||
|
}),
|
||||||
|
UnderlineExt,
|
||||||
|
TextStyle,
|
||||||
|
Color,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph'],
|
||||||
|
defaultAlignment: 'left',
|
||||||
|
}),
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'rte-link',
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Placeholder.configure({ placeholder }),
|
||||||
|
],
|
||||||
|
content: value,
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
onChange(editor.getHTML())
|
||||||
|
},
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: 'rte-content focus:outline-none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync da valore esterno (es. modifica replyTo dopo il mount)
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor && !editor.isDestroyed && value !== editor.getHTML()) {
|
||||||
|
editor.commands.setContent(value, { emitUpdate: false })
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded-md overflow-hidden focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 bg-background">
|
||||||
|
<Toolbar editor={editor} />
|
||||||
|
<EditorContent
|
||||||
|
editor={editor}
|
||||||
|
style={{ minHeight }}
|
||||||
|
className="px-4 py-3 overflow-y-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* TagBadge – badge colorato per un singolo tag/label.
|
||||||
|
*
|
||||||
|
* Mostra il nome del tag con il colore di sfondo configurato.
|
||||||
|
* Se `onRemove` è fornito, mostra un pulsante × per rimuovere il tag.
|
||||||
|
*/
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { LabelResponse } from '@/types/api.types'
|
||||||
|
|
||||||
|
// Colore di default quando il tag non ha colore configurato
|
||||||
|
const DEFAULT_COLOR = '#6b7280'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcola il colore del testo (bianco o nero) in base alla luminosità
|
||||||
|
* del colore di sfondo per garantire un contrasto leggibile.
|
||||||
|
*/
|
||||||
|
function getTextColor(hexColor: string): string {
|
||||||
|
const hex = hexColor.replace('#', '')
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16)
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16)
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16)
|
||||||
|
// Formula luminosità relativa (WCAG)
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||||
|
return luminance > 0.5 ? '#1f2937' : '#ffffff'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagBadgeProps {
|
||||||
|
label: LabelResponse
|
||||||
|
onRemove?: () => void
|
||||||
|
size?: 'sm' | 'md'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagBadge({ label, onRemove, size = 'sm', className }: TagBadgeProps) {
|
||||||
|
const bgColor = label.color || DEFAULT_COLOR
|
||||||
|
const textColor = getTextColor(bgColor)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center gap-1 rounded-full font-medium transition-all',
|
||||||
|
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: bgColor, color: textColor }}
|
||||||
|
title={label.name}
|
||||||
|
>
|
||||||
|
<span className="truncate max-w-[120px]">{label.name}</span>
|
||||||
|
{onRemove && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onRemove()
|
||||||
|
}}
|
||||||
|
className="flex-shrink-0 rounded-full p-0.5 hover:opacity-70 transition-opacity"
|
||||||
|
style={{ color: textColor }}
|
||||||
|
aria-label={`Rimuovi tag ${label.name}`}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagBadgeList – mostra una lista compatta di tag badge.
|
||||||
|
* Se ci sono più tag del limite `maxVisible`, mostra un badge "+N".
|
||||||
|
*/
|
||||||
|
interface TagBadgeListProps {
|
||||||
|
labels: LabelResponse[]
|
||||||
|
onRemove?: (labelId: string) => void
|
||||||
|
maxVisible?: number
|
||||||
|
size?: 'sm' | 'md'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagBadgeList({
|
||||||
|
labels,
|
||||||
|
onRemove,
|
||||||
|
maxVisible = 3,
|
||||||
|
size = 'sm',
|
||||||
|
className,
|
||||||
|
}: TagBadgeListProps) {
|
||||||
|
if (!labels || labels.length === 0) return null
|
||||||
|
|
||||||
|
const visible = labels.slice(0, maxVisible)
|
||||||
|
const overflow = labels.length - maxVisible
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-1 flex-wrap', className)}>
|
||||||
|
{visible.map((label) => (
|
||||||
|
<TagBadge
|
||||||
|
key={label.id}
|
||||||
|
label={label}
|
||||||
|
onRemove={onRemove ? () => onRemove(label.id) : undefined}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{overflow > 0 && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground"
|
||||||
|
title={labels
|
||||||
|
.slice(maxVisible)
|
||||||
|
.map((l) => l.name)
|
||||||
|
.join(', ')}
|
||||||
|
>
|
||||||
|
+{overflow}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* TagSelector – pannello di selezione/creazione tag per messaggi.
|
||||||
|
*
|
||||||
|
* Modalità d'uso:
|
||||||
|
* - single: apre un Dialog per impostare i tag di un singolo messaggio
|
||||||
|
* - bulk: apre un Dialog per aggiungere o rimuovere tag da più messaggi
|
||||||
|
*
|
||||||
|
* Funzionalità:
|
||||||
|
* - Lista tutti i tag del tenant con checkbox
|
||||||
|
* - Ricerca/filtro tag
|
||||||
|
* - Crea nuovi tag con nome e colore (solo per admin)
|
||||||
|
* - Mostra anteprima del badge colorato
|
||||||
|
*/
|
||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import { Plus, Search, Tag, Check, Trash2 } from 'lucide-react'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/Dialog'
|
||||||
|
import { TagBadge } from './TagBadge'
|
||||||
|
import { labelsApi } from '@/api/labels.api'
|
||||||
|
import { getErrorMessage } from '@/api/client'
|
||||||
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { LabelResponse } from '@/types/api.types'
|
||||||
|
|
||||||
|
// ─── Tavolozza colori predefiniti ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const PRESET_COLORS = [
|
||||||
|
'#ef4444', // rosso
|
||||||
|
'#f97316', // arancione
|
||||||
|
'#eab308', // giallo
|
||||||
|
'#22c55e', // verde
|
||||||
|
'#14b8a6', // teal
|
||||||
|
'#3b82f6', // blu
|
||||||
|
'#8b5cf6', // viola
|
||||||
|
'#ec4899', // rosa
|
||||||
|
'#6b7280', // grigio
|
||||||
|
'#78716c', // marrone
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── Tipi ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface TagSelectorSingleProps {
|
||||||
|
mode: 'single'
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
/** ID dei tag attualmente assegnati al messaggio */
|
||||||
|
currentLabelIds: string[]
|
||||||
|
/** Chiamata quando l'utente conferma la selezione */
|
||||||
|
onApply: (labelIds: string[]) => void
|
||||||
|
isApplying?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagSelectorBulkProps {
|
||||||
|
mode: 'bulk'
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
/** Numero di messaggi selezionati (per info nell'UI) */
|
||||||
|
messageCount: number
|
||||||
|
/** Chiamata con i label selezionati e l'azione (add/remove) */
|
||||||
|
onApply: (labelIds: string[], action: 'add' | 'remove') => void
|
||||||
|
isApplying?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type TagSelectorProps = TagSelectorSingleProps | TagSelectorBulkProps
|
||||||
|
|
||||||
|
// ─── Componente principale ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function TagSelector(props: TagSelectorProps) {
|
||||||
|
const { open, onClose, onApply, isApplying } = props
|
||||||
|
const { user } = useAuth()
|
||||||
|
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
// Stato selezione (per single: IDs scelti; per bulk: IDs da applicare)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(
|
||||||
|
props.mode === 'single' ? new Set(props.currentLabelIds) : new Set(),
|
||||||
|
)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
|
const [newName, setNewName] = useState('')
|
||||||
|
const [newColor, setNewColor] = useState(PRESET_COLORS[5]) // blu default
|
||||||
|
|
||||||
|
// Reset dello stato all'apertura
|
||||||
|
const handleOpenChange = (isOpen: boolean) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSearchQuery('')
|
||||||
|
setShowCreate(false)
|
||||||
|
setNewName('')
|
||||||
|
setNewColor(PRESET_COLORS[5])
|
||||||
|
if (props.mode === 'bulk') {
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
} else if (props.mode === 'single') {
|
||||||
|
setSelectedIds(new Set(props.currentLabelIds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carica lista tag del tenant
|
||||||
|
const { data: allLabels = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['labels'],
|
||||||
|
queryFn: labelsApi.list,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
enabled: open,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tag filtrati per ricerca
|
||||||
|
const filteredLabels = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return allLabels
|
||||||
|
const q = searchQuery.toLowerCase()
|
||||||
|
return allLabels.filter((l) => l.name.toLowerCase().includes(q))
|
||||||
|
}, [allLabels, searchQuery])
|
||||||
|
|
||||||
|
// Crea nuovo tag
|
||||||
|
const createMutation = useMutation<LabelResponse, Error, void>({
|
||||||
|
mutationFn: () => labelsApi.create({ name: newName.trim(), color: newColor }),
|
||||||
|
onSuccess: (newLabel) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['labels'] })
|
||||||
|
setSelectedIds((prev) => new Set([...prev, newLabel.id]))
|
||||||
|
setNewName('')
|
||||||
|
setShowCreate(false)
|
||||||
|
toast.success(`Tag "${newLabel.name}" creato`)
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Elimina tag
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => labelsApi.delete(id),
|
||||||
|
onSuccess: (_, deletedId) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['labels'] })
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.delete(deletedId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
toast.success('Tag eliminato')
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleLabel = (id: string) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplySingle = () => {
|
||||||
|
if (props.mode === 'single') {
|
||||||
|
onApply(Array.from(selectedIds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyBulk = (action: 'add' | 'remove') => {
|
||||||
|
if (props.mode === 'bulk') {
|
||||||
|
onApply(Array.from(selectedIds), action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canConfirm = selectedIds.size > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Tag className="h-5 w-5 text-primary" />
|
||||||
|
{props.mode === 'single' ? 'Gestisci tag' : `Assegna tag a ${props.mode === 'bulk' ? props.messageCount : ''} messaggi`}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Ricerca tag */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Cerca tag…"
|
||||||
|
className="pl-9 h-9"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista tag */}
|
||||||
|
<div className="max-h-60 overflow-y-auto space-y-1 rounded-lg border bg-muted/20 p-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
) : filteredLabels.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground text-center py-4">
|
||||||
|
{searchQuery ? 'Nessun tag trovato' : 'Nessun tag disponibile'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
filteredLabels.map((label) => (
|
||||||
|
<LabelRow
|
||||||
|
key={label.id}
|
||||||
|
label={label}
|
||||||
|
isSelected={selectedIds.has(label.id)}
|
||||||
|
onToggle={() => toggleLabel(label.id)}
|
||||||
|
onDelete={isAdmin ? () => deleteMutation.mutate(label.id) : undefined}
|
||||||
|
isDeleting={deleteMutation.isPending && deleteMutation.variables === label.id}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Crea nuovo tag (admin only) */}
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowCreate(!showCreate)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 text-primary flex-shrink-0" />
|
||||||
|
<span className="text-primary font-medium">Crea nuovo tag</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showCreate && (
|
||||||
|
<div className="px-3 pb-3 pt-1 border-t bg-muted/10 space-y-3">
|
||||||
|
<Input
|
||||||
|
placeholder="Nome tag…"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
className="h-8 text-sm"
|
||||||
|
maxLength={100}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && newName.trim()) createMutation.mutate()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Selettore colore */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-xs text-muted-foreground">Colore</p>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{PRESET_COLORS.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewColor(color)}
|
||||||
|
className={cn(
|
||||||
|
'h-6 w-6 rounded-full border-2 transition-all',
|
||||||
|
newColor === color
|
||||||
|
? 'border-foreground scale-110'
|
||||||
|
: 'border-transparent hover:scale-105',
|
||||||
|
)}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
title={color}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Anteprima badge */}
|
||||||
|
{newName.trim() && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground">Anteprima:</span>
|
||||||
|
<TagBadge
|
||||||
|
label={{ id: 'preview', tenant_id: '', name: newName.trim(), color: newColor }}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8"
|
||||||
|
disabled={!newName.trim()}
|
||||||
|
onClick={() => createMutation.mutate()}
|
||||||
|
isLoading={createMutation.isPending}
|
||||||
|
>
|
||||||
|
Crea tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer azioni */}
|
||||||
|
<DialogFooter className="flex-col gap-2 sm:flex-row">
|
||||||
|
<Button variant="outline" onClick={() => handleOpenChange(false)} className="sm:order-first">
|
||||||
|
Annulla
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{props.mode === 'single' ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleApplySingle}
|
||||||
|
isLoading={isApplying}
|
||||||
|
>
|
||||||
|
Applica tag
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2 flex-1 sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={!canConfirm}
|
||||||
|
onClick={() => handleApplyBulk('remove')}
|
||||||
|
isLoading={isApplying}
|
||||||
|
className="flex-1 sm:flex-none"
|
||||||
|
>
|
||||||
|
Rimuovi
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!canConfirm}
|
||||||
|
onClick={() => handleApplyBulk('add')}
|
||||||
|
isLoading={isApplying}
|
||||||
|
className="flex-1 sm:flex-none"
|
||||||
|
>
|
||||||
|
Aggiungi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Riga singolo label nella lista ──────────────────────────────────────────
|
||||||
|
|
||||||
|
interface LabelRowProps {
|
||||||
|
label: LabelResponse
|
||||||
|
isSelected: boolean
|
||||||
|
onToggle: () => void
|
||||||
|
onDelete?: () => void
|
||||||
|
isDeleting?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelRow({ label, isSelected, onToggle, onDelete, isDeleting }: LabelRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer hover:bg-muted/50 transition-colors group',
|
||||||
|
isSelected && 'bg-primary/5',
|
||||||
|
)}
|
||||||
|
onClick={onToggle}
|
||||||
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors',
|
||||||
|
isSelected
|
||||||
|
? 'bg-primary border-primary text-primary-foreground'
|
||||||
|
: 'border-muted-foreground/40',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSelected && <Check className="h-3 w-3" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badge con colore */}
|
||||||
|
<TagBadge label={label} size="sm" className="pointer-events-none" />
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* Pulsante elimina (solo admin, visibile su hover) */}
|
||||||
|
{onDelete && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDelete()
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-all',
|
||||||
|
isDeleting ? 'opacity-50 pointer-events-none' : 'opacity-0 group-hover:opacity-100',
|
||||||
|
)}
|
||||||
|
title="Elimina tag"
|
||||||
|
aria-label={`Elimina tag ${label.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -82,6 +82,82 @@
|
|||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Rich Text Editor (TipTap / ProseMirror) ─────────────────────────────── */
|
||||||
|
|
||||||
|
.rte-content {
|
||||||
|
outline: none;
|
||||||
|
min-height: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder */
|
||||||
|
.rte-content p.is-editor-empty:first-child::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tipografia */
|
||||||
|
.rte-content h1 { font-size: 1.6rem; font-weight: 700; margin: 0.6rem 0 0.3rem; line-height: 1.3; }
|
||||||
|
.rte-content h2 { font-size: 1.3rem; font-weight: 600; margin: 0.5rem 0 0.25rem; line-height: 1.3; }
|
||||||
|
.rte-content h3 { font-size: 1.1rem; font-weight: 600; margin: 0.4rem 0 0.2rem; line-height: 1.3; }
|
||||||
|
.rte-content p { margin: 0.2rem 0; }
|
||||||
|
.rte-content p + p { margin-top: 0.4rem; }
|
||||||
|
|
||||||
|
/* Elenchi */
|
||||||
|
.rte-content ul { list-style-type: disc; padding-left: 1.5rem; margin: 0.3rem 0; }
|
||||||
|
.rte-content ol { list-style-type: decimal; padding-left: 1.5rem; margin: 0.3rem 0; }
|
||||||
|
.rte-content li { margin: 0.15rem 0; }
|
||||||
|
|
||||||
|
/* Citazione */
|
||||||
|
.rte-content blockquote {
|
||||||
|
border-left: 3px solid hsl(var(--border));
|
||||||
|
padding-left: 0.85rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link */
|
||||||
|
.rte-content a.rte-link,
|
||||||
|
.rte-content a {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.rte-content a:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
/* Codice */
|
||||||
|
.rte-content code {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.rte-content pre {
|
||||||
|
background: hsl(222 47% 11%);
|
||||||
|
color: hsl(210 40% 96%);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
}
|
||||||
|
.rte-content pre code { background: none; color: inherit; padding: 0; }
|
||||||
|
|
||||||
|
/* Separatore orizzontale */
|
||||||
|
.rte-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid hsl(var(--border));
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selezione testo */
|
||||||
|
.rte-content .ProseMirror-selectednode { outline: 2px solid hsl(var(--primary)); }
|
||||||
|
|
||||||
/* Badge PEC stati */
|
/* Badge PEC stati */
|
||||||
.pec-badge-draft { @apply bg-gray-100 text-gray-700 border-gray-200; }
|
.pec-badge-draft { @apply bg-gray-100 text-gray-700 border-gray-200; }
|
||||||
.pec-badge-queued { @apply bg-yellow-100 text-yellow-700 border-yellow-200; }
|
.pec-badge-queued { @apply bg-yellow-100 text-yellow-700 border-yellow-200; }
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useForm, useFieldArray } from 'react-hook-form'
|
import { useForm, useFieldArray } from 'react-hook-form'
|
||||||
import { Send, X, Plus, ArrowLeft, AlertCircle } from 'lucide-react'
|
import { Send, X, Plus, ArrowLeft, AlertCircle, Paperclip, Upload } from 'lucide-react'
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query'
|
import { useQuery, useMutation } from '@tanstack/react-query'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { Label } from '@/components/ui/Label'
|
import { Label } from '@/components/ui/Label'
|
||||||
|
import { RichTextEditor } from '@/components/RichTextEditor/RichTextEditor'
|
||||||
import { sendApi } from '@/api/send.api'
|
import { sendApi } from '@/api/send.api'
|
||||||
import { mailboxesApi } from '@/api/mailboxes.api'
|
import { mailboxesApi } from '@/api/mailboxes.api'
|
||||||
import { getErrorMessage } from '@/api/client'
|
import { getErrorMessage } from '@/api/client'
|
||||||
@@ -17,7 +18,22 @@ interface ComposeFormValues {
|
|||||||
to_addresses: { value: string }[]
|
to_addresses: { value: string }[]
|
||||||
cc_addresses: { value: string }[]
|
cc_addresses: { value: string }[]
|
||||||
subject: string
|
subject: string
|
||||||
body_text: string
|
}
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20 MB
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Estrae testo semplice dall'HTML del rich editor per la parte text/plain dell'email */
|
||||||
|
function htmlToText(html: string): string {
|
||||||
|
if (!html || html === '<p></p>') return ''
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = html
|
||||||
|
return div.textContent || div.innerText || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComposePage() {
|
export function ComposePage() {
|
||||||
@@ -25,6 +41,27 @@ export function ComposePage() {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const replyTo = location.state?.replyTo as MessageResponse | undefined
|
const replyTo = location.state?.replyTo as MessageResponse | undefined
|
||||||
const [showCc, setShowCc] = useState(false)
|
const [showCc, setShowCc] = useState(false)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Corpo HTML (gestito da TipTap)
|
||||||
|
const [bodyHtml, setBodyHtml] = useState<string>(() => {
|
||||||
|
if (!replyTo) return ''
|
||||||
|
const date = new Date(
|
||||||
|
replyTo.received_at || replyTo.created_at
|
||||||
|
).toLocaleDateString('it-IT')
|
||||||
|
return [
|
||||||
|
'<p></p>',
|
||||||
|
'<p></p>',
|
||||||
|
'<hr>',
|
||||||
|
`<p><strong>In risposta al messaggio del ${date}</strong></p>`,
|
||||||
|
`<p>Da: ${replyTo.from_address || ''}</p>`,
|
||||||
|
`<p>A: ${replyTo.to_addresses?.join(', ') || ''}</p>`,
|
||||||
|
`<p>Oggetto: ${replyTo.subject || ''}</p>`,
|
||||||
|
].join('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Allegati
|
||||||
|
const [attachments, setAttachments] = useState<File[]>([])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -34,12 +71,11 @@ export function ComposePage() {
|
|||||||
} = useForm<ComposeFormValues>({
|
} = useForm<ComposeFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
mailbox_id: replyTo?.mailbox_id || '',
|
mailbox_id: replyTo?.mailbox_id || '',
|
||||||
to_addresses: replyTo ? [{ value: replyTo.from_address || '' }] : [{ value: '' }],
|
to_addresses: replyTo
|
||||||
|
? [{ value: replyTo.from_address || '' }]
|
||||||
|
: [{ value: '' }],
|
||||||
cc_addresses: [],
|
cc_addresses: [],
|
||||||
subject: replyTo ? `Re: ${replyTo.subject || ''}` : '',
|
subject: replyTo ? `Re: ${replyTo.subject || ''}` : '',
|
||||||
body_text: replyTo
|
|
||||||
? `\n\n---\nIn risposta al messaggio del ${new Date(replyTo.received_at || replyTo.created_at).toLocaleDateString('it-IT')}\nDa: ${replyTo.from_address || ''}\nA: ${replyTo.to_addresses?.join(', ') || ''}\nOggetto: ${replyTo.subject || ''}`
|
|
||||||
: '',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -62,7 +98,10 @@ export function ComposePage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const sendMutation = useMutation({
|
const sendMutation = useMutation({
|
||||||
mutationFn: sendApi.send,
|
mutationFn: (args: {
|
||||||
|
data: Parameters<typeof sendApi.sendMultipart>[0]
|
||||||
|
files: File[]
|
||||||
|
}) => sendApi.sendMultipart(args.data, args.files),
|
||||||
onSuccess: (job) => {
|
onSuccess: (job) => {
|
||||||
toast.success(`PEC inviata! Job ID: ${job.id.slice(0, 8)}...`)
|
toast.success(`PEC inviata! Job ID: ${job.id.slice(0, 8)}...`)
|
||||||
navigate('/sent')
|
navigate('/sent')
|
||||||
@@ -72,8 +111,29 @@ export function ComposePage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = async (data: ComposeFormValues) => {
|
// ── Gestione allegati ──────────────────────────────────────────────────────
|
||||||
const toAddresses = data.to_addresses
|
|
||||||
|
const handleFileAdd = (files: FileList | null) => {
|
||||||
|
if (!files) return
|
||||||
|
const valid: File[] = []
|
||||||
|
for (const file of Array.from(files)) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
toast.error(`"${file.name}" supera il limite di 20 MB`)
|
||||||
|
} else {
|
||||||
|
valid.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valid.length) setAttachments((prev) => [...prev, ...valid])
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAttachment = (idx: number) => {
|
||||||
|
setAttachments((prev) => prev.filter((_, i) => i !== idx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Submit ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const onSubmit = async (formData: ComposeFormValues) => {
|
||||||
|
const toAddresses = formData.to_addresses
|
||||||
.map((t) => t.value.trim())
|
.map((t) => t.value.trim())
|
||||||
.filter((v) => v.length > 0)
|
.filter((v) => v.length > 0)
|
||||||
|
|
||||||
@@ -83,14 +143,18 @@ export function ComposePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendMutation.mutateAsync({
|
await sendMutation.mutateAsync({
|
||||||
mailbox_id: data.mailbox_id,
|
data: {
|
||||||
to_addresses: toAddresses,
|
mailbox_id: formData.mailbox_id,
|
||||||
cc_addresses: data.cc_addresses
|
to_addresses: toAddresses,
|
||||||
.map((c) => c.value.trim())
|
cc_addresses: formData.cc_addresses
|
||||||
.filter((v) => v.length > 0),
|
.map((c) => c.value.trim())
|
||||||
subject: data.subject,
|
.filter((v) => v.length > 0),
|
||||||
body_text: data.body_text,
|
subject: formData.subject,
|
||||||
reply_to_message_id: replyTo?.id,
|
body_text: htmlToText(bodyHtml),
|
||||||
|
body_html: bodyHtml,
|
||||||
|
reply_to_message_id: replyTo?.id,
|
||||||
|
},
|
||||||
|
files: attachments,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,28 +163,29 @@ export function ComposePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b bg-background px-6 py-4 flex items-center justify-between">
|
<div className="border-b bg-background px-6 py-4 flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
<ArrowLeft className="h-5 w-5" />
|
||||||
<ArrowLeft className="h-5 w-5" />
|
</Button>
|
||||||
</Button>
|
<div>
|
||||||
<div>
|
<h1 className="text-lg font-semibold">
|
||||||
<h1 className="text-lg font-semibold">
|
{replyTo ? 'Rispondi a PEC' : 'Nuova PEC'}
|
||||||
{replyTo ? 'Rispondi a PEC' : 'Nuova PEC'}
|
</h1>
|
||||||
</h1>
|
{replyTo && (
|
||||||
{replyTo && (
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground">
|
In risposta a: {replyTo.subject}
|
||||||
In risposta a: {replyTo.subject}
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="max-w-3xl mx-auto px-6 py-8 space-y-6">
|
<form
|
||||||
{/* Avviso */}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="max-w-4xl mx-auto px-6 py-6 space-y-5"
|
||||||
|
>
|
||||||
|
{/* Avviso informativo */}
|
||||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 flex items-start gap-2">
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 flex items-start gap-2">
|
||||||
<AlertCircle className="h-4 w-4 text-blue-600 mt-0.5 flex-shrink-0" />
|
<AlertCircle className="h-4 w-4 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||||
<p className="text-sm text-blue-700">
|
<p className="text-sm text-blue-700">
|
||||||
@@ -130,7 +195,7 @@ export function ComposePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Casella mittente */}
|
{/* Casella mittente */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
<Label htmlFor="mailbox_id">Casella mittente *</Label>
|
||||||
{mailboxesLoading ? (
|
{mailboxesLoading ? (
|
||||||
<div className="h-10 rounded-md border bg-muted animate-pulse" />
|
<div className="h-10 rounded-md border bg-muted animate-pulse" />
|
||||||
@@ -141,8 +206,10 @@ export function ComposePage() {
|
|||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
id="mailbox_id"
|
id="mailbox_id"
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||||
{...register('mailbox_id', { required: 'Seleziona una casella mittente' })}
|
{...register('mailbox_id', {
|
||||||
|
required: 'Seleziona una casella mittente',
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<option value="">Seleziona casella...</option>
|
<option value="">Seleziona casella...</option>
|
||||||
{activeCaselle.map((mb) => (
|
{activeCaselle.map((mb) => (
|
||||||
@@ -158,7 +225,7 @@ export function ComposePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Destinatari A: */}
|
{/* Destinatari A: */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label>Destinatari (A:) *</Label>
|
<Label>Destinatari (A:) *</Label>
|
||||||
<Button
|
<Button
|
||||||
@@ -178,7 +245,8 @@ export function ComposePage() {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="destinatario@pec.it"
|
placeholder="destinatario@pec.it"
|
||||||
{...register(`to_addresses.${idx}.value`, {
|
{...register(`to_addresses.${idx}.value`, {
|
||||||
required: idx === 0 ? 'Almeno un destinatario è obbligatorio' : false,
|
required:
|
||||||
|
idx === 0 ? 'Almeno un destinatario è obbligatorio' : false,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{toFields.length > 1 && (
|
{toFields.length > 1 && (
|
||||||
@@ -202,7 +270,7 @@ export function ComposePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CC */}
|
{/* CC */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
{!showCc ? (
|
{!showCc ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -247,7 +315,7 @@ export function ComposePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Oggetto */}
|
{/* Oggetto */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="subject">Oggetto *</Label>
|
<Label htmlFor="subject">Oggetto *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="subject"
|
id="subject"
|
||||||
@@ -259,25 +327,92 @@ export function ComposePage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Corpo */}
|
{/* Corpo – Rich Text Editor */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="body_text">Testo del messaggio</Label>
|
<Label>Testo del messaggio</Label>
|
||||||
<textarea
|
<RichTextEditor
|
||||||
id="body_text"
|
value={bodyHtml}
|
||||||
rows={12}
|
onChange={setBodyHtml}
|
||||||
placeholder="Testo della PEC..."
|
placeholder="Scrivi il testo della PEC..."
|
||||||
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y"
|
minHeight="280px"
|
||||||
{...register('body_text')}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Azioni */}
|
{/* ── Allegati ────────────────────────────────────────────────── */}
|
||||||
<div className="flex items-center justify-between pt-4 border-t">
|
<div className="space-y-2">
|
||||||
<Button
|
<div className="flex items-center justify-between">
|
||||||
type="button"
|
<Label>Allegati</Label>
|
||||||
variant="outline"
|
<span className="text-xs text-muted-foreground">Max 20 MB per file</span>
|
||||||
onClick={() => navigate(-1)}
|
</div>
|
||||||
|
|
||||||
|
{/* Lista allegati caricati */}
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{attachments.map((file, idx) => (
|
||||||
|
<div
|
||||||
|
key={`${file.name}-${file.size}-${idx}`}
|
||||||
|
className="flex items-center gap-2.5 px-3 py-2 bg-muted/50 border rounded-md"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
|
||||||
|
<span
|
||||||
|
className="text-sm flex-1 truncate font-medium"
|
||||||
|
title={file.name}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatFileSize(file.size)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-1 rounded p-0.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||||
|
onClick={() => removeAttachment(idx)}
|
||||||
|
title="Rimuovi allegato"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Zona drag-and-drop / click per aggiungere file */}
|
||||||
|
<div
|
||||||
|
className="border-2 border-dashed rounded-md p-5 flex flex-col items-center gap-2 cursor-pointer hover:border-primary/60 hover:bg-primary/5 transition-colors"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleFileAdd(e.dataTransfer.files)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Upload className="h-7 w-7 text-muted-foreground" />
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
Clicca o trascina i file qui
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Qualsiasi tipo di file • Max 20 MB per file
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input file nascosto */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => handleFileAdd(e.target.files)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Azioni ──────────────────────────────────────────────────── */}
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t">
|
||||||
|
<Button type="button" variant="outline" onClick={() => navigate(-1)}>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -287,6 +422,11 @@ export function ComposePage() {
|
|||||||
>
|
>
|
||||||
<Send className="h-4 w-4 mr-2" />
|
<Send className="h-4 w-4 mr-2" />
|
||||||
Invia PEC
|
Invia PEC
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<span className="ml-1.5 bg-primary-foreground/20 text-primary-foreground rounded-full px-1.5 text-xs font-semibold">
|
||||||
|
+{attachments.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
*
|
*
|
||||||
* Funzionalità:
|
* Funzionalità:
|
||||||
* - Selezione singola e multipla tramite checkbox
|
* - Selezione singola e multipla tramite checkbox
|
||||||
* - Barra azioni bulk (stella, rimuovi stella, archivia, rimuovi archivio)
|
* - Barra azioni bulk (stella, rimuovi stella, archivia, rimuovi archivio, assegna tag)
|
||||||
* - Pulsanti azione rapida su hover di ogni riga (stella, archivia)
|
* - Pulsanti azione rapida su hover di ogni riga (stella, archivia)
|
||||||
* - Tutte le azioni funzionano anche in senso inverso (unstar / unarchive)
|
* - Badge tag colorati per ogni messaggio
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
@@ -31,13 +31,17 @@ import {
|
|||||||
CheckSquare,
|
CheckSquare,
|
||||||
Square,
|
Square,
|
||||||
X,
|
X,
|
||||||
|
Tag,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { Input } from '@/components/ui/Input'
|
import { Input } from '@/components/ui/Input'
|
||||||
import { PecStateBadge } from '@/components/PecBadge/PecBadge'
|
import { PecStateBadge } from '@/components/PecBadge/PecBadge'
|
||||||
|
import { TagBadgeList } from '@/components/TagManager/TagBadge'
|
||||||
|
import { TagSelector } from '@/components/TagManager/TagSelector'
|
||||||
import { messagesApi } from '@/api/messages.api'
|
import { messagesApi } from '@/api/messages.api'
|
||||||
|
import { labelsApi } from '@/api/labels.api'
|
||||||
import { mailboxesApi } from '@/api/mailboxes.api'
|
import { mailboxesApi } from '@/api/mailboxes.api'
|
||||||
import { virtualBoxesApi } from '@/api/virtual_boxes.api'
|
import { virtualBoxesApi } from '@/api/virtual_boxes.api'
|
||||||
import { formatRelative, truncate } from '@/lib/utils'
|
import { formatRelative, truncate } from '@/lib/utils'
|
||||||
@@ -75,6 +79,9 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
// ── Stato selezione ─────────────────────────────────────────────────────────
|
// ── Stato selezione ─────────────────────────────────────────────────────────
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// ── Stato dialog tag bulk ────────────────────────────────────────────────────
|
||||||
|
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||||
|
|
||||||
// Resetta lo stato UI ogni volta che si cambia casella, virtual box o cartella
|
// Resetta lo stato UI ogni volta che si cambia casella, virtual box o cartella
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSearchInput('')
|
setSearchInput('')
|
||||||
@@ -248,6 +255,31 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Bulk tag ─────────────────────────────────────────────────────────────────
|
||||||
|
const bulkLabelMutation = useMutation({
|
||||||
|
mutationFn: labelsApi.bulkLabels,
|
||||||
|
onSuccess: (result, payload) => {
|
||||||
|
invalidateMessages()
|
||||||
|
setSelectedIds(new Set())
|
||||||
|
setShowTagSelector(false)
|
||||||
|
const n = result.updated
|
||||||
|
if (payload.action === 'add') {
|
||||||
|
toast.success(`Tag aggiunti a ${n} ${n === 1 ? 'messaggio' : 'messaggi'}`)
|
||||||
|
} else {
|
||||||
|
toast.success(`Tag rimossi da ${n} ${n === 1 ? 'messaggio' : 'messaggi'}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleBulkTag = (labelIds: string[], action: 'add' | 'remove') => {
|
||||||
|
bulkLabelMutation.mutate({
|
||||||
|
message_ids: Array.from(selectedIds),
|
||||||
|
label_ids: labelIds,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleBulkStar = () =>
|
const handleBulkStar = () =>
|
||||||
bulkMutation.mutate({ ids: Array.from(selectedIds), is_starred: true })
|
bulkMutation.mutate({ ids: Array.from(selectedIds), is_starred: true })
|
||||||
const handleBulkUnstar = () =>
|
const handleBulkUnstar = () =>
|
||||||
@@ -495,6 +527,17 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
Ripristina dalla posta
|
Ripristina dalla posta
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Assegna tag bulk */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 text-xs border-blue-200 hover:bg-blue-100 dark:border-blue-700"
|
||||||
|
onClick={() => setShowTagSelector(true)}
|
||||||
|
>
|
||||||
|
<Tag className="h-3.5 w-3.5 mr-1 text-primary" />
|
||||||
|
Tag
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -599,6 +642,18 @@ export function InboxPage({ viewMode }: InboxPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Dialog assegnazione tag bulk ── */}
|
||||||
|
{showTagSelector && (
|
||||||
|
<TagSelector
|
||||||
|
mode="bulk"
|
||||||
|
open={showTagSelector}
|
||||||
|
onClose={() => setShowTagSelector(false)}
|
||||||
|
messageCount={selectedCount}
|
||||||
|
onApply={handleBulkTag}
|
||||||
|
isApplying={bulkLabelMutation.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -715,6 +770,13 @@ function MessageRow({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tag badges */}
|
||||||
|
{message.labels && message.labels.length > 0 && (
|
||||||
|
<div className="mt-1" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<TagBadgeList labels={message.labels} maxVisible={4} size="sm" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{message.body_text && (
|
{message.body_text && (
|
||||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||||
{truncate(message.body_text, 120)}
|
{truncate(message.body_text, 120)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
@@ -10,12 +11,16 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
Mail,
|
Mail,
|
||||||
Send,
|
Send,
|
||||||
|
Tag,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
import { PecStateBadge, PecTypeBadge } from '@/components/PecBadge/PecBadge'
|
import { PecStateBadge, PecTypeBadge } from '@/components/PecBadge/PecBadge'
|
||||||
import { ReceiptTree } from '@/components/ReceiptTree/ReceiptTree'
|
import { ReceiptTree } from '@/components/ReceiptTree/ReceiptTree'
|
||||||
|
import { TagBadge } from '@/components/TagManager/TagBadge'
|
||||||
|
import { TagSelector } from '@/components/TagManager/TagSelector'
|
||||||
import { messagesApi } from '@/api/messages.api'
|
import { messagesApi } from '@/api/messages.api'
|
||||||
|
import { labelsApi } from '@/api/labels.api'
|
||||||
import { formatDate, formatBytes } from '@/lib/utils'
|
import { formatDate, formatBytes } from '@/lib/utils'
|
||||||
import { getErrorMessage } from '@/api/client'
|
import { getErrorMessage } from '@/api/client'
|
||||||
|
|
||||||
@@ -24,6 +29,9 @@ export function MessageDetailPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
// Dialog tag
|
||||||
|
const [showTagSelector, setShowTagSelector] = useState(false)
|
||||||
|
|
||||||
// Carica messaggio
|
// Carica messaggio
|
||||||
const {
|
const {
|
||||||
data: message,
|
data: message,
|
||||||
@@ -72,6 +80,15 @@ export function MessageDetailPage() {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Download allegato autenticato
|
||||||
|
const handleDownloadAttachment = async (att: { id: string; filename: string }) => {
|
||||||
|
try {
|
||||||
|
await messagesApi.downloadAttachment(message?.id ?? id!, att.id, att.filename)
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(getErrorMessage(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ripristina dall'archivio
|
// Ripristina dall'archivio
|
||||||
const unarchiveMutation = useMutation({
|
const unarchiveMutation = useMutation({
|
||||||
mutationFn: () => messagesApi.unarchive(id!),
|
mutationFn: () => messagesApi.unarchive(id!),
|
||||||
@@ -83,6 +100,38 @@ export function MessageDetailPage() {
|
|||||||
onError: (error) => toast.error(getErrorMessage(error)),
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Imposta tag del messaggio
|
||||||
|
const setLabelsMutation = useMutation({
|
||||||
|
mutationFn: (labelIds: string[]) =>
|
||||||
|
labelsApi.setMessageLabels(id!, { label_ids: labelIds }),
|
||||||
|
onSuccess: (updatedLabels) => {
|
||||||
|
// Aggiorna la cache del messaggio con i nuovi label
|
||||||
|
queryClient.setQueryData(['message', id], (old: typeof message) => {
|
||||||
|
if (!old) return old
|
||||||
|
return { ...old, labels: updatedLabels }
|
||||||
|
})
|
||||||
|
// Invalida la lista messaggi per aggiornare i badge nella inbox
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||||
|
setShowTagSelector(false)
|
||||||
|
toast.success('Tag aggiornati')
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rimuove singolo tag (click su × nel badge)
|
||||||
|
const removeLabelMutation = useMutation({
|
||||||
|
mutationFn: (labelId: string) =>
|
||||||
|
labelsApi.removeMessageLabels(id!, { label_ids: [labelId] }),
|
||||||
|
onSuccess: (updatedLabels) => {
|
||||||
|
queryClient.setQueryData(['message', id], (old: typeof message) => {
|
||||||
|
if (!old) return old
|
||||||
|
return { ...old, labels: updatedLabels }
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['messages'] })
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(getErrorMessage(error)),
|
||||||
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -103,6 +152,8 @@ export function MessageDetailPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLabelIds = (message.labels || []).map((l) => l.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
@@ -117,6 +168,22 @@ export function MessageDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Gestisci tag */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowTagSelector(true)}
|
||||||
|
title="Gestisci tag"
|
||||||
|
>
|
||||||
|
<Tag className="h-4 w-4 mr-1 text-primary" />
|
||||||
|
Tag
|
||||||
|
{message.labels && message.labels.length > 0 && (
|
||||||
|
<span className="ml-1 text-xs bg-primary text-primary-foreground rounded-full px-1.5 py-0.5">
|
||||||
|
{message.labels.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Stella / Preferito */}
|
{/* Stella / Preferito */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -212,6 +279,28 @@ export function MessageDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tag badges */}
|
||||||
|
{message.labels && message.labels.length > 0 && (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{message.labels.map((label) => (
|
||||||
|
<TagBadge
|
||||||
|
key={label.id}
|
||||||
|
label={label}
|
||||||
|
size="md"
|
||||||
|
onRemove={() => removeLabelMutation.mutate(label.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowTagSelector(true)}
|
||||||
|
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-sm text-muted-foreground border border-dashed border-muted-foreground/40 hover:border-primary hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<Tag className="h-3.5 w-3.5" />
|
||||||
|
Modifica
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Dettagli busta */}
|
{/* Dettagli busta */}
|
||||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-2">
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-2">
|
||||||
<div className="grid grid-cols-[auto,1fr] gap-x-4 gap-y-1.5 text-sm">
|
<div className="grid grid-cols-[auto,1fr] gap-x-4 gap-y-1.5 text-sm">
|
||||||
@@ -291,11 +380,11 @@ export function MessageDetailPage() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
{attachments.map((att) => (
|
{attachments.map((att) => (
|
||||||
<a
|
<button
|
||||||
key={att.id}
|
key={att.id}
|
||||||
href={messagesApi.getAttachmentUrl(message.id, att.id)}
|
type="button"
|
||||||
download={att.filename}
|
onClick={() => handleDownloadAttachment(att)}
|
||||||
className="flex items-center gap-3 rounded-lg border bg-background p-3 hover:bg-muted/50 transition-colors group"
|
className="flex items-center gap-3 rounded-lg border bg-background p-3 hover:bg-muted/50 transition-colors group text-left w-full"
|
||||||
>
|
>
|
||||||
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||||
<Paperclip className="h-5 w-5 text-primary" />
|
<Paperclip className="h-5 w-5 text-primary" />
|
||||||
@@ -307,7 +396,7 @@ export function MessageDetailPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Download className="h-4 w-4 text-muted-foreground group-hover:text-foreground flex-shrink-0" />
|
<Download className="h-4 w-4 text-muted-foreground group-hover:text-foreground flex-shrink-0" />
|
||||||
</a>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,6 +431,18 @@ export function MessageDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dialog gestione tag */}
|
||||||
|
{showTagSelector && (
|
||||||
|
<TagSelector
|
||||||
|
mode="single"
|
||||||
|
open={showTagSelector}
|
||||||
|
onClose={() => setShowTagSelector(false)}
|
||||||
|
currentLabelIds={currentLabelIds}
|
||||||
|
onApply={(labelIds) => setLabelsMutation.mutate(labelIds)}
|
||||||
|
isApplying={setLabelsMutation.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,47 @@ export interface ConnectionTestResult {
|
|||||||
capabilities: string[] | null
|
capabilities: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Label (Tag) ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LabelResponse {
|
||||||
|
id: string
|
||||||
|
tenant_id: string
|
||||||
|
name: string
|
||||||
|
color: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelCreate {
|
||||||
|
name: string
|
||||||
|
color?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LabelUpdate {
|
||||||
|
name?: string
|
||||||
|
color?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageLabelSetRequest {
|
||||||
|
label_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageLabelAddRequest {
|
||||||
|
label_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageLabelRemoveRequest {
|
||||||
|
label_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageBulkLabelRequest {
|
||||||
|
message_ids: string[]
|
||||||
|
label_ids: string[]
|
||||||
|
action: 'add' | 'remove'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageBulkLabelResponse {
|
||||||
|
updated: number
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Message ──────────────────────────────────────────────────────────────────
|
// ─── Message ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export type PecDirection = 'inbound' | 'outbound'
|
export type PecDirection = 'inbound' | 'outbound'
|
||||||
@@ -185,6 +226,7 @@ export interface MessageResponse {
|
|||||||
raw_eml_path: string | null
|
raw_eml_path: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
labels: LabelResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageListResponse {
|
export interface MessageListResponse {
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ server {
|
|||||||
# Redirect HTTP → HTTPS in produzione (commentato per dev)
|
# Redirect HTTP → HTTPS in produzione (commentato per dev)
|
||||||
# return 301 https://$host$request_uri;
|
# return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
# Resolver Docker interno – re-risolve i nomi dei container ogni 30s
|
||||||
|
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||||
|
|
||||||
# ── API Backend ───────────────────────────────────────────────────────────
|
# ── API Backend ───────────────────────────────────────────────────────────
|
||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api burst=20 nodelay;
|
limit_req zone=api burst=20 nodelay;
|
||||||
|
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -30,7 +34,8 @@ server {
|
|||||||
location /api/v1/auth/login {
|
location /api/v1/auth/login {
|
||||||
limit_req zone=auth burst=5 nodelay;
|
limit_req zone=auth burst=5 nodelay;
|
||||||
|
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -40,24 +45,29 @@ server {
|
|||||||
|
|
||||||
# ── Health check ──────────────────────────────────────────────────────────
|
# ── Health check ──────────────────────────────────────────────────────────
|
||||||
location /health {
|
location /health {
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Swagger UI (solo dev) ─────────────────────────────────────────────────
|
# ── Swagger UI (solo dev) ─────────────────────────────────────────────────
|
||||||
location /docs {
|
location /docs {
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
}
|
}
|
||||||
location /redoc {
|
location /redoc {
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
}
|
}
|
||||||
location /openapi.json {
|
location /openapi.json {
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── WebSocket ─────────────────────────────────────────────────────────────
|
# ── WebSocket ─────────────────────────────────────────────────────────────
|
||||||
location /ws/ {
|
location /ws/ {
|
||||||
proxy_pass http://backend:8000;
|
set $backend_upstream http://backend:8000;
|
||||||
|
proxy_pass $backend_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
@@ -67,7 +77,8 @@ server {
|
|||||||
|
|
||||||
# ── Frontend React (Vite dev server) ─────────────────────────────────────
|
# ── Frontend React (Vite dev server) ─────────────────────────────────────
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://frontend:3000;
|
set $frontend_upstream http://frontend:3000;
|
||||||
|
proxy_pass $frontend_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -80,7 +91,8 @@ server {
|
|||||||
|
|
||||||
# ── Vite HMR WebSocket ────────────────────────────────────────────────────
|
# ── Vite HMR WebSocket ────────────────────────────────────────────────────
|
||||||
location /@vite/ {
|
location /@vite/ {
|
||||||
proxy_pass http://frontend:3000;
|
set $frontend_upstream http://frontend:3000;
|
||||||
|
proxy_pass $frontend_upstream;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ from typing import Any
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.database import AsyncSessionLocal
|
from app.database import AsyncSessionLocal
|
||||||
from app.models import Mailbox, Message, SendJob
|
from app.models import Attachment, Mailbox, Message, SendJob
|
||||||
from app.smtp.sender import SmtpSender
|
from app.smtp.sender import SmtpSender
|
||||||
from app.storage.minio_client import upload_outbound_eml
|
from app.storage.minio_client import download_attachment, upload_outbound_eml
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -105,6 +105,36 @@ async def send_pec(ctx: dict[str, Any], send_job_id: str) -> dict:
|
|||||||
f"per job {send_job_id} → {msg.to_addresses}"
|
f"per job {send_job_id} → {msg.to_addresses}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Carica allegati da MinIO (se presenti) ────────────────────────────
|
||||||
|
attachments_data: list[dict] | None = None
|
||||||
|
if msg.has_attachments:
|
||||||
|
att_result = await db.execute(
|
||||||
|
select(Attachment).where(Attachment.message_id == msg.id)
|
||||||
|
)
|
||||||
|
att_records = list(att_result.scalars().all())
|
||||||
|
if att_records:
|
||||||
|
attachments_data = []
|
||||||
|
for att in att_records:
|
||||||
|
try:
|
||||||
|
content = await download_attachment(att.storage_path)
|
||||||
|
attachments_data.append(
|
||||||
|
{
|
||||||
|
"filename": att.filename,
|
||||||
|
"content": content,
|
||||||
|
"content_type": att.content_type or "application/octet-stream",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"[send_pec] Allegato caricato per invio: "
|
||||||
|
f"{att.filename} ({len(content)} bytes)"
|
||||||
|
)
|
||||||
|
except Exception as att_err:
|
||||||
|
logger.warning(
|
||||||
|
f"[send_pec] Impossibile caricare allegato "
|
||||||
|
f"'{att.filename}' ({att.storage_path}): {att_err}"
|
||||||
|
)
|
||||||
|
# Continua senza questo allegato
|
||||||
|
|
||||||
# ── Tenta invio SMTP ──────────────────────────────────────────────────
|
# ── Tenta invio SMTP ──────────────────────────────────────────────────
|
||||||
try:
|
try:
|
||||||
sender = SmtpSender(mailbox)
|
sender = SmtpSender(mailbox)
|
||||||
@@ -114,7 +144,7 @@ async def send_pec(ctx: dict[str, Any], send_job_id: str) -> dict:
|
|||||||
subject=msg.subject or "",
|
subject=msg.subject or "",
|
||||||
body_text=msg.body_text or "",
|
body_text=msg.body_text or "",
|
||||||
body_html=msg.body_html,
|
body_html=msg.body_html,
|
||||||
attachments=None, # allegati in fase successiva (Fase 5)
|
attachments=attachments_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Successo: aggiorna DB ─────────────────────────────────────────
|
# ── Successo: aggiorna DB ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -180,6 +180,39 @@ async def upload_outbound_eml(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def download_attachment(storage_path: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Scarica un allegato da MinIO e restituisce i byte.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
storage_path: percorso oggetto MinIO (senza bucket name)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Byte del file scaricato
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: se il download fallisce
|
||||||
|
"""
|
||||||
|
client = get_minio_client()
|
||||||
|
bucket = settings.minio_bucket
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await client.get_object(
|
||||||
|
bucket_name=bucket,
|
||||||
|
object_name=storage_path,
|
||||||
|
)
|
||||||
|
data = await response.read()
|
||||||
|
response.close()
|
||||||
|
await response.release()
|
||||||
|
logger.debug(
|
||||||
|
f"Allegato scaricato: s3://{bucket}/{storage_path} ({len(data)} bytes)"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Errore download allegato {storage_path}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def ensure_bucket_exists() -> None:
|
async def ensure_bucket_exists() -> None:
|
||||||
"""Verifica che il bucket MinIO esista, altrimenti lo crea."""
|
"""Verifica che il bucket MinIO esista, altrimenti lo crea."""
|
||||||
client = get_minio_client()
|
client = get_minio_client()
|
||||||
|
|||||||
Reference in New Issue
Block a user