Internal
Public Access
Add Delete Folder to the profile tree context menu
ProfileRepository::deleteFolder() removes a folder without ever deleting
the profiles or subfolders inside it -- everything directly under the
deleted folder shifts up to take its place (its parent, or the top level
if it had none), exactly as if that one path segment were removed from
each affected path. This is a labels-only operation (a "folder" is just a
grouping string on each profile, not a container that owns them), so
that's the least-surprising behavior versus silently bulk-deleting saved
connections.
Right-clicking a folder in the tree now offers "Delete Folder"; if it
isn't empty, a confirmation states exactly how many profiles/subfolders
will move and to where.
Covered by 7 new unit tests, which caught the same class of bug fixed in
3fab2f9: the new code's own folder-path remap also bound an unguarded
null QString (a folder moving to root) against the `folder_path NOT
NULL` column.
Fixes #20.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,123 @@ bool ProfileRepository::createFolder(const QString& folderPath) const
|
||||
return true;
|
||||
}
|
||||
|
||||
// Deleting a folder never destroys profiles: everything directly inside it
|
||||
// (profiles and subfolders alike) is shifted up to take the deleted
|
||||
// folder's place, exactly as if that one path segment had been removed
|
||||
// from each of their paths. This is the least-surprising behavior for what
|
||||
// is fundamentally just an organizational label, not a container that owns
|
||||
// its contents.
|
||||
bool ProfileRepository::deleteFolder(const QString& folderPath) const
|
||||
{
|
||||
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString normalized = normalizedFolderPath(folderPath);
|
||||
if (normalized.isEmpty()) {
|
||||
setLastError(QStringLiteral("Folder path is required."));
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
|
||||
QString parentPath;
|
||||
const int lastSlash = normalized.lastIndexOf(QChar::fromLatin1('/'));
|
||||
if (lastSlash >= 0) {
|
||||
parentPath = normalized.left(lastSlash);
|
||||
}
|
||||
|
||||
const QString prefix = normalized + QStringLiteral("/");
|
||||
auto remap = [&](const QString& oldPath) {
|
||||
if (oldPath == normalized) {
|
||||
return nonNullTrimmed(parentPath);
|
||||
}
|
||||
const QString remainder = oldPath.mid(prefix.length());
|
||||
return nonNullTrimmed(parentPath.isEmpty() ? remainder
|
||||
: parentPath + QStringLiteral("/") + remainder);
|
||||
};
|
||||
|
||||
QSqlDatabase database = QSqlDatabase::database(m_connectionName);
|
||||
if (!database.transaction()) {
|
||||
setLastError(database.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reassign affected profiles. Matching is done in C++ (not SQL LIKE)
|
||||
// so a folder name containing '%' or '_' can't be misinterpreted as a
|
||||
// wildcard.
|
||||
QSqlQuery selectProfiles(database);
|
||||
if (!selectProfiles.exec(QStringLiteral("SELECT id, folder_path FROM profiles"))) {
|
||||
setLastError(selectProfiles.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
std::vector<std::pair<qint64, QString>> profileUpdates;
|
||||
while (selectProfiles.next()) {
|
||||
const QString path = normalizedFolderPath(selectProfiles.value(1).toString());
|
||||
if (path == normalized || path.startsWith(prefix)) {
|
||||
profileUpdates.emplace_back(selectProfiles.value(0).toLongLong(), remap(path));
|
||||
}
|
||||
}
|
||||
for (const auto& [id, newPath] : profileUpdates) {
|
||||
QSqlQuery update(database);
|
||||
update.prepare(QStringLiteral("UPDATE profiles SET folder_path = ? WHERE id = ?"));
|
||||
update.addBindValue(newPath);
|
||||
update.addBindValue(id);
|
||||
if (!update.exec()) {
|
||||
setLastError(update.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign (or drop, for the folder itself) affected explicit folder
|
||||
// markers the same way.
|
||||
QSqlQuery selectFolders(database);
|
||||
if (!selectFolders.exec(QStringLiteral("SELECT path FROM profile_folders"))) {
|
||||
setLastError(selectFolders.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
QStringList affectedFolders;
|
||||
while (selectFolders.next()) {
|
||||
const QString path = normalizedFolderPath(selectFolders.value(0).toString());
|
||||
if (path == normalized || path.startsWith(prefix)) {
|
||||
affectedFolders.push_back(path);
|
||||
}
|
||||
}
|
||||
for (const QString& oldPath : affectedFolders) {
|
||||
QSqlQuery remove(database);
|
||||
remove.prepare(QStringLiteral("DELETE FROM profile_folders WHERE path = ?"));
|
||||
remove.addBindValue(oldPath);
|
||||
if (!remove.exec()) {
|
||||
setLastError(remove.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString newPath = remap(oldPath);
|
||||
if (oldPath != normalized && !newPath.isEmpty()) {
|
||||
QSqlQuery insert(database);
|
||||
insert.prepare(QStringLiteral("INSERT OR IGNORE INTO profile_folders(path) VALUES (?)"));
|
||||
insert.addBindValue(newPath);
|
||||
if (!insert.exec()) {
|
||||
setLastError(insert.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!database.commit()) {
|
||||
setLastError(database.lastError().text());
|
||||
database.rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery,
|
||||
ProfileSortOrder sortOrder) const
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user