feat(workspace): deleted folders go to the trash as a whole, after a confirmation
A deleted personal folder is kept in trashed_folders with its subfolders; its notes are trashed and tied to it. The trash lists the folder as one entry with its note count; restoring brings back the folder tree (empty subfolders too) and all notes, deleting for good, emptying the trash and the notes trash delay remove it with its notes. Folders in shared folders keep the old way. Deleting a folder now asks first, in a small popover at the click (ConfirmPopover, documented in ui-patterns). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9dcfa6c27e
commit
b26b07cfea
18 files changed
+564
-25
No files matched your search
@@ -63,6 +63,38 @@ func (s *Server) handleDeleteFolder(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleRestoreFolder brings a folder from the trash back with its subfolders and notes.
|
||||
func (s *Server) handleRestoreFolder(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docIDs, err := s.store.RestoreFolder(userID, chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeTrashError(w, err)
|
||||
return
|
||||
}
|
||||
// Everyone with access to one of the notes reloads their tree
|
||||
users := map[string]bool{userID: true}
|
||||
for _, docID := range docIDs {
|
||||
authorized, _ := s.store.GetAuthorizedDocUsers(docID)
|
||||
for _, uid := range authorized {
|
||||
users[uid] = true
|
||||
}
|
||||
}
|
||||
for uid := range users {
|
||||
s.wsHub.BroadcastToUser(uid, "WORKSPACE_FOLDERS_UPDATED", map[string]string{"restored": "folder"})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"restored": true, "restoredDocIds": docIDs})
|
||||
}
|
||||
|
||||
// handlePurgeFolder deletes a folder in the trash with its notes for good.
|
||||
func (s *Server) handlePurgeFolder(w http.ResponseWriter, r *http.Request) {
|
||||
docIDs, err := s.store.PurgeFolder(auth.GetUserID(r.Context()), chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeTrashError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"deleted": true, "deletedDocIds": docIDs})
|
||||
}
|
||||
|
||||
func (s *Server) handleMoveFolder(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
|
||||
@@ -238,6 +238,8 @@ func (s *Server) Router() http.Handler {
|
||||
r.Post("/api/v1/folders", s.handleCreateFolder)
|
||||
r.Post("/api/v1/folders/move", s.handleMoveFolder)
|
||||
r.Delete("/api/v1/folders", s.handleDeleteFolder)
|
||||
r.Post("/api/v1/folders/trash/{id}/restore", s.handleRestoreFolder)
|
||||
r.Delete("/api/v1/folders/trash/{id}", s.handlePurgeFolder)
|
||||
|
||||
r.Get("/api/v1/workspace/folders/shared", s.handleGetSharedFolders)
|
||||
r.Post("/api/v1/workspace/folders/shared", s.handleCreateSharedFolder)
|
||||
|
||||
@@ -86,6 +86,7 @@ func purgeStmts(section string) []struct {
|
||||
{`DELETE FROM doc_user_folders WHERE user_id = ?`, 1},
|
||||
{`DELETE FROM doc_pins WHERE user_id = ?`, 1},
|
||||
{`DELETE FROM workspace_folders WHERE user_id = ?`, 1},
|
||||
{`DELETE FROM trashed_folders WHERE user_id = ?`, 1},
|
||||
{`DELETE FROM tag_pages WHERE user_id = ?`, 1},
|
||||
}
|
||||
case SectionLists:
|
||||
|
||||
@@ -133,11 +133,13 @@ func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
|
||||
var deletedDocIDs []string
|
||||
var writes []sqlStmt
|
||||
seenDocID := make(map[string]bool)
|
||||
inSharedFolder := false
|
||||
|
||||
mounts, _, err := s.getUserFolderMounts(userID)
|
||||
if err == nil {
|
||||
sfID, subPath, isMatch := matchMountPath(clean, mounts)
|
||||
if isMatch && sfID != "" {
|
||||
inSharedFolder = true
|
||||
if subPath == "" {
|
||||
// Root of a shared folder
|
||||
shFolder, sfErr := s.GetSharedFolderByID(userID, sfID)
|
||||
@@ -201,6 +203,22 @@ func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
|
||||
for _, id := range deletedDocIDs {
|
||||
writes = append(writes, trashStmts(userID, id)...)
|
||||
}
|
||||
// A personal folder goes to the trash as a whole (with its subfolders), so it can come back as it was
|
||||
if !inSharedFolder {
|
||||
subfolders := []string{}
|
||||
if frows, err := s.db.Query(`SELECT path FROM workspace_folders WHERE user_id = ? AND (shared_folder_id = '' OR shared_folder_id IS NULL)
|
||||
AND path LIKE ?`, userID, clean+"/%"); err == nil {
|
||||
for frows.Next() {
|
||||
var p string
|
||||
if frows.Scan(&p) == nil {
|
||||
subfolders = append(subfolders, p)
|
||||
}
|
||||
}
|
||||
_ = frows.Close()
|
||||
}
|
||||
_, folderWrites := trashFolderStmts(userID, clean, subfolders, deletedDocIDs)
|
||||
writes = append(writes, folderWrites...)
|
||||
}
|
||||
|
||||
// Also clear any personal folder placements for shared notes in this folder
|
||||
writes = append(writes,
|
||||
|
||||
@@ -38,6 +38,7 @@ var migrations = []migration{
|
||||
{15, "Spracheingabe pro Person", (*Store).migrateVoiceInput},
|
||||
{16, "Aufbewahrung und Papierkorb für Aufgaben", (*Store).migrateRetention},
|
||||
{17, "OAuth für den MCP-Zugang", (*Store).migrateOAuth},
|
||||
{18, "Ordner im Papierkorb", (*Store).migrateFolderTrash},
|
||||
}
|
||||
|
||||
func (s *Store) migrate() error {
|
||||
|
||||
@@ -207,6 +207,13 @@ func (s *Store) PurgeExpired(now time.Time, allowNever bool) (PurgeResult, error
|
||||
}
|
||||
res.Notes = append(res.Notes, id)
|
||||
}
|
||||
// Folders in the notes trash: same delay; their notes went above (they were trashed at the same time)
|
||||
if _, err := s.db.Exec(`DELETE FROM trashed_folders WHERE id IN (
|
||||
SELECT tf.id FROM trashed_folders tf JOIN users u ON u.id = tf.user_id
|
||||
WHERE `+daysSQL("u.note_trash_days")+` > 0 AND `+cutoffSQL("tf.trashed_at", daysSQL("u.note_trash_days"))+`)`,
|
||||
neverAs(allowNever, d.NoteTrash), since, since, sqlNow(now), neverAs(allowNever, d.NoteTrash)); err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Ticked-off list items: the list owner's delay, from ticking them off
|
||||
rows, err = s.db.Query(`SELECT li.id, li.list_id FROM list_items li JOIN lists l ON l.id = li.list_id JOIN users u ON u.id = l.user_id
|
||||
|
||||
@@ -429,10 +429,10 @@ func TestFolderManagementAndDeletion(t *testing.T) {
|
||||
t.Fatalf("expected 2 deleted docs, got %d (%v)", len(deletedDocs), deletedDocs)
|
||||
}
|
||||
|
||||
// The notes are in the trash now, no longer in the tree
|
||||
// The folder is in the trash with both notes, which are no longer in the tree
|
||||
trash, _ := store.GetTrash(user.ID)
|
||||
if len(trash) != 2 {
|
||||
t.Fatalf("expected doc1 and doc2 in the trash, got %v", trash)
|
||||
if len(trash) != 1 || trash[0].Kind != "folder" || trash[0].NoteCount != 2 {
|
||||
t.Fatalf("expected the folder with doc1 and doc2 in the trash, got %v", trash)
|
||||
}
|
||||
docs, _ := store.GetDocuments(user.ID)
|
||||
for _, d := range docs {
|
||||
|
||||
+159
-5
@@ -2,7 +2,10 @@ package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
)
|
||||
@@ -18,14 +21,38 @@ var ErrNotInTrash = errors.New("Notiz nicht im Papierkorb")
|
||||
// ErrDocInTrash refuses changes to a trashed note; it has to be restored first.
|
||||
var ErrDocInTrash = errors.New("Die Notiz liegt im Papierkorb. Stelle sie zuerst wieder her.")
|
||||
|
||||
// TrashedDocument is one entry of the trash list.
|
||||
// Deleted personal folders go to the trash as a whole: trashed_folders keeps the folder and its
|
||||
// subfolders, and its notes are trashed with trash_folder_id pointing there. The trash lists the folder
|
||||
// as one entry; restoring it brings back the folder tree (empty subfolders too) and all of its notes.
|
||||
// Folders inside shared folders keep the old way (their notes go to the trash one by one).
|
||||
|
||||
// TrashedDocument is one entry of the trash list: a note, or a deleted folder (Kind "folder", ID of the
|
||||
// trashed folder, Title its name, FolderPath the folder it was in, NoteCount the notes it holds).
|
||||
type TrashedDocument struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
FolderPath string `json:"folderPath"`
|
||||
DeletedAt string `json:"deletedAt"`
|
||||
IsOwner bool `json:"isOwner"`
|
||||
NoteCount int `json:"noteCount,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Store) migrateFolderTrash() error {
|
||||
if err := s.addColumnIfMissing("documents", "trash_folder_id", "TEXT NOT NULL DEFAULT ''"); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.execInTx(
|
||||
stmt(`CREATE TABLE IF NOT EXISTS trashed_folders (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
folders TEXT NOT NULL DEFAULT '[]',
|
||||
trashed_at TEXT NOT NULL
|
||||
)`),
|
||||
stmt(`CREATE INDEX IF NOT EXISTS idx_trashed_folders_user ON trashed_folders(user_id)`),
|
||||
)
|
||||
}
|
||||
|
||||
const trashScope = `is_archived = 1 AND (user_id = ? OR archived_by = ?)`
|
||||
@@ -41,12 +68,15 @@ func (s *Store) trashDocument(userID, docID string) error {
|
||||
return s.execInTx(trashStmts(userID, docID)...)
|
||||
}
|
||||
|
||||
// GetTrash lists the notes in the user's trash, newest deletion first.
|
||||
// GetTrash lists the notes and folders in the user's trash, newest deletion first. Notes of a trashed
|
||||
// folder are part of the folder's entry.
|
||||
func (s *Store) GetTrash(userID string) ([]TrashedDocument, error) {
|
||||
rows, err := s.db.Query(`SELECT d.id, d.title, d.icon, d.folder_path, d.shared_folder_id, d.sub_folder_path, d.archived_at, d.user_id,
|
||||
COALESCE(wsf.name, '')
|
||||
FROM documents d LEFT JOIN workspace_shared_folders wsf ON wsf.id = d.shared_folder_id
|
||||
WHERE d.`+trashScope+` ORDER BY d.archived_at DESC`, userID, userID)
|
||||
WHERE d.`+trashScope+`
|
||||
AND NOT EXISTS (SELECT 1 FROM trashed_folders tf WHERE tf.id = d.trash_folder_id)
|
||||
ORDER BY d.archived_at DESC`, userID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -64,12 +94,128 @@ func (s *Store) GetTrash(userID string) ([]TrashedDocument, error) {
|
||||
t.FolderPath += "/" + subPath
|
||||
}
|
||||
}
|
||||
t.Kind = "note"
|
||||
t.IsOwner = ownerID == userID
|
||||
out = append(out, t)
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
frows, err := s.db.Query(`SELECT tf.id, tf.path, tf.trashed_at,
|
||||
(SELECT COUNT(*) FROM documents d WHERE d.trash_folder_id = tf.id AND d.is_archived = 1)
|
||||
FROM trashed_folders tf WHERE tf.user_id = ?`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer frows.Close()
|
||||
for frows.Next() {
|
||||
var t TrashedDocument
|
||||
var p string
|
||||
if frows.Scan(&t.ID, &p, &t.DeletedAt, &t.NoteCount) != nil {
|
||||
continue
|
||||
}
|
||||
t.Kind = "folder"
|
||||
t.Title = path.Base(p)
|
||||
if parent := path.Dir(p); parent != "." {
|
||||
t.FolderPath = parent
|
||||
}
|
||||
t.IsOwner = true
|
||||
out = append(out, t)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].DeletedAt > out[j].DeletedAt })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// trashFolderStmts records a deleted personal folder (with its subfolders) in the trash and ties its
|
||||
// notes to it; the notes themselves are trashed by the caller.
|
||||
func trashFolderStmts(userID, folderPath string, subfolders, docIDs []string) (string, []sqlStmt) {
|
||||
id := domain.NewID("tfld")
|
||||
paths, _ := json.Marshal(subfolders)
|
||||
out := []sqlStmt{stmt(`INSERT INTO trashed_folders (id, user_id, path, folders, trashed_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
id, userID, folderPath, string(paths), domain.NowISO())}
|
||||
for _, docID := range docIDs {
|
||||
out = append(out, stmt(`UPDATE documents SET trash_folder_id = ? WHERE id = ?`, id, docID))
|
||||
}
|
||||
return id, out
|
||||
}
|
||||
|
||||
type trashedFolder struct {
|
||||
path string
|
||||
folders []string
|
||||
}
|
||||
|
||||
func (s *Store) trashedFolder(userID, id string) (*trashedFolder, error) {
|
||||
var f trashedFolder
|
||||
var raw string
|
||||
err := s.db.QueryRow(`SELECT path, folders FROM trashed_folders WHERE id = ? AND user_id = ?`, id, userID).Scan(&f.path, &raw)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrNotInTrash
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(raw), &f.folders)
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (s *Store) trashedFolderDocs(id string) ([]string, error) {
|
||||
rows, err := s.db.Query(`SELECT id FROM documents WHERE trash_folder_id = ? AND is_archived = 1`, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var docID string
|
||||
if rows.Scan(&docID) == nil {
|
||||
ids = append(ids, docID)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// RestoreFolder brings a trashed folder back: the folder and all of its subfolders (empty ones too),
|
||||
// then its notes. Returns the restored note IDs.
|
||||
func (s *Store) RestoreFolder(userID, id string) ([]string, error) {
|
||||
f, err := s.trashedFolder(userID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range append([]string{f.path}, f.folders...) {
|
||||
if _, err := s.CreateFolder(userID, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
docIDs, err := s.trashedFolderDocs(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, docID := range docIDs {
|
||||
if err := s.RestoreDocument(userID, docID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err = s.db.Exec(`DELETE FROM trashed_folders WHERE id = ?`, id)
|
||||
return docIDs, err
|
||||
}
|
||||
|
||||
// PurgeFolder deletes a trashed folder and its notes for good. Returns the deleted note IDs.
|
||||
func (s *Store) PurgeFolder(userID, id string) ([]string, error) {
|
||||
if _, err := s.trashedFolder(userID, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docIDs, err := s.trashedFolderDocs(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, docID := range docIDs {
|
||||
if err := s.hardDeleteDocument(docID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err = s.db.Exec(`DELETE FROM trashed_folders WHERE id = ?`, id)
|
||||
return docIDs, err
|
||||
}
|
||||
|
||||
func (s *Store) trashedDocOwner(userID, docID string) (string, error) {
|
||||
var owner string
|
||||
err := s.db.QueryRow(`SELECT user_id FROM documents WHERE id = ? AND `+trashScope, docID, userID, userID).Scan(&owner)
|
||||
@@ -101,7 +247,7 @@ func (s *Store) RestoreDocument(userID, docID string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE documents SET is_archived = 0, archived_at = '', archived_by = '' WHERE id = ?`, docID)
|
||||
_, err = s.db.Exec(`UPDATE documents SET is_archived = 0, archived_at = '', archived_by = '', trash_folder_id = '' WHERE id = ?`, docID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -113,7 +259,7 @@ func (s *Store) PurgeDocument(userID, docID string) error {
|
||||
return s.hardDeleteDocument(docID)
|
||||
}
|
||||
|
||||
// EmptyTrash deletes every note in the user's trash for good and returns their IDs.
|
||||
// EmptyTrash deletes every note and folder in the user's trash for good and returns the note IDs.
|
||||
func (s *Store) EmptyTrash(userID string) ([]string, error) {
|
||||
items, err := s.GetTrash(userID)
|
||||
if err != nil {
|
||||
@@ -121,6 +267,14 @@ func (s *Store) EmptyTrash(userID string) ([]string, error) {
|
||||
}
|
||||
ids := make([]string, 0, len(items))
|
||||
for _, t := range items {
|
||||
if t.Kind == "folder" {
|
||||
docIDs, err := s.PurgeFolder(userID, t.ID)
|
||||
ids = append(ids, docIDs...)
|
||||
if err != nil {
|
||||
return ids, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := s.hardDeleteDocument(t.ID); err != nil {
|
||||
return ids, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package storage
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
)
|
||||
@@ -91,3 +92,104 @@ func TestTrashHidesRestoresAndPurgesNotes(t *testing.T) {
|
||||
t.Errorf("purge must remove the note and its shares")
|
||||
}
|
||||
}
|
||||
|
||||
// A deleted personal folder goes to the trash as one entry with its notes and subfolders (empty ones too)
|
||||
// and comes back as it was; deleting it for good or letting it expire removes it with its notes.
|
||||
func TestDeletedFolderGoesToTrashAsAWhole(t *testing.T) {
|
||||
store, err := NewStore(filepath.Join(t.TempDir(), "foldertrash.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
anna, _ := store.CreateUser("anna@example.com", "hash", "Anna", "member")
|
||||
ben, _ := store.CreateUser("ben@example.com", "hash", "Ben", "member")
|
||||
for _, p := range []string{"Projekte", "Projekte/Stadt", "Projekte/Leer", "Privat"} {
|
||||
if _, err := store.CreateFolder(anna.ID, p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
top, _ := store.CreateDocument(anna.ID, "", "Übersicht", "", "Projekte", nil, nil, false)
|
||||
inner, _ := store.CreateDocument(anna.ID, "", "Verkehr", "", "Projekte/Stadt", nil, nil, false)
|
||||
loose, _ := store.CreateDocument(anna.ID, "", "Einzeln", "", "Privat", nil, nil, false)
|
||||
_ = store.DeleteDocument(anna.ID, loose.ID)
|
||||
|
||||
if _, err := store.DeleteFolder(anna.ID, "Projekte"); err != nil {
|
||||
t.Fatalf("DeleteFolder: %v", err)
|
||||
}
|
||||
folders, _ := store.GetFolders(anna.ID)
|
||||
for _, f := range folders {
|
||||
if f == "Projekte" || f == "Projekte/Leer" {
|
||||
t.Fatalf("deleted folders must be gone from the tree: %v", folders)
|
||||
}
|
||||
}
|
||||
trash, _ := store.GetTrash(anna.ID)
|
||||
var folderEntry *TrashedDocument
|
||||
for i := range trash {
|
||||
if trash[i].Kind == "folder" {
|
||||
folderEntry = &trash[i]
|
||||
} else if trash[i].ID != loose.ID {
|
||||
t.Errorf("notes of the folder belong to its entry, not to the list: %+v", trash[i])
|
||||
}
|
||||
}
|
||||
if len(trash) != 2 || folderEntry == nil || folderEntry.Title != "Projekte" || folderEntry.NoteCount != 2 {
|
||||
t.Fatalf("trash should list the folder with 2 notes and the single note: %+v", trash)
|
||||
}
|
||||
|
||||
if _, err := store.RestoreFolder(ben.ID, folderEntry.ID); err != ErrNotInTrash {
|
||||
t.Errorf("someone else must not restore the folder: %v", err)
|
||||
}
|
||||
restored, err := store.RestoreFolder(anna.ID, folderEntry.ID)
|
||||
if err != nil || len(restored) != 2 {
|
||||
t.Fatalf("RestoreFolder: %v %v", restored, err)
|
||||
}
|
||||
folders, _ = store.GetFolders(anna.ID)
|
||||
want := map[string]bool{"Projekte": false, "Projekte/Stadt": false, "Projekte/Leer": false}
|
||||
for _, f := range folders {
|
||||
if _, ok := want[f]; ok {
|
||||
want[f] = true
|
||||
}
|
||||
}
|
||||
for f, ok := range want {
|
||||
if !ok {
|
||||
t.Errorf("folder %q should be back (empty ones too): %v", f, folders)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{top.ID, inner.ID} {
|
||||
if d, err := store.GetDocumentByID(anna.ID, id); err != nil || d.IsArchived {
|
||||
t.Errorf("note %s should be back: %+v %v", id, d, err)
|
||||
}
|
||||
}
|
||||
if trash, _ = store.GetTrash(anna.ID); len(trash) != 1 || trash[0].ID != loose.ID {
|
||||
t.Fatalf("only the single note stays in the trash: %+v", trash)
|
||||
}
|
||||
|
||||
// Deleted for good: folder entry and notes are gone
|
||||
_, _ = store.DeleteFolder(anna.ID, "Projekte")
|
||||
trash, _ = store.GetTrash(anna.ID)
|
||||
for _, e := range trash {
|
||||
if e.Kind == "folder" {
|
||||
if ids, err := store.PurgeFolder(anna.ID, e.ID); err != nil || len(ids) != 2 {
|
||||
t.Fatalf("PurgeFolder: %v %v", ids, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
var n int
|
||||
_ = store.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE id IN (?, ?)`, top.ID, inner.ID).Scan(&n)
|
||||
if n != 0 {
|
||||
t.Errorf("notes of a purged folder must be gone")
|
||||
}
|
||||
|
||||
// Expired like notes: entry and notes vanish with the notes trash delay
|
||||
again, _ := store.CreateDocument(anna.ID, "", "Neu", "", "Archiv", nil, nil, false)
|
||||
_, _ = store.CreateFolder(anna.ID, "Archiv")
|
||||
_, _ = store.DeleteFolder(anna.ID, "Archiv")
|
||||
if _, err := store.PurgeExpired(time.Now().AddDate(0, 2, 0), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = store.db.QueryRow(`SELECT COUNT(*) FROM trashed_folders`).Scan(&n)
|
||||
var docs int
|
||||
_ = store.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE id = ?`, again.ID).Scan(&docs)
|
||||
if n != 0 || docs != 0 {
|
||||
t.Errorf("expired folder and its note should be gone: folders %d, notes %d", n, docs)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user