feat: add cascade deletion for notebooks with delete preview (#471)

* feat: decrease chunking size for maximum ollama compatibility

* docs: improve i18n info on Claude.md

* feat: add cascade deletion for notebooks with delete preview

- Add Notebook.get_delete_preview() to show counts of affected items
- Add Notebook.delete(delete_exclusive_sources) for cascade deletion
- Always delete notes when notebook is deleted
- Allow user to choose: delete or keep exclusive sources
- Shared sources are always unlinked but never deleted
- Add NotebookDeleteDialog component with radio button options
- Add delete-preview API endpoint
- Update delete endpoint with delete_exclusive_sources param
- Add i18n support for all 5 locales

Closes #77

* docs: remove harcoded config settings
This commit is contained in:
Luis Novo 2026-01-25 14:56:14 -03:00 committed by GitHub
parent f14020d385
commit 4e411e0488
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 527 additions and 55 deletions

View file

@ -3,7 +3,13 @@ from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
from api.models import NotebookCreate, NotebookResponse, NotebookUpdate
from api.models import (
NotebookCreate,
NotebookDeletePreview,
NotebookDeleteResponse,
NotebookResponse,
NotebookUpdate,
)
from open_notebook.database.repository import ensure_record_id, repo_query
from open_notebook.domain.notebook import Notebook, Source
from open_notebook.exceptions import InvalidInputError
@ -82,6 +88,35 @@ async def create_notebook(notebook: NotebookCreate):
)
@router.get(
"/notebooks/{notebook_id}/delete-preview", response_model=NotebookDeletePreview
)
async def get_notebook_delete_preview(notebook_id: str):
"""Get a preview of what will be deleted when this notebook is deleted."""
try:
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
preview = await notebook.get_delete_preview()
return NotebookDeletePreview(
notebook_id=str(notebook.id),
notebook_name=notebook.name,
note_count=preview["note_count"],
exclusive_source_count=preview["exclusive_source_count"],
shared_source_count=preview["shared_source_count"],
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting delete preview for notebook {notebook_id}: {e}")
raise HTTPException(
status_code=500,
detail=f"Error fetching notebook deletion preview: {str(e)}",
)
@router.get("/notebooks/{notebook_id}", response_model=NotebookResponse)
async def get_notebook(notebook_id: str):
"""Get a specific notebook by ID."""
@ -255,17 +290,34 @@ async def remove_source_from_notebook(notebook_id: str, source_id: str):
)
@router.delete("/notebooks/{notebook_id}")
async def delete_notebook(notebook_id: str):
"""Delete a notebook."""
@router.delete("/notebooks/{notebook_id}", response_model=NotebookDeleteResponse)
async def delete_notebook(
notebook_id: str,
delete_exclusive_sources: bool = Query(
False,
description="Whether to delete sources that belong only to this notebook",
),
):
"""
Delete a notebook with cascade deletion.
Always deletes all notes associated with the notebook.
If delete_exclusive_sources is True, also deletes sources that belong only
to this notebook (not linked to any other notebooks).
"""
try:
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
await notebook.delete()
result = await notebook.delete(delete_exclusive_sources=delete_exclusive_sources)
return {"message": "Notebook deleted successfully"}
return NotebookDeleteResponse(
message="Notebook deleted successfully",
deleted_notes=result["deleted_notes"],
deleted_sources=result["deleted_sources"],
unlinked_sources=result["unlinked_sources"],
)
except HTTPException:
raise
except Exception as e: