1#include <IssueDetailedView.h>
2
3#include <IssueItem.h>
4#include <CircularPixmap.h>
5#include <GitServerCache.h>
6#include <IRestApi.h>
7#include <PrCommitsList.h>
8#include <PrChangesList.h>
9#include <PrCommentsList.h>
10#include <AddCodeReviewDialog.h>
11
12#include <QMessageBox>
13#include <QLocale>
14#include <QPushButton>
15#include <QToolButton>
16#include <QButtonGroup>
17#include <QStackedLayout>
18#include <QMenu>
19#include <QEvent>
20
21using namespace GitServer;
22
23IssueDetailedView::IssueDetailedView(const QSharedPointer<GitBase> &git,
24 const QSharedPointer<GitServerCache> &gitServerCache, QWidget *parent)
25 : QFrame(parent)
26 , mGit(git)
27 , mGitServerCache(gitServerCache)
28 , mBtnGroup(new QButtonGroup())
29 , mTitleLabel(new QLabel())
30 , mStackedLayout(new QStackedLayout())
31 , mPrCommentsList(new PrCommentsList(mGitServerCache))
32 , mPrChangesList(new PrChangesList(mGit))
33 , mPrCommitsList(new PrCommitsList(mGitServerCache))
34 , mReviewBtn(new QToolButton())
35{
36 mTitleLabel->setText(tr("Detailed View"));
37 mTitleLabel->setObjectName("HeaderTitle");
38
39 const auto comments = new QToolButton();
40 comments->setIcon(QIcon(":/icons/comments"));
41 comments->setObjectName("ViewBtnOption");
42 comments->setToolTip(tr("Comments view"));
43 comments->setCheckable(true);
44 comments->setChecked(true);
45 comments->setDisabled(true);
46 mBtnGroup->addButton(comments, static_cast<int>(Buttons::Comments));
47
48 const auto changes = new QToolButton();
49 changes->setIcon(QIcon(":/icons/changes"));
50 changes->setObjectName("ViewBtnOption");
51 changes->setToolTip(tr("Changes view"));
52 changes->setCheckable(true);
53 changes->setDisabled(true);
54 mBtnGroup->addButton(changes, static_cast<int>(Buttons::Changes));
55
56 const auto commits = new QToolButton();
57 commits->setIcon(QIcon(":/icons/commit"));
58 commits->setObjectName("ViewBtnOption");
59 commits->setToolTip(tr("Commits view"));
60 commits->setCheckable(true);
61 commits->setDisabled(true);
62 mBtnGroup->addButton(commits, static_cast<int>(Buttons::Commits));
63
64 const auto actionGroup = new QActionGroup(this);
65 const auto reviewMenu = new QMenu(mReviewBtn);
66 reviewMenu->installEventFilter(this);
67 mReviewBtn->setToolButtonStyle(Qt::ToolButtonIconOnly);
68 mReviewBtn->setPopupMode(QToolButton::InstantPopup);
69 mReviewBtn->setIcon(QIcon(":/icons/review_comment"));
70 mReviewBtn->setToolTip(tr("Start review"));
71 mReviewBtn->setObjectName("ViewBtnOption");
72 mReviewBtn->setDisabled(true);
73 mReviewBtn->setMenu(reviewMenu);
74
75 auto action = new QAction(tr("Only comments"), reviewMenu);
76 action->setToolTip(tr("Comment"));
77 action->setCheckable(true);
78 action->setChecked(true);
79 action->setData(static_cast<int>(ReviewMode::Comment));
80 actionGroup->addAction(action);
81 reviewMenu->addAction(action);
82
83 action = new QAction(tr("Review: Approve"), reviewMenu);
84 action->setCheckable(true);
85 action->setToolTip(tr("The comments will be part of a review"));
86 action->setData(static_cast<int>(ReviewMode::Approve));
87 actionGroup->addAction(action);
88 reviewMenu->addAction(action);
89
90 action = new QAction(tr("Review: Request changes"), reviewMenu);
91 action->setCheckable(true);
92 action->setToolTip(tr("The comments will be part of a review"));
93 action->setData(static_cast<int>(ReviewMode::RequestChanges));
94 actionGroup->addAction(action);
95 reviewMenu->addAction(action);
96 connect(actionGroup, &QActionGroup::triggered, this, &IssueDetailedView::openAddReviewDlg);
97
98 mAddComment = new QPushButton(this);
99 mAddComment->setCheckable(true);
100 mAddComment->setChecked(false);
101 mAddComment->setIcon(QIcon(":/icons/add_comment"));
102 mAddComment->setToolTip(tr("Add new comment"));
103 mAddComment->setDisabled(true);
104 mAddComment->setObjectName("ViewBtnOption");
105 connect(mAddComment, &QPushButton::clicked, mPrCommentsList, &PrCommentsList::addGlobalComment);
106
107 mCloseIssue = new QPushButton(this);
108 mCloseIssue->setCheckable(true);
109 mCloseIssue->setChecked(false);
110 mCloseIssue->setIcon(QIcon(":/icons/close_issue"));
111 mCloseIssue->setToolTip(tr("Close"));
112 mCloseIssue->setDisabled(true);
113 mCloseIssue->setObjectName("ViewBtnOption");
114 connect(mCloseIssue, &QPushButton::clicked, this, &IssueDetailedView::closeIssue);
115
116 const auto refresh = new QPushButton(this);
117 refresh->setIcon(QIcon(":/icons/refresh"));
118 refresh->setObjectName("ViewBtnOption");
119 refresh->setToolTip(tr("Refresh"));
120 connect(refresh, &QPushButton::clicked, this, [this]() { loadData(mConfig, mIssueNumber, true); });
121
122 const auto headerFrame = new QFrame(this);
123 headerFrame->setObjectName("IssuesHeaderFrameBig");
124 const auto headerLayout = new QHBoxLayout(headerFrame);
125 headerLayout->setContentsMargins(QMargins());
126 headerLayout->setSpacing(10);
127 headerLayout->addWidget(mTitleLabel);
128 headerLayout->addStretch();
129 headerLayout->addWidget(comments);
130 headerLayout->addWidget(changes);
131 headerLayout->addWidget(commits);
132 headerLayout->addSpacing(20);
133 headerLayout->addWidget(mReviewBtn);
134 headerLayout->addWidget(mAddComment);
135 headerLayout->addWidget(mCloseIssue);
136 headerLayout->addSpacing(20);
137 headerLayout->addWidget(refresh);
138
139 const auto footerFrame = new QFrame(this);
140 footerFrame->setObjectName("IssuesFooterFrame");
141
142 mPrCommitsList->setVisible(false);
143
144 mStackedLayout->insertWidget(static_cast<int>(Buttons::Comments), mPrCommentsList);
145 mStackedLayout->insertWidget(static_cast<int>(Buttons::Changes), mPrChangesList);
146 mStackedLayout->insertWidget(static_cast<int>(Buttons::Commits), mPrCommitsList);
147 mStackedLayout->setCurrentWidget(mPrCommentsList);
148
149 const auto issuesLayout = new QVBoxLayout(this);
150 issuesLayout->setContentsMargins(QMargins());
151 issuesLayout->setSpacing(0);
152 issuesLayout->addWidget(headerFrame);
153 issuesLayout->addLayout(mStackedLayout);
154 issuesLayout->addWidget(footerFrame);
155
156#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
157 connect(mBtnGroup, &QButtonGroup::idClicked, this, &IssueDetailedView::onViewChange);
158#else
159 connect(mBtnGroup, SIGNAL(buttonClicked(int)), this, SLOT(onViewChange(int)));
160#endif
161
162 connect(mPrCommentsList, &PrCommentsList::frameReviewLink, mPrChangesList, &PrChangesList::addLinks);
163 connect(mPrChangesList, &PrChangesList::gotoReview, this, [this](int frameId) {
164 mBtnGroup->button(static_cast<int>(Buttons::Comments))->setChecked(true);
165 mStackedLayout->setCurrentIndex(static_cast<int>(Buttons::Comments));
166 mPrCommentsList->highlightComment(frameId);
167 });
168 connect(mPrChangesList, &PrChangesList::addCodeReview, this, &IssueDetailedView::addCodeReview);
169 connect(mPrCommitsList, &PrCommitsList::openDiff, this, &IssueDetailedView::openDiff);
170}
171
172IssueDetailedView::~IssueDetailedView()
173{
174 delete mBtnGroup;
175}
176
177void IssueDetailedView::loadData(IssueDetailedView::Config config, int issueNum, bool force)
178{
179 if (mIssueNumber == issueNum && !force)
180 return;
181
182 mConfig = config;
183 mIssueNumber = issueNum;
184
185 mIssue = mConfig == Config::Issues ? mGitServerCache->getIssue(issueNum) : mGitServerCache->getPullRequest(issueNum);
186
187 mCloseIssue->setIcon(
188 QIcon(QString::fromUtf8(mConfig == Config::Issues ? ":/icons/close_issue" : ":/icons/close_pr")));
189
190 const auto title = mIssue.title.count() >= 40 ? mIssue.title.left(40).append("...") : mIssue.title;
191 mTitleLabel->setText(QString("#%1 - %2").arg(mIssue.number).arg(title));
192
193 mPrCommentsList->loadData(static_cast<PrCommentsList::Config>(mConfig), issueNum);
194
195 if (mConfig == Config::PullRequests)
196 {
197 mPrCommitsList->loadData(mIssue.number);
198
199 const auto pr = mGitServerCache->getPullRequest(mIssue.number);
200 mPrChangesList->loadData(pr);
201 }
202
203 mBtnGroup->button(static_cast<int>(Buttons::Commits))->setEnabled(mConfig == Config::PullRequests);
204 mBtnGroup->button(static_cast<int>(Buttons::Changes))->setEnabled(mConfig == Config::PullRequests);
205 mBtnGroup->button(static_cast<int>(Buttons::Comments))->setEnabled(true);
206 mReviewBtn->setEnabled(mConfig == Config::PullRequests);
207 mCloseIssue->setEnabled(true);
208 mAddComment->setEnabled(true);
209}
210
211bool IssueDetailedView::eventFilter(QObject *obj, QEvent *event)
212{
213 if (const auto menu = qobject_cast<QMenu *>(obj); menu && event->type() == QEvent::Show)
214 {
215 auto localPos = menu->parentWidget()->pos();
216 localPos.setX(localPos.x() - menu->width() + menu->parentWidget()->width());
217 auto pos = mapToGlobal(localPos);
218 menu->show();
219 pos.setY(pos.y() + menu->parentWidget()->height());
220 menu->move(pos);
221 return true;
222 }
223
224 return false;
225}
226
227void IssueDetailedView::onViewChange(int viewId)
228{
229 mAddComment->setEnabled(viewId == static_cast<int>(Buttons::Comments));
230 mStackedLayout->setCurrentIndex(viewId);
231}
232
233void IssueDetailedView::closeIssue()
234{
235 if (const auto ret = QMessageBox::question(this, tr("Close issue"), tr("Are you sure you want to close the issue?"));
236 ret == QMessageBox::Yes)
237 {
238 mIssue.isOpen = false;
239 mGitServerCache->getApi()->updateIssue(mIssue.number, mIssue);
240 }
241}
242
243void IssueDetailedView::openAddReviewDlg(QAction *sender)
244{
245 const auto mode = static_cast<ReviewMode>(sender->data().toInt());
246 QString modeStr;
247 switch (mode)
248 {
249 case ReviewMode::Comment:
250 mReviewBtn->setIcon(QIcon(":/icons/review_comment"));
251 mReviewBtn->setToolTip(tr("Comment review"));
252 modeStr = QString::fromUtf8("COMMENT");
253 break;
254 case ReviewMode::Approve:
255 mReviewBtn->setIcon(QIcon(":/icons/review_approve"));
256 mReviewBtn->setToolTip(tr("Approve review"));
257 modeStr = QString::fromUtf8("APPROVE");
258 break;
259 case ReviewMode::RequestChanges:
260 mReviewBtn->setIcon(QIcon(":/icons/review_change"));
261 mReviewBtn->setToolTip(tr("Request changes"));
262 modeStr = QString::fromUtf8("REQUEST_CHANGES");
263 break;
264 }
265
266 const auto dlg = new AddCodeReviewDialog(mode, this);
267 connect(dlg, &AddCodeReviewDialog::commentAdded, this,
268 [this, modeStr](const QString &text) { addReview(text, modeStr); });
269
270 dlg->exec();
271}
272
273void IssueDetailedView::addReview(const QString &body, const QString &mode)
274{
275 mGitServerCache->getApi()->addPrReview(mIssueNumber, body, mode);
276}
277
278void IssueDetailedView::addCodeReview(int line, const QString &path, const QString &body)
279{
280 const auto lastCommit = mGitServerCache->getPullRequest(mIssueNumber).commits.constLast();
281 mGitServerCache->getApi()->addPrCodeReview(mIssueNumber, body, path, line, lastCommit.sha);
282}
283