forked from qt-creator/qt-creator
Axivion: Support named filters
Support loading, displaying and applying of existing named filters to the current displayed issues. Change-Id: Iab079228c8ffbfa75d1bc4ff3d378fc9b6bd1c53 Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
This commit is contained in:
@@ -185,6 +185,7 @@ public:
|
||||
void updateUi(const QString &kind);
|
||||
void initDashboardList(const QString &preferredProject = {});
|
||||
void resetDashboard();
|
||||
void updateNamedFilters();
|
||||
|
||||
const std::optional<Dto::TableInfoDto> currentTableInfo() const { return m_currentTableInfo; }
|
||||
IssueListSearch searchFromUi() const;
|
||||
@@ -201,6 +202,7 @@ private:
|
||||
void onSearchParameterChanged();
|
||||
void updateVersionItemsEnabledState();
|
||||
void updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDto> &info);
|
||||
void updateAllFilters(const QVariant &namedFilter);
|
||||
void setFiltersEnabled(bool enabled);
|
||||
void fetchTable();
|
||||
void fetchIssues(const IssueListSearch &search);
|
||||
@@ -219,6 +221,7 @@ private:
|
||||
QComboBox *m_ownerFilter = nullptr;
|
||||
QComboBox *m_versionStart = nullptr;
|
||||
QComboBox *m_versionEnd = nullptr;
|
||||
QComboBox *m_namedFilters = nullptr;
|
||||
Guard m_signalBlocker;
|
||||
QLineEdit *m_pathGlobFilter = nullptr; // FancyLineEdit instead?
|
||||
QLabel *m_totalRows = nullptr;
|
||||
@@ -334,6 +337,15 @@ IssuesWidget::IssuesWidget(QWidget *parent)
|
||||
m_pathGlobFilter->setPlaceholderText(Tr::tr("Path globbing"));
|
||||
connect(m_pathGlobFilter, &QLineEdit::textEdited, this, &IssuesWidget::onSearchParameterChanged);
|
||||
|
||||
m_namedFilters = new QComboBox(this);
|
||||
m_namedFilters->setToolTip(Tr::tr("Named filters"));
|
||||
m_namedFilters->setMinimumContentsLength(25);
|
||||
connect(m_namedFilters, &QComboBox::currentIndexChanged, this, [this] {
|
||||
if (m_signalBlocker.isLocked())
|
||||
return;
|
||||
updateAllFilters(m_namedFilters->currentData());
|
||||
});
|
||||
|
||||
m_issuesView = new BaseTreeView(this);
|
||||
m_issuesView->setFrameShape(QFrame::StyledPanel); // Bring back Qt default
|
||||
m_issuesView->setFrameShadow(QFrame::Sunken); // Bring back Qt default
|
||||
@@ -372,7 +384,7 @@ IssuesWidget::IssuesWidget(QWidget *parent)
|
||||
|
||||
Column {
|
||||
Row { m_dashboards, m_dashboardProjects, empty, m_typesLayout, st, m_versionStart, m_versionEnd, st },
|
||||
Row { m_addedFilter, m_removedFilter, Space(1), m_ownerFilter, m_pathGlobFilter },
|
||||
Row { m_addedFilter, m_removedFilter, Space(1), m_ownerFilter, m_pathGlobFilter, m_namedFilters },
|
||||
m_stack,
|
||||
Row { st, m_totalRows }
|
||||
}.attachTo(widget);
|
||||
@@ -436,6 +448,28 @@ void IssuesWidget::resetDashboard()
|
||||
m_dashboardListUninitialized = true;
|
||||
}
|
||||
|
||||
void IssuesWidget::updateNamedFilters()
|
||||
{
|
||||
QList<NamedFilter> globalFilters;
|
||||
QList<NamedFilter> userFilters;
|
||||
knownNamedFilters(&globalFilters, &userFilters);
|
||||
|
||||
Utils::sort(globalFilters, [](const NamedFilter &lhs, const NamedFilter &rhs) {
|
||||
return lhs.displayName < rhs.displayName;
|
||||
});
|
||||
Utils::sort(userFilters, [](const NamedFilter &lhs, const NamedFilter &rhs) {
|
||||
return lhs.displayName < rhs.displayName;
|
||||
});
|
||||
GuardLocker lock(m_signalBlocker);
|
||||
m_namedFilters->clear();
|
||||
|
||||
m_namedFilters->addItem(Tr::tr("Show all")); // no active named filter
|
||||
for (const auto &it : userFilters)
|
||||
m_namedFilters->addItem(it.displayName, QVariant::fromValue(it));
|
||||
for (const auto &it : globalFilters)
|
||||
m_namedFilters->addItem(it.displayName, QVariant::fromValue(it));
|
||||
}
|
||||
|
||||
void IssuesWidget::initDashboardList(const QString &preferredProject)
|
||||
{
|
||||
const QString currentProject = preferredProject.isEmpty() ? m_dashboardProjects->currentText()
|
||||
@@ -477,10 +511,13 @@ void IssuesWidget::reinitProjectList(const QString ¤tProject)
|
||||
m_issuesView->hideProgressIndicator();
|
||||
return;
|
||||
}
|
||||
{
|
||||
GuardLocker lock(m_signalBlocker);
|
||||
m_dashboardProjects->addItems(info->projects);
|
||||
if (!currentProject.isEmpty() && info->projects.contains(currentProject))
|
||||
m_dashboardProjects->setCurrentText(currentProject);
|
||||
}
|
||||
fetchNamedFilters();
|
||||
};
|
||||
{
|
||||
GuardLocker lock(m_signalBlocker);
|
||||
@@ -693,6 +730,7 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDt
|
||||
m_versionStart->clear();
|
||||
m_versionEnd->clear();
|
||||
m_pathGlobFilter->clear();
|
||||
m_namedFilters->clear();
|
||||
|
||||
m_currentProject.clear();
|
||||
m_currentPrefix.clear();
|
||||
@@ -755,6 +793,30 @@ void IssuesWidget::updateBasicProjectInfo(const std::optional<Dto::ProjectInfoDt
|
||||
updateVersionItemsEnabledState();
|
||||
}
|
||||
|
||||
void IssuesWidget::updateAllFilters(const QVariant &namedFilter)
|
||||
{
|
||||
NamedFilter nf;
|
||||
if (namedFilter.isValid())
|
||||
nf = namedFilter.value<NamedFilter>();
|
||||
const bool clearOnly = nf.key.isEmpty();
|
||||
const std::optional<Dto::NamedFilterInfoDto> filterInfo
|
||||
= clearOnly ? std::nullopt : namedFilterInfoForKey(nf.key, nf.global);
|
||||
|
||||
GuardLocker lock(m_signalBlocker);
|
||||
if (filterInfo) {
|
||||
m_headerView->updateExistingColumnInfos(filterInfo->filters, filterInfo->sorters);
|
||||
const auto it = filterInfo->filters.find("any path");
|
||||
if (it != filterInfo->filters.cend())
|
||||
m_pathGlobFilter->setText(it->second);
|
||||
else
|
||||
m_pathGlobFilter->clear();
|
||||
} else {
|
||||
// clear all filters / sorters
|
||||
m_headerView->updateExistingColumnInfos({}, std::nullopt);
|
||||
m_pathGlobFilter->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void IssuesWidget::setFiltersEnabled(bool enabled)
|
||||
{
|
||||
m_addedFilter->setEnabled(enabled);
|
||||
@@ -763,6 +825,7 @@ void IssuesWidget::setFiltersEnabled(bool enabled)
|
||||
m_versionStart->setEnabled(enabled);
|
||||
m_versionEnd->setEnabled(enabled);
|
||||
m_pathGlobFilter->setEnabled(enabled);
|
||||
m_namedFilters->setEnabled(enabled);
|
||||
}
|
||||
|
||||
IssueListSearch IssuesWidget::searchFromUi() const
|
||||
@@ -966,6 +1029,7 @@ public:
|
||||
void setIssueDetailsHtml(const QString &html) { m_issueDetails->setHtml(html); }
|
||||
void handleAnchorClicked(const QUrl &url);
|
||||
void updateToolbarButtons();
|
||||
void updateNamedFilters();
|
||||
|
||||
private:
|
||||
void openFilterHelp();
|
||||
@@ -1144,6 +1208,11 @@ void AxivionPerspective::updateToolbarButtons()
|
||||
m_showFilterHelp->setEnabled(pInfo && pInfo->issueFilterHelp);
|
||||
}
|
||||
|
||||
void AxivionPerspective::updateNamedFilters()
|
||||
{
|
||||
m_issuesWidget->updateNamedFilters();
|
||||
}
|
||||
|
||||
void AxivionPerspective::openFilterHelp()
|
||||
{
|
||||
const std::optional<Dto::ProjectInfoDto> projInfo = projectInfo();
|
||||
@@ -1213,4 +1282,10 @@ void updatePerspectiveToolbar()
|
||||
theAxivionPerspective->updateToolbarButtons();
|
||||
}
|
||||
|
||||
void updateNamedFilters()
|
||||
{
|
||||
QTC_ASSERT(theAxivionPerspective, return);
|
||||
theAxivionPerspective->updateNamedFilters();
|
||||
}
|
||||
|
||||
} // Axivion::Internal
|
||||
|
@@ -15,5 +15,6 @@ void reinitDashboard(const QString &projectName);
|
||||
void resetDashboard();
|
||||
void updateIssueDetails(const QString &html);
|
||||
void updatePerspectiveToolbar();
|
||||
void updateNamedFilters();
|
||||
|
||||
} // Axivion::Internal
|
||||
|
@@ -155,7 +155,15 @@ static DashboardInfo toDashboardInfo(const GetDtoStorage<Dto::DashboardInfoDto>
|
||||
projectUrls.insert(project.name, project.url);
|
||||
}
|
||||
}
|
||||
return {dashboardStorage.url, versionNumber, projects, projectUrls, infoDto.checkCredentialsUrl};
|
||||
return {
|
||||
dashboardStorage.url,
|
||||
versionNumber,
|
||||
projects,
|
||||
projectUrls,
|
||||
infoDto.checkCredentialsUrl,
|
||||
infoDto.namedFiltersUrl,
|
||||
infoDto.userNamedFiltersUrl
|
||||
};
|
||||
}
|
||||
|
||||
QUrlQuery IssueListSearch::toUrlQuery(QueryMode mode) const
|
||||
@@ -213,6 +221,7 @@ public:
|
||||
void handleIssuesForFile(const Dto::FileViewDto &fileView);
|
||||
void enableInlineIssues(bool enable);
|
||||
void fetchIssueInfo(const QString &id);
|
||||
void fetchNamedFilters();
|
||||
|
||||
void onSessionLoaded(const QString &sessionName);
|
||||
void onAboutToSaveSession();
|
||||
@@ -229,11 +238,14 @@ public:
|
||||
std::optional<DashboardInfo> m_dashboardInfo;
|
||||
std::optional<Dto::ProjectInfoDto> m_currentProjectInfo;
|
||||
std::optional<QString> m_analysisVersion;
|
||||
QList<Dto::NamedFilterInfoDto> m_globalNamedFilters;
|
||||
QList<Dto::NamedFilterInfoDto> m_userNamedFilters;
|
||||
Project *m_project = nullptr;
|
||||
bool m_runningQuery = false;
|
||||
TaskTreeRunner m_taskTreeRunner;
|
||||
std::unordered_map<IDocument *, std::unique_ptr<TaskTree>> m_docMarksTrees;
|
||||
TaskTreeRunner m_issueInfoRunner;
|
||||
TaskTreeRunner m_namedFilterRunner;
|
||||
FileInProjectFinder m_fileFinder; // FIXME maybe obsolete when path mapping is implemented
|
||||
QMetaObject::Connection m_fileFinderConnection;
|
||||
QHash<FilePath, QSet<TextMark *>> m_allMarks;
|
||||
@@ -279,6 +291,46 @@ std::optional<Dto::ProjectInfoDto> projectInfo()
|
||||
return dd->m_currentProjectInfo;
|
||||
}
|
||||
|
||||
void fetchNamedFilters()
|
||||
{
|
||||
QTC_ASSERT(dd, return);
|
||||
dd->fetchNamedFilters();
|
||||
}
|
||||
|
||||
void knownNamedFilters(QList<NamedFilter> *global, QList<NamedFilter> *user)
|
||||
{
|
||||
QTC_ASSERT(dd, return);
|
||||
QTC_ASSERT(global, return);
|
||||
QTC_ASSERT(user, return);
|
||||
|
||||
*global = Utils::transform(dd->m_globalNamedFilters, [](const Dto::NamedFilterInfoDto &dto) {
|
||||
return NamedFilter{dto.key, dto.displayName, true};
|
||||
});
|
||||
*user = Utils::transform(dd->m_userNamedFilters, [](const Dto::NamedFilterInfoDto &dto) {
|
||||
return NamedFilter{dto.key, dto.displayName, false};
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<Dto::NamedFilterInfoDto> namedFilterInfoForKey(const QString &key, bool global)
|
||||
{
|
||||
QTC_ASSERT(dd, return std::nullopt);
|
||||
|
||||
const auto findFilter = [](const QList<Dto::NamedFilterInfoDto> filters, const QString &key)
|
||||
-> std::optional<Dto::NamedFilterInfoDto> {
|
||||
const int index = Utils::indexOf(filters, [key](const Dto::NamedFilterInfoDto &dto) {
|
||||
return dto.key == key;
|
||||
});
|
||||
if (index == -1)
|
||||
return std::nullopt;
|
||||
return filters.at(index);
|
||||
};
|
||||
|
||||
if (global)
|
||||
return findFilter(dd->m_globalNamedFilters, key);
|
||||
else
|
||||
return findFilter(dd->m_userNamedFilters, key);
|
||||
}
|
||||
|
||||
// FIXME: extend to give some details?
|
||||
// FIXME: move when curl is no more in use?
|
||||
bool handleCertificateIssue(const Utils::Id &serverId)
|
||||
@@ -388,6 +440,7 @@ static QByteArray contentTypeData(ContentType contentType)
|
||||
{
|
||||
switch (contentType) {
|
||||
case ContentType::Html: return s_htmlContentType;
|
||||
case ContentType::Json: return s_jsonContentType;
|
||||
case ContentType::PlainText: return s_plaintextContentType;
|
||||
case ContentType::Svg: return s_svgContentType;
|
||||
}
|
||||
@@ -910,6 +963,62 @@ void AxivionPluginPrivate::fetchIssueInfo(const QString &id)
|
||||
});
|
||||
}
|
||||
|
||||
static QList<Dto::NamedFilterInfoDto> extractNamedFiltersFromJsonArray(const QByteArray &json)
|
||||
{
|
||||
QList<Dto::NamedFilterInfoDto> result;
|
||||
QJsonParseError error;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json, &error);
|
||||
if (error.error != QJsonParseError::NoError)
|
||||
return result;
|
||||
if (!doc.isArray())
|
||||
return result;
|
||||
const QJsonArray array = doc.array();
|
||||
for (const QJsonValue &value : array) {
|
||||
if (!value.isObject())
|
||||
continue;
|
||||
const QJsonDocument objDocument(value.toObject());
|
||||
const auto filter = Dto::NamedFilterInfoDto::deserializeExpected(objDocument.toJson());
|
||||
if (filter)
|
||||
result.append(*filter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AxivionPluginPrivate::fetchNamedFilters()
|
||||
{
|
||||
QTC_ASSERT(m_dashboardInfo, return);
|
||||
|
||||
// use simple downloadDatarecipe() as we cannot handle an array of a dto at the moment
|
||||
const Storage<DownloadData> globalStorage;
|
||||
const Storage<DownloadData> userStorage;
|
||||
|
||||
const auto onSetup = [this, globalStorage, userStorage] {
|
||||
QTC_ASSERT(m_dashboardInfo, return);
|
||||
globalStorage->inputUrl = m_dashboardInfo->source.resolved(
|
||||
*m_dashboardInfo->globalNamedFilters);
|
||||
globalStorage->expectedContentType = ContentType::Json;
|
||||
userStorage->inputUrl = m_dashboardInfo->source.resolved(
|
||||
*m_dashboardInfo->userNamedFilters);
|
||||
userStorage->expectedContentType = ContentType::Json;
|
||||
};
|
||||
const auto onDone = [this, globalStorage, userStorage] {
|
||||
m_globalNamedFilters = extractNamedFiltersFromJsonArray(globalStorage->outputData);
|
||||
m_userNamedFilters = extractNamedFiltersFromJsonArray(userStorage->outputData);
|
||||
updateNamedFilters();
|
||||
};
|
||||
|
||||
Group namedFiltersGroup = Group {
|
||||
globalStorage,
|
||||
userStorage,
|
||||
onGroupSetup(onSetup),
|
||||
downloadDataRecipe(globalStorage) || successItem,
|
||||
downloadDataRecipe(userStorage) || successItem,
|
||||
onGroupDone(onDone)
|
||||
};
|
||||
|
||||
m_namedFilterRunner.start(namedFiltersGroup);
|
||||
}
|
||||
|
||||
void AxivionPluginPrivate::handleOpenedDocs()
|
||||
{
|
||||
const QList<IDocument *> openDocuments = DocumentModel::openedDocuments();
|
||||
@@ -1086,7 +1195,10 @@ void switchActiveDashboardId(const Id &toDashboardId)
|
||||
dd->m_apiToken.reset();
|
||||
dd->m_dashboardInfo.reset();
|
||||
dd->m_currentProjectInfo.reset();
|
||||
dd->m_globalNamedFilters.clear();
|
||||
dd->m_userNamedFilters.clear();
|
||||
updatePerspectiveToolbar();
|
||||
updateNamedFilters();
|
||||
}
|
||||
|
||||
const std::optional<DashboardInfo> currentDashboardInfo()
|
||||
|
@@ -62,10 +62,13 @@ public:
|
||||
QStringList projects;
|
||||
QHash<QString, QUrl> projectUrls;
|
||||
std::optional<QUrl> checkCredentialsUrl;
|
||||
std::optional<QUrl> globalNamedFilters;
|
||||
std::optional<QUrl> userNamedFilters;
|
||||
};
|
||||
|
||||
enum class ContentType {
|
||||
Html,
|
||||
Json,
|
||||
PlainText,
|
||||
Svg
|
||||
};
|
||||
@@ -101,6 +104,18 @@ Tasking::Group lineMarkerRecipe(const Utils::FilePath &filePath, const LineMarke
|
||||
|
||||
void fetchDashboardAndProjectInfo(const DashboardInfoHandler &handler, const QString &projectName);
|
||||
std::optional<Dto::ProjectInfoDto> projectInfo();
|
||||
|
||||
struct NamedFilter
|
||||
{
|
||||
QString key;
|
||||
QString displayName;
|
||||
bool global = false;
|
||||
};
|
||||
|
||||
void fetchNamedFilters();
|
||||
void knownNamedFilters(QList<NamedFilter> *global, QList<NamedFilter> *user);
|
||||
std::optional<Dto::NamedFilterInfoDto> namedFilterInfoForKey(const QString &key, bool global);
|
||||
|
||||
bool handleCertificateIssue();
|
||||
|
||||
QIcon iconForIssue(const std::optional<Dto::IssueKind> &issueKind);
|
||||
@@ -117,3 +132,4 @@ Utils::FilePath findFileForIssuePath(const Utils::FilePath &issuePath);
|
||||
|
||||
} // Axivion::Internal
|
||||
|
||||
Q_DECLARE_METATYPE(Axivion::Internal::NamedFilter)
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "axiviontr.h"
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/fancylineedit.h>
|
||||
#include <utils/icon.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
@@ -211,6 +212,51 @@ const QMap<QString, QString> IssueHeaderView::currentFilterMapping() const
|
||||
return filter;
|
||||
}
|
||||
|
||||
void IssueHeaderView::updateExistingColumnInfos(
|
||||
const std::map<QString, QString> &filters,
|
||||
const std::optional<std::vector<Dto::SortInfoDto>> &sorters)
|
||||
{
|
||||
// update filters..
|
||||
for (int i = 0, end = m_columnInfoList.size(); i < end; ++i) {
|
||||
ColumnInfo &info = m_columnInfoList[i];
|
||||
const auto filterItem = filters.find(info.key);
|
||||
if (filterItem == filters.end())
|
||||
info.filter.reset();
|
||||
else
|
||||
info.filter.emplace(filterItem->second);
|
||||
|
||||
if (sorters) { // ..and sorters if needed
|
||||
bool found = false;
|
||||
for (const Dto::SortInfoDto &dto : *sorters) {
|
||||
if (dto.key != info.key)
|
||||
continue;
|
||||
info.sortOrder = dto.getDirectionEnum() == Dto::SortDirection::asc
|
||||
? Qt::AscendingOrder : Qt::DescendingOrder;
|
||||
found = true;
|
||||
}
|
||||
if (!found)
|
||||
info.sortOrder.reset();
|
||||
} else { // or clear them
|
||||
info.sortOrder.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// update sort order
|
||||
m_currentSortIndexes.clear();
|
||||
if (sorters) {
|
||||
for (const Dto::SortInfoDto &dto : *sorters) {
|
||||
int index = Utils::indexOf(m_columnInfoList, [key = dto.key](const ColumnInfo &ci) {
|
||||
return ci.key == key;
|
||||
});
|
||||
if (index == -1) // legit
|
||||
continue;
|
||||
m_currentSortIndexes << index;
|
||||
}
|
||||
}
|
||||
// inform UI
|
||||
emit filterChanged();
|
||||
}
|
||||
|
||||
void IssueHeaderView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
|
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "dashboard/dto.h"
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QList>
|
||||
|
||||
@@ -30,6 +32,8 @@ public:
|
||||
const QString currentSortString() const;
|
||||
const QMap<QString, QString> currentFilterMapping() const;
|
||||
|
||||
void updateExistingColumnInfos(const std::map<QString, QString> &filters,
|
||||
const std::optional<std::vector<Dto::SortInfoDto>> &sorters);
|
||||
signals:
|
||||
void filterChanged();
|
||||
void sortTriggered();
|
||||
|
Reference in New Issue
Block a user