feat(GODT-2816): Wait until mandatory fields are filled then fill body and title.

This commit is contained in:
Romain LE JEUNE
2023-07-28 16:35:52 +02:00
committed by Romain Le Jeune
parent 3d64c5f894
commit 80d729e3e5
15 changed files with 885 additions and 766 deletions

View File

@ -33,7 +33,7 @@ const (
DefaultMaxSessionCountForBugReport = 10 DefaultMaxSessionCountForBugReport = 10
) )
func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, description, username, email, client string, attachLogs bool) error { func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, title, description, username, email, client string, attachLogs bool) error {
var account string var account string
if info, err := bridge.QueryUserInfo(username); err == nil { if info, err := bridge.QueryUserInfo(username); err == nil {
@ -82,7 +82,7 @@ func (bridge *Bridge) ReportBug(ctx context.Context, osType, osVersion, descript
OS: osType, OS: osType,
OSVersion: osVersion, OSVersion: osVersion,
Title: "[Bridge] Bug", Title: "[Bridge] Bug - " + title,
Description: description, Description: description,
Client: client, Client: client,

View File

@ -221,6 +221,15 @@ bool QMLBackend::areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const {
} }
//****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category.
//****************************************************************************************************************************************************
QString QMLBackend::getBugCategory(quint8 categoryId) const {
return reportFlow_.getCategory(categoryId);
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] categoryId The id of the bug category. /// \param[in] categoryId The id of the bug category.
/// \return Set of question for this category. /// \return Set of question for this category.
@ -919,14 +928,15 @@ void QMLBackend::triggerReset() const {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] category The category of the bug.
/// \param[in] description The description of the bug. /// \param[in] description The description of the bug.
/// \param[in] address The email address. /// \param[in] address The email address.
/// \param[in] emailClient The email client. /// \param[in] emailClient The email client.
/// \param[in] includeLogs Should the logs be included in the report. /// \param[in] includeLogs Should the logs be included in the report.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
void QMLBackend::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const { void QMLBackend::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const {
HANDLE_EXCEPTION( HANDLE_EXCEPTION(
app().grpc().reportBug(description, address, emailClient, includeLogs); app().grpc().reportBug(category, description, address, emailClient, includeLogs);
) )
} }

View File

@ -58,6 +58,7 @@ public: // member functions.
Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available. Q_INVOKABLE bool isPortFree(int port) const; ///< Check if a given network port is available.
Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL. Q_INVOKABLE QString nativePath(QUrl const &url) const; ///< Retrieve the native path of a local URL.
Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file. Q_INVOKABLE bool areSameFileOrFolder(QUrl const &lhs, QUrl const &rhs) const; ///< Check if two local URL point to the same file.
Q_INVOKABLE QString getBugCategory(quint8 categoryId) const; ///< Get a Category name.
Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category. Q_INVOKABLE QVariantList getQuestionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question. Q_INVOKABLE void setQuestionAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question. Q_INVOKABLE QString getQuestionAnswer(quint8 questionId) const; ///< Get the answer for a given question.
@ -193,7 +194,7 @@ public slots: // slot for signals received from QML -> To be forwarded to Bridge
void checkUpdates() const; ///< Slot for the update check. void checkUpdates() const; ///< Slot for the update check.
void installUpdate() const; ///< Slot for the update install. void installUpdate() const; ///< Slot for the update install.
void triggerReset() const; ///< Slot for the triggering of reset. void triggerReset() const; ///< Slot for the triggering of reset.
void reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report. void reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) const; ///< Slot for the bug report.
void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates. void exportTLSCertificates() const; ///< Slot for the export of the TLS certificates.
void onResetFinished(); ///< Slot for the reset finish signal. void onResetFinished(); ///< Slot for the reset finish signal.
void onVersionChanged(); ///< Slot for the version change signal. void onVersionChanged(); ///< Slot for the version change signal.

View File

@ -21,7 +21,7 @@ SettingsView {
property var questions:Backend.bugQuestions property var questions:Backend.bugQuestions
property var categoryId:0 property var categoryId:0
property var questionSet:ListModel{} property var questionSet:ListModel{}
property bool error: questionRepeater.error
signal questionAnswered signal questionAnswered
function setCategoryId(catId) { function setCategoryId(catId) {
@ -44,8 +44,38 @@ SettingsView {
type: Label.Heading type: Label.Heading
} }
TextEdit {
Layout.fillWidth: true
color: root.colorScheme.text_weak
font.family: ProtonStyle.font_family
font.letterSpacing: ProtonStyle.caption_letter_spacing
font.pixelSize: ProtonStyle.caption_font_size
font.weight: ProtonStyle.fontWeight_400
readOnly: true
selectByMouse: true
selectedTextColor: root.colorScheme.text_invert
// No way to set lineHeight: ProtonStyle.caption_line_height
selectionColor: root.colorScheme.interaction_norm
text: qsTr("* Mandatory questions")
wrapMode: Text.WordWrap
}
Repeater { Repeater {
id: questionRepeater
model: root.questionSet model: root.questionSet
property bool error :{
for (var i = 0; i < questionRepeater.count; i++) {
if (questionRepeater.itemAt(i).error)
return true;
}
return false;
}
function validate(){
for (var i = 0; i < questionRepeater.count; i++) {
questionRepeater.itemAt(i).validate()
}
}
QuestionItem { QuestionItem {
Layout.fillWidth: true Layout.fillWidth: true
@ -62,7 +92,7 @@ SettingsView {
mandatory: root.questions[modelData].mandatory ? root.questions[modelData].mandatory : false mandatory: root.questions[modelData].mandatory ? root.questions[modelData].mandatory : false
answerList: root.questions[modelData].answerList ? root.questions[modelData].answerList : [] answerList: root.questions[modelData].answerList ? root.questions[modelData].answerList : []
onAnswerChanged:{ onAnswerChanged: {
Backend.setQuestionAnswer(modelData, answer); Backend.setQuestionAnswer(modelData, answer);
} }
@ -70,7 +100,7 @@ SettingsView {
function onVisibleChanged() { function onVisibleChanged() {
setDefaultValue(Backend.getQuestionAnswer(modelData)) setDefaultValue(Backend.getQuestionAnswer(modelData))
} }
target:root target: root
} }
} }
} }
@ -81,11 +111,13 @@ SettingsView {
Button { Button {
id: continueButton id: continueButton
colorScheme: root.colorScheme colorScheme: root.colorScheme
enabled: !loading enabled: !loading && !root.error
text: qsTr("Continue") text: qsTr("Continue")
onClicked: { onClicked: {
submit(); questionRepeater.validate()
if (!root.error)
submit();
} }
} }
} }

View File

@ -20,6 +20,7 @@ SettingsView {
property var selectedAddress property var selectedAddress
property var categoryId:-1 property var categoryId:-1
property string category: Backend.getBugCategory(root.categoryId)
signal bugReportWasSent signal bugReportWasSent
@ -41,7 +42,7 @@ SettingsView {
function submit() { function submit() {
sendButton.loading = true; sendButton.loading = true;
Backend.reportBug(description.text, address.text, emailClient.text, includeLogs.checked); Backend.reportBug(root.category, description.text, address.text, emailClient.text, includeLogs.checked);
} }
fillHeight: true fillHeight: true
@ -69,7 +70,7 @@ SettingsView {
// want TextArea implicitHeight (which is height of all text) // want TextArea implicitHeight (which is height of all text)
// to be considered in SettingsView internal scroll view // to be considered in SettingsView internal scroll view
implicitHeight: height implicitHeight: height
label: qsTr("Your answers") label: "Your answers to: " + qsTr(root.category);
readOnly : true readOnly : true
} }
TextField { TextField {

View File

@ -236,7 +236,15 @@ FocusScope {
KeyNavigation.tab: root.KeyNavigation.tab KeyNavigation.tab: root.KeyNavigation.tab
KeyNavigation.up: root.KeyNavigation.up KeyNavigation.up: root.KeyNavigation.up
bottomPadding: 8 bottomPadding: 8
color: control.enabled ? root.colorScheme.text_norm : root.colorScheme.text_disabled color: {
if (!control.enabled) {
return root.colorScheme.text_disabled
}
if (control.readOnly) {
return root.colorScheme.text_hint
}
return root.colorScheme.text_norm
}
// enforcing default focus here within component // enforcing default focus here within component
focus: root.focus focus: root.focus
@ -258,7 +266,7 @@ FocusScope {
background: Rectangle { background: Rectangle {
anchors.fill: parent anchors.fill: parent
border.color: { border.color: {
if (!control.enabled) { if (!control.enabled || control.readOnly) {
return root.colorScheme.field_disabled; return root.colorScheme.field_disabled;
} }
if (control.activeFocus) { if (control.activeFocus) {

View File

@ -45,6 +45,15 @@ Item {
} }
return "" return ""
} }
property bool error: {
if (root.type === QuestionItem.InputType.TextInput)
return textInput.error;
if (root.type === QuestionItem.InputType.Radio)
return selectionRadio.error;
if (root.type === QuestionItem.InputType.Checkbox)
return selectionCheckBox.error;
return false
}
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
textInput.setDefaultValue(defaultValue) textInput.setDefaultValue(defaultValue)
@ -52,6 +61,12 @@ Item {
selectionCheckBox.setDefaultValue(defaultValue) selectionCheckBox.setDefaultValue(defaultValue)
} }
function validate() {
textInput.validate()
selectionRadio.validate()
selectionCheckBox.validate()
}
implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin implicitHeight: children[0].implicitHeight + children[0].anchors.topMargin + children[0].anchors.bottomMargin
ColumnLayout { ColumnLayout {
@ -61,7 +76,7 @@ Item {
Label { Label {
id: mainLabel id: mainLabel
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: qsTr(root.text) text: root.mandatory ? qsTr(root.text+" *") : qsTr(root.text)
type: Label.Body type: Label.Body
} }
ColumnLayout { ColumnLayout {
@ -98,7 +113,7 @@ Item {
} }
onTextChanged: { onTextChanged: {
// Rise max length error immediately while typing if mandatory field // Rise max length error immediately while typing if mandatory field
if (mandatory && textInput.text.length > textInput._maxLength) { if (textInput.text.length > textInput._maxLength) {
validate(); validate();
} }
} }
@ -108,9 +123,11 @@ Item {
ButtonGroup { ButtonGroup {
id: selectionRadio id: selectionRadio
property string text: { property string text: {
return checkedButton ? checkedButton.text : ""; return checkedButton ? checkedButton.text : "";
} }
property bool error: root.mandatory
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
const values = root.type === QuestionItem.InputType.Radio ? defaultValue : []; const values = root.type === QuestionItem.InputType.Radio ? defaultValue : [];
@ -118,6 +135,17 @@ Item {
buttons[i].checked = values.includes(buttons[i].text); buttons[i].checked = values.includes(buttons[i].text);
} }
} }
function validate() {
if (mandatory && selectionRadio.text.length === 0) {
error = true;
return
}
error = false;
}
onTextChanged: {
validate();
}
} }
Repeater { Repeater {
model: root.answerList model: root.answerList
@ -142,6 +170,7 @@ Item {
} }
return str.slice(0, -delimitor.length); return str.slice(0, -delimitor.length);
} }
property bool error: root.mandatory
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
const values = root.type === QuestionItem.InputType.Checkbox ? defaultValue.split(delimitor) : []; const values = root.type === QuestionItem.InputType.Checkbox ? defaultValue.split(delimitor) : [];
@ -149,6 +178,18 @@ Item {
buttons[i].checked = values.includes(buttons[i].text); buttons[i].checked = values.includes(buttons[i].text);
} }
} }
function validate() {
if (mandatory && selectionCheckBox.text.length === 0) {
error = true;
return
}
error = false;
}
onTextChanged: {
validate();
}
} }
Repeater { Repeater {
model: root.answerList model: root.answerList

View File

@ -137,13 +137,13 @@ TEST_F(BugReportFlowFixture, validFile) {
EXPECT_TRUE(flow_.setAnswer(0, "pwet")); EXPECT_TRUE(flow_.setAnswer(0, "pwet"));
EXPECT_FALSE(flow_.setAnswer(1, "pwet")); EXPECT_FALSE(flow_.setAnswer(1, "pwet"));
qDebug() << flow_.collectAnswers(0);
EXPECT_EQ(flow_.collectAnswers(0), "Category: I can't receive mail\n\r - What happened?\n\rpwet\n\r"); EXPECT_EQ(flow_.collectAnswers(0), " - What happened?\n\rpwet\n\r");
EXPECT_EQ(flow_.collectAnswers(1), ""); EXPECT_EQ(flow_.collectAnswers(1), "");
EXPECT_EQ(flow_.getAnswer(0), "pwet"); EXPECT_EQ(flow_.getAnswer(0), "pwet");
EXPECT_EQ(flow_.getAnswer(1), ""); EXPECT_EQ(flow_.getAnswer(1), "");
flow_.clearAnswers(); flow_.clearAnswers();
EXPECT_EQ(flow_.collectAnswers(0), "Category: I can't receive mail\n\r"); EXPECT_EQ(flow_.collectAnswers(0), "");
} }

View File

@ -53,17 +53,17 @@ bool BugReportFlow::parse(const QString& filepath) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'bugCategories' property. /// \return The value for the 'bugCategories' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QStringList BugReportFlow::categories() const { QStringList BugReportFlow::categories() const {
return categories_; return categories_;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \return The value for the 'bugQuestions' property. /// \return The value for the 'bugQuestions' property.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
QVariantList BugReportFlow::questions() const { QVariantList BugReportFlow::questions() const {
return questions_; return questions_;
} }
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
@ -91,6 +91,19 @@ bool BugReportFlow::setAnswer(quint8 questionId, QString const &answer) {
} }
//****************************************************************************************************************************************************
/// \param[in] questionId The id of the question.
/// \return answer the given question.
//****************************************************************************************************************************************************
QString BugReportFlow::getCategory(quint8 categoryId) const {
QString category;
if (categoryId <= categories_.count() - 1) {
category = categories_[categoryId];
}
return category;
}
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] questionId The id of the question. /// \param[in] questionId The id of the question.
/// \return answer the given question. /// \return answer the given question.
@ -113,7 +126,6 @@ QString BugReportFlow::collectAnswers(quint8 categoryId) const {
if (categoryId > categories_.count() - 1) if (categoryId > categories_.count() - 1)
return answers; return answers;
answers += "Category: " + categories_[categoryId] + "\n\r";
QVariantList sets = this->questionSet(categoryId); QVariantList sets = this->questionSet(categoryId);
for (QVariant const &var: sets) { for (QVariant const &var: sets) {
const QString& answer = getAnswer(var.toInt()); const QString& answer = getAnswer(var.toInt());

View File

@ -38,9 +38,9 @@ public: // member functions.
[[nodiscard]] QStringList categories() const; ///< Getter for the 'bugCategories' property. [[nodiscard]] QStringList categories() const; ///< Getter for the 'bugCategories' property.
[[nodiscard]] QVariantList questions() const; ///< Getter for the 'bugQuestions' property. [[nodiscard]] QVariantList questions() const; ///< Getter for the 'bugQuestions' property.
[[nodiscard]] QVariantList questionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category. [[nodiscard]] QVariantList questionSet(quint8 categoryId) const; ///< Retrieve the set of question for a given bug category.
[[nodiscard]] bool setAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question. [[nodiscard]] bool setAnswer(quint8 questionId, QString const &answer); ///< Feed an answer for a given question.
[[nodiscard]] QString getAnswer(quint8 questionId) const; ///< Collect answer for a given questions. [[nodiscard]] QString getCategory(quint8 categoryId) const; ///< Get category name.
[[nodiscard]] QString getAnswer(quint8 questionId) const; ///< Get answer for a given question.
[[nodiscard]] QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions. [[nodiscard]] QString collectAnswers(quint8 categoryId) const; ///< Collect answer for a given set of questions.
void clearAnswers(); ///< Clear all collected answers. void clearAnswers(); ///< Clear all collected answers.

View File

@ -353,16 +353,18 @@ grpc::Status GRPCClient::currentEmailClient(QString &outName) {
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
/// \param[in] category The category of the bug.
/// \param[in] description The description of the bug. /// \param[in] description The description of the bug.
/// \param[in] address The email address. /// \param[in] address The email address.
/// \param[in] emailClient The email client. /// \param[in] emailClient The email client.
/// \param[in] includeLogs Should the report include the logs. /// \param[in] includeLogs Should the report include the logs.
/// \return The status for the gRPC call. /// \return The status for the gRPC call.
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
grpc::Status GRPCClient::reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs) { grpc::Status GRPCClient::reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs) {
ReportBugRequest request; ReportBugRequest request;
request.set_ostype(QSysInfo::productType().toStdString()); request.set_ostype(QSysInfo::productType().toStdString());
request.set_osversion(QSysInfo::prettyProductName().toStdString()); request.set_osversion(QSysInfo::prettyProductName().toStdString());
request.set_title(category.toStdString());
request.set_description(description.toStdString()); request.set_description(description.toStdString());
request.set_address(address.toStdString()); request.set_address(address.toStdString());
request.set_emailclient(emailClient.toStdString()); request.set_emailclient(emailClient.toStdString());

View File

@ -77,7 +77,7 @@ public: // member functions.
grpc::Status colorSchemeName(QString &outName); ///< Performs the "colorSchemeName' gRPC call. grpc::Status colorSchemeName(QString &outName); ///< Performs the "colorSchemeName' gRPC call.
grpc::Status setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' gRPC call. grpc::Status setColorSchemeName(QString const &name); ///< Performs the "setColorSchemeName' gRPC call.
grpc::Status currentEmailClient(QString &outName); ///< Performs the 'currentEmailClient' gRPC call. grpc::Status currentEmailClient(QString &outName); ///< Performs the 'currentEmailClient' gRPC call.
grpc::Status reportBug(QString const &description, QString const &address, QString const &emailClient, bool includeLogs); ///< Performs the 'ReportBug' gRPC call. grpc::Status reportBug(QString const &category, QString const &description, QString const &address, QString const &emailClient, bool includeLogs); ///< Performs the 'ReportBug' gRPC call.
grpc::Status exportTLSCertificates(QString const &folderPath); ///< Performs the 'ExportTLSCertificates' gRPC call. grpc::Status exportTLSCertificates(QString const &folderPath); ///< Performs the 'ExportTLSCertificates' gRPC call.
grpc::Status quit(); ///< Perform the "Quit" gRPC call. grpc::Status quit(); ///< Perform the "Quit" gRPC call.
grpc::Status restart(); ///< Performs the Restart gRPC call. grpc::Status restart(); ///< Performs the Restart gRPC call.

File diff suppressed because it is too large Load Diff

View File

@ -147,10 +147,11 @@ message GuiReadyResponse {
message ReportBugRequest { message ReportBugRequest {
string osType = 1; string osType = 1;
string osVersion = 2; string osVersion = 2;
string description = 3; string title = 3;
string address = 4; string description = 4;
string emailClient = 5; string address = 5;
bool includeLogs = 6; string emailClient = 6;
bool includeLogs = 7;
} }

View File

@ -330,6 +330,7 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
s.log.WithFields(logrus.Fields{ s.log.WithFields(logrus.Fields{
"osType": report.OsType, "osType": report.OsType,
"osVersion": report.OsVersion, "osVersion": report.OsVersion,
"title": report.Title,
"description": report.Description, "description": report.Description,
"address": report.Address, "address": report.Address,
"emailClient": report.EmailClient, "emailClient": report.EmailClient,
@ -345,6 +346,7 @@ func (s *Service) ReportBug(_ context.Context, report *ReportBugRequest) (*empty
context.Background(), context.Background(),
report.OsType, report.OsType,
report.OsVersion, report.OsVersion,
report.Title,
report.Description, report.Description,
report.Address, report.Address,
report.Address, report.Address,