MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - app - FolderOperationsController.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 94.1 % 152 143
Test Date: 2026-06-21 21:10:19 Functions: 94.7 % 19 18
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 55.4 % 280 155

             Branch data     Line data    Source code
       1                 :             : #include "FolderOperationsController.h"
       2                 :             : 
       3                 :             : #include <QInputDialog>
       4                 :             : #include <QLineEdit>
       5                 :             : #include <QMessageBox>
       6                 :             : #include <algorithm>
       7                 :             : #include <memory>
       8                 :             : 
       9                 :             : #include "service/ImapService.h"
      10                 :             : #include "ui/CommandBar.h"
      11                 :             : #include "ui/FolderTree.h"
      12                 :             : 
      13                 :          58 : FolderOperationsController::FolderOperationsController(const Deps &deps,
      14                 :          58 :                                                        QObject *parent)
      15                 :          58 :     : QObject(parent), m_d(deps) {
      16                 :             :   // Modal test seams: real dialogs by default (unit tests override).
      17                 :           1 :   m_promptText = [this](const QString &title, const QString &label,
      18                 :             :                         const QString &initial, bool *ok) {
      19                 :           1 :     return QInputDialog::getText(m_d.folderTree, title, label,
      20         [ +  - ]:           1 :                                  QLineEdit::Normal, initial, ok);
      21                 :          58 :   };
      22                 :         117 :   m_confirm = [this](const QString &title, const QString &text) {
      23         [ +  - ]:           1 :     return QMessageBox::question(m_d.folderTree, title, text) ==
      24                 :           1 :            QMessageBox::Yes;
      25                 :          58 :   };
      26                 :             : 
      27                 :             :   // T-290: FolderTree context-menu requests
      28                 :          58 :   connect(m_d.folderTree, &FolderTree::createFolderRequested, this,
      29         [ +  - ]:          58 :           &FolderOperationsController::createFolder);
      30                 :          58 :   connect(m_d.folderTree, &FolderTree::deleteFolderRequested, this,
      31         [ +  - ]:          58 :           &FolderOperationsController::deleteFolder);
      32                 :          58 :   connect(m_d.folderTree, &FolderTree::renameFolderRequested, this,
      33         [ +  - ]:          58 :           &FolderOperationsController::renameFolder);
      34                 :          58 :   connect(m_d.folderTree, &FolderTree::moveFolderRequested, this,
      35         [ +  - ]:          58 :           &FolderOperationsController::moveFolder);
      36                 :          58 : }
      37                 :             : 
      38                 :          33 : QStringList FolderOperationsController::folderFlagsForPath(
      39                 :             :     const QString &folderPath) const {
      40         [ +  + ]:          55 :   for (const auto &folder : m_folders) {
      41         [ +  + ]:          44 :     if (folder.path == folderPath)
      42                 :          22 :       return folder.flags;
      43                 :             :   }
      44                 :          11 :   return {};
      45                 :             : }
      46                 :             : 
      47                 :          30 : bool FolderOperationsController::isProtectedFolderPath(
      48                 :             :     const QString &folderPath) const {
      49         [ +  - ]:          30 :   return FolderTree::isSpecialFolder(folderPath,
      50         [ +  - ]:          60 :                                      folderFlagsForPath(folderPath));
      51                 :             : }
      52                 :             : 
      53                 :           9 : void FolderOperationsController::createFolder(const QString &parentPath) {
      54                 :           9 :   bool ok = false;
      55                 :             :   QString name = m_promptText(
      56                 :          18 :       QStringLiteral("Neuer Ordner"),
      57                 :           9 :       parentPath.isEmpty()
      58   [ +  +  +  -  :          32 :           ? QStringLiteral("Name des neuen Ordners:")
                   -  - ]
      59   [ +  +  +  +  :          13 :           : QStringLiteral("Name des neuen Unterordners von '%1':")
          +  +  -  -  -  
                      - ]
      60                 :             :                 .arg(parentPath),
      61         [ +  - ]:          27 :       QString(), &ok);
      62                 :             : 
      63   [ +  +  +  -  :           9 :   if (!ok || name.trimmed().isEmpty())
          +  +  +  +  +  
                +  -  - ]
      64                 :           4 :     return;
      65                 :             : 
      66         [ +  - ]:           5 :   name = name.trimmed();
      67                 :           5 :   QString fullPath = parentPath.isEmpty()
      68                 :           5 :                          ? name
      69   [ +  +  +  -  :           5 :                          : parentPath + effectiveDelimiter() + name;
          +  -  +  -  +  
          +  +  +  -  -  
                   -  - ]
      70                 :             : 
      71   [ +  -  +  - ]:           5 :   m_d.imap->executeAfterIdle(
      72                 :          15 :       [this, fullPath]() { m_d.imap->createFolder(fullPath); });
      73         [ +  + ]:           9 : }
      74                 :             : 
      75                 :           9 : void FolderOperationsController::deleteFolder(const QString &folderPath) {
      76   [ +  -  +  + ]:           9 :   if (isProtectedFolderPath(folderPath)) {
      77         [ +  - ]:           2 :     emit keyedStatusMessage(
      78                 :           4 :         QStringLiteral("folder"),
      79                 :           4 :         QStringLiteral("Spezialordner koennen nicht geloescht werden"), 3000);
      80                 :           5 :     return;
      81                 :             :   }
      82                 :             : 
      83                 :             :   // Count children for warning
      84         [ +  - ]:           7 :   const QString delimiter = effectiveDelimiter();
      85                 :           7 :   int childCount = 0;
      86                 :           7 :   QStringList childPaths;
      87   [ +  -  +  -  :          26 :   for (const auto &f : m_folders) {
                   +  + ]
      88   [ +  -  +  -  :          19 :     if (f.path.startsWith(folderPath + delimiter)) {
                   +  + ]
      89                 :           4 :       childCount++;
      90         [ +  - ]:           4 :       childPaths.append(f.path);
      91                 :             :     }
      92                 :             :   }
      93                 :             : 
      94                 :             :   QString msg =
      95         [ +  - ]:          14 :       QStringLiteral("Ordner '%1' wirklich loeschen?").arg(folderPath);
      96         [ +  + ]:           7 :   if (childCount > 0)
      97                 :           4 :     msg += QStringLiteral("\n\nDieser Ordner hat %1 Unterordner, die "
      98                 :             :                           "ebenfalls geloescht werden.")
      99   [ +  -  +  - ]:           4 :                .arg(childCount);
     100                 :             : 
     101   [ +  -  +  + ]:          14 :   if (!m_confirm(QStringLiteral("Ordner loeschen"), msg))
     102                 :           3 :     return;
     103                 :             : 
     104                 :             :   // Build delete list: children first (bottom-up by path depth)
     105                 :           4 :   QStringList deleteList = childPaths;
     106   [ +  -  +  -  :           4 :   std::sort(deleteList.begin(), deleteList.end(),
                   +  - ]
     107                 :           2 :             [&delimiter](const QString &a, const QString &b) {
     108                 :           2 :               return a.count(delimiter) > b.count(delimiter);
     109                 :             :             });
     110         [ +  - ]:           4 :   deleteList.append(folderPath); // parent last
     111                 :             : 
     112                 :             :   // FIX: IMAP servers send BYE when deleting the currently SELECTed folder.
     113                 :             :   // We must switch to INBOX before starting the delete chain.
     114                 :             :   // Check if current folder is the folder being deleted or a child of it.
     115                 :           4 :   QString currentFolder = m_d.imap->selectedFolder();
     116         [ +  - ]:           8 :   bool needSwitch = (currentFolder == folderPath) ||
     117   [ +  -  +  -  :           8 :                     currentFolder.startsWith(folderPath + delimiter);
          -  +  +  -  -  
                      - ]
     118                 :             : 
     119                 :             :   // Chain sequential deletes
     120         [ +  - ]:           4 :   auto remaining = std::make_shared<QStringList>(deleteList);
     121         [ +  - ]:           4 :   auto deleteNext = std::make_shared<std::function<void()>>();
     122         [ -  - ]:           8 :   *deleteNext = [this, remaining, deleteNext]() {
     123         [ -  + ]:           6 :     if (remaining->isEmpty())
     124                 :           0 :       return;
     125         [ +  - ]:           6 :     QString next = remaining->takeFirst();
     126         [ +  + ]:           6 :     if (!remaining->isEmpty()) {
     127         [ +  - ]:           2 :       auto conn = std::make_shared<QMetaObject::Connection>();
     128                 :           4 :       *conn = connect(m_d.imap, &ImapService::folderDeleted, this,
     129   [ +  -  -  - ]:           4 :                       [deleteNext, conn](const QString &) {
     130                 :           2 :                         QObject::disconnect(*conn);
     131                 :           2 :                         (*deleteNext)();
     132                 :           2 :                       });
     133                 :           2 :     }
     134   [ +  -  +  - ]:           6 :     m_d.imap->executeAfterIdle(
     135                 :          18 :         [this, next]() { m_d.imap->deleteFolder(next); });
     136         [ +  - ]:          10 :   };
     137                 :             : 
     138         [ -  + ]:           4 :   if (needSwitch) {
     139                 :             :     // Switch to INBOX first, then start deleting
     140         [ #  # ]:           0 :     m_d.folderTree->selectFolder(QStringLiteral("INBOX"));
     141                 :             :     // Wait for SELECT INBOX to complete before deleting
     142         [ #  # ]:           0 :     auto conn = std::make_shared<QMetaObject::Connection>();
     143                 :           0 :     *conn = connect(m_d.imap, &ImapService::folderSelected, this,
     144   [ #  #  #  # ]:           0 :                     [deleteNext, conn](const QString &, int, quint32, quint64) {
     145                 :           0 :                       QObject::disconnect(*conn);
     146                 :           0 :                       (*deleteNext)();
     147                 :           0 :                     });
     148                 :           0 :   } else {
     149         [ +  - ]:           4 :     (*deleteNext)();
     150                 :             :   }
     151   [ +  +  +  +  :          13 : }
                   +  + ]
     152                 :             : 
     153                 :           5 : void FolderOperationsController::renameFolder(const QString &folderPath) {
     154   [ +  -  +  + ]:           5 :   if (isProtectedFolderPath(folderPath)) {
     155         [ +  - ]:           1 :     emit keyedStatusMessage(
     156                 :           2 :         QStringLiteral("folder"),
     157                 :           2 :         QStringLiteral("Spezialordner koennen nicht umbenannt werden"), 3000);
     158                 :           2 :     return;
     159                 :             :   }
     160                 :             : 
     161                 :             :   // Extract current name (last segment after delimiter)
     162         [ +  - ]:           4 :   const QString delimiter = effectiveDelimiter();
     163         [ +  - ]:           4 :   int lastSep = folderPath.lastIndexOf(delimiter);
     164                 :             :   QString currentName = (lastSep >= 0)
     165         [ +  + ]:           4 :                             ? folderPath.mid(lastSep + delimiter.length())
     166         [ +  - ]:           4 :                             : folderPath;
     167   [ +  +  +  - ]:           4 :   QString parentPath = (lastSep >= 0) ? folderPath.left(lastSep) : QString();
     168                 :             : 
     169                 :           4 :   bool ok = false;
     170                 :             :   QString newName = m_promptText(
     171                 :           8 :       QStringLiteral("Ordner umbenennen"),
     172         [ +  - ]:          12 :       QStringLiteral("Neuer Name für '%1':").arg(currentName),
     173         [ +  - ]:           8 :       currentName, &ok);
     174                 :             : 
     175   [ +  -  +  -  :           4 :   if (!ok || newName.trimmed().isEmpty() || newName.trimmed() == currentName)
          +  -  +  -  +  
          +  +  -  +  -  
          +  +  -  -  -  
                      - ]
     176                 :           1 :     return;
     177                 :             : 
     178         [ +  - ]:           3 :   newName = newName.trimmed();
     179                 :             :   QString newPath =
     180   [ +  +  +  -  :           3 :       parentPath.isEmpty() ? newName : parentPath + delimiter + newName;
          +  -  +  +  -  
                      - ]
     181                 :             : 
     182   [ +  -  +  -  :           3 :   m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
                   -  - ]
     183                 :           3 :     m_d.imap->renameFolder(folderPath, newPath);
     184                 :           3 :   });
     185   [ +  +  +  +  :           7 : }
             +  +  +  + ]
     186                 :             : 
     187                 :           6 : void FolderOperationsController::moveFolder(const QString &folderPath) {
     188   [ +  -  +  + ]:           6 :   if (isProtectedFolderPath(folderPath)) {
     189         [ +  - ]:           1 :     emit keyedStatusMessage(
     190                 :           2 :         QStringLiteral("folder"),
     191                 :           2 :         QStringLiteral("Spezialordner koennen nicht verschoben werden"), 3000);
     192                 :           1 :     return;
     193                 :             :   }
     194                 :             : 
     195                 :             :   // Activate CommandBar in FolderSwitch mode to pick target
     196         [ +  - ]:           5 :   m_d.commandBar->activate(CommandBar::FolderSwitch);
     197                 :             : 
     198                 :             :   // One-shot connection: when folder is selected, perform RENAME
     199         [ +  - ]:           5 :   auto conn = std::make_shared<QMetaObject::Connection>();
     200                 :          10 :   *conn = connect(
     201                 :           5 :       m_d.commandBar, &CommandBar::folderSelected, this,
     202   [ +  -  -  - ]:          10 :       [this, folderPath, conn](CommandBar::Mode mode,
     203                 :             :                                const QString &targetFolder) {
     204         [ +  - ]:           3 :         QObject::disconnect(*conn);
     205         [ +  + ]:           3 :         if (mode != CommandBar::FolderSwitch)
     206                 :           1 :           return;
     207                 :             : 
     208                 :             :         // Build new path: targetFolder + delimiter + folderName
     209         [ +  - ]:           2 :         const QString delimiter = effectiveDelimiter();
     210         [ +  - ]:           2 :         int lastSep = folderPath.lastIndexOf(delimiter);
     211                 :             :         QString folderName = (lastSep >= 0)
     212         [ -  + ]:           2 :                                  ? folderPath.mid(lastSep + delimiter.length())
     213         [ -  - ]:           2 :                                  : folderPath;
     214   [ +  -  +  - ]:           2 :         QString newPath = targetFolder + delimiter + folderName;
     215                 :             : 
     216   [ +  -  +  -  :           2 :         m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
                   -  - ]
     217                 :           2 :           m_d.imap->renameFolder(folderPath, newPath);
     218                 :           2 :         });
     219                 :           7 :       });
     220                 :             : 
     221                 :             :   // Clean up if cancelled
     222         [ +  - ]:           5 :   auto cancelConn = std::make_shared<QMetaObject::Connection>();
     223                 :          10 :   *cancelConn = connect(m_d.commandBar, &CommandBar::cancelled, this,
     224   [ +  -  -  - ]:          10 :                         [conn, cancelConn]() {
     225                 :           3 :                           QObject::disconnect(*conn);
     226                 :           3 :                           QObject::disconnect(*cancelConn);
     227                 :           5 :                         });
     228                 :           5 : }
        

Generated by: LCOV version 2.0-1