[Feedback] IDEA-331610 Replace old general feedback form

IJ-CR-115375

GitOrigin-RevId: c79e50d87ba1a5b5ae716378f69f8c354298744d
This commit is contained in:
Dmitry Pogrebnoy
2023-09-25 18:08:35 +02:00
committed by intellij-monorepo-bot
parent 6424b95a93
commit e29620624e
10 changed files with 17 additions and 460 deletions

View File

@@ -3,8 +3,6 @@ package com.intellij.idea.customization.base
import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.ide.impl.customization.BaseJetBrainsExternalProductResourceUrls
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormData
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormFieldIds
import com.intellij.util.Url
import com.intellij.util.Urls
@@ -21,26 +19,7 @@ class IntelliJIdeaExternalResourceUrls : BaseJetBrainsExternalProductResourceUrl
override val shortProductNameUsedInForms: String
get() = "IDEA"
override val zenDeskFeedbackFormData: ZenDeskFeedbackFormData
get() = object : ZenDeskFeedbackFormData {
override val formUrl: String = "https://jbsintellij.zendesk.com"
override val formId: Long = 360001912739
override val productId: String = "ij_idea"
override val fieldIds = object : ZenDeskFeedbackFormFieldIds {
override val product: Long = 28147552
override val country: Long = 28102551
override val rating: Long = 29444529
override val build: Long = 28500325
override val os: Long = 28151042
override val timezone: Long = 28500645
override val eval: Long = 28351649
override val systemInfo: Long = 360021010939
override val needSupport: Long = 22996310
override val topic: Long = 28116681
}
}
override val useNewEvaluationFeedbackForm: Boolean
override val useInIdeFeedback: Boolean
get() = true
override val youTubeChannelUrl: Url

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.feedback
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.dialog.components
import com.intellij.icons.AllIcons
import org.jetbrains.annotations.Nls
@@ -8,7 +8,6 @@ import java.awt.Graphics
import java.awt.event.*
import javax.swing.*
class RatingComponent : JComponent() {
private val myIconSize = 32
private val myIconGap = 4

View File

@@ -1,8 +1,8 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.dialog.uiBlocks
import com.intellij.ide.feedback.RatingComponent
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.feedback.dialog.components.RatingComponent
import com.intellij.platform.feedback.dialog.createBoldJBLabel
import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
import com.intellij.ui.dsl.builder.*

View File

@@ -1,7 +1,7 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.dialog.uiBlocks
import com.intellij.ide.feedback.RatingComponent
import com.intellij.platform.feedback.dialog.components.RatingComponent
import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
import com.intellij.ui.components.JBLabel
import com.intellij.ui.dsl.builder.BottomGap

View File

@@ -1,7 +1,6 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.feedback.localization.dialog
import com.intellij.ide.feedback.RatingComponent
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
@@ -10,6 +9,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ex.MultiLineLabel
import com.intellij.platform.feedback.dialog.*
import com.intellij.platform.feedback.dialog.components.RatingComponent
import com.intellij.platform.feedback.impl.FEEDBACK_REPORT_ID_KEY
import com.intellij.platform.feedback.impl.FeedbackRequestData
import com.intellij.platform.feedback.impl.FeedbackRequestType

View File

@@ -1,271 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.feedback
import com.intellij.CommonBundle
import com.intellij.icons.AllIcons
import com.intellij.ide.actions.AboutDialog
import com.intellij.ide.actions.ReportProblemAction
import com.intellij.ide.actions.SendFeedbackAction
import com.intellij.notification.Notification
import com.intellij.notification.NotificationListener
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationBundle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormData
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.LicensingFacade
import com.intellij.ui.PopupBorder
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.TextComponentEmptyText
import com.intellij.ui.components.dialog
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.layout.selectedValueMatches
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import org.jetbrains.annotations.Nls
import java.awt.Component
import java.awt.event.ActionEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.util.function.Predicate
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.JComboBox
import javax.swing.JComponent
import javax.swing.event.HyperlinkEvent
private data class ZenDeskComboOption(val displayName: @Nls String, val id: String) {
override fun toString(): String = displayName
}
private val topicOptions = listOf(
ZenDeskComboOption(ApplicationBundle.message("feedback.form.topic.bug"), "ij_bug"),
ZenDeskComboOption(ApplicationBundle.message("feedback.form.topic.howto"), "ij_howto"),
ZenDeskComboOption(ApplicationBundle.message("feedback.form.topic.problem"), "ij_problem"),
ZenDeskComboOption(ApplicationBundle.message("feedback.form.topic.suggestion"), "ij_suggestion"),
ZenDeskComboOption(ApplicationBundle.message("feedback.form.topic.misc"), "ij_misc")
)
class FeedbackForm(
private val project: Project?,
private val zenDeskFormData: ZenDeskFeedbackFormData,
val isEvaluation: Boolean
) : DialogWrapper(project, false) {
private var details = ""
private var email = LicensingFacade.INSTANCE?.getLicenseeEmail().orEmpty()
private var needSupport = false
private var shareSystemInformation = false
private var ratingComponent: RatingComponent? = null
private var missingRatingTooltip: JComponent? = null
private var topic: ZenDeskComboOption? = null
private lateinit var topicComboBox: JComboBox<ZenDeskComboOption?>
init {
title = if (isEvaluation) ApplicationBundle.message("feedback.form.title") else ApplicationBundle.message("feedback.form.prompt")
init()
}
override fun createCenterPanel(): JComponent {
return panel {
if (isEvaluation) {
row {
label(ApplicationBundle.message("feedback.form.evaluation.prompt")).applyToComponent {
font = JBFont.h1()
}
}
row {
label(ApplicationBundle.message("feedback.form.comment"))
}
row {
label(ApplicationBundle.message("feedback.form.rating", ApplicationNamesInfo.getInstance().fullProductName))
}
row {
ratingComponent = RatingComponent().also {
it.addPropertyChangeListener { evt ->
if (evt.propertyName == RatingComponent.RATING_PROPERTY) {
missingRatingTooltip?.isVisible = false
}
}
cell(it)
}
missingRatingTooltip = label(ApplicationBundle.message("feedback.form.rating.required")).applyToComponent {
border = JBUI.Borders.compound(PopupBorder.Factory.createColored(JBUI.CurrentTheme.Validator.errorBorderColor()),
JBUI.Borders.empty(4, 8))
background = JBUI.CurrentTheme.Validator.errorBackgroundColor()
isVisible = false
isOpaque = true
}.component
}
}
else {
row {
topicComboBox = comboBox(CollectionComboBoxModel(topicOptions))
.label(ApplicationBundle.message("feedback.form.topic"), LabelPosition.TOP)
.bindItem({ topic }, { topic = it})
.errorOnApply(ApplicationBundle.message("feedback.form.topic.required")) {
it.selectedItem == null
}.component
icon(AllIcons.General.BalloonInformation)
.gap(RightGap.SMALL)
.visibleIf(topicComboBox.selectedValueMatches { it?.id == "ij_bug" })
text(ApplicationBundle.message("feedback.form.issue")) {
ReportProblemAction.Handler.submit(project)
}.visibleIf(topicComboBox.selectedValueMatches { it?.id == "ij_bug" })
}
}
row {
val label = if (isEvaluation) ApplicationBundle.message("feedback.form.evaluation.details") else ApplicationBundle.message("feedback.form.details")
textArea()
.label(label, LabelPosition.TOP)
.bindText(::details)
.align(Align.FILL)
.rows(5)
.focused()
.errorOnApply(ApplicationBundle.message("feedback.form.details.required")) {
it.text.isBlank()
}
.applyToComponent {
emptyText.text = if (isEvaluation)
ApplicationBundle.message("feedback.form.evaluation.details.emptyText")
else
ApplicationBundle.message("feedback.form.details.emptyText")
putClientProperty(TextComponentEmptyText.STATUS_VISIBLE_FUNCTION, Predicate<JBTextArea> { it.text.isEmpty() })
addKeyListener(object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.keyCode == KeyEvent.VK_TAB) {
if ((e.modifiersEx and KeyEvent.SHIFT_DOWN_MASK) != 0) {
transferFocusBackward()
}
else {
transferFocus()
}
e.consume()
}
}
})
}
}.resizableRow()
row {
textField()
.label(ApplicationBundle.message("feedback.form.email"), LabelPosition.TOP)
.bindText(::email)
.columns(COLUMNS_MEDIUM)
.errorOnApply(ApplicationBundle.message("feedback.form.email.required")) { it.text.isBlank() }
.errorOnApply(ApplicationBundle.message("feedback.form.email.invalid")) { !it.text.matches(Regex(".+@.+\\..+")) }
}
row {
checkBox(ApplicationBundle.message("feedback.form.need.support"))
.bindSelected(::needSupport)
}
row {
checkBox(ApplicationBundle.message("feedback.form.share.system.information"))
.bindSelected(::shareSystemInformation)
.gap(RightGap.SMALL)
@Suppress("DialogTitleCapitalization")
link(ApplicationBundle.message("feedback.form.share.system.information.link")) {
showSystemInformation()
}
}
row {
comment(ApplicationBundle.message("feedback.form.consent"))
}
}
}
private fun showSystemInformation() {
val systemInfo = AboutDialog(project).extendedAboutText
val scrollPane = JBScrollPane(JBTextArea(systemInfo))
dialog(ApplicationBundle.message("feedback.form.system.information.title"), scrollPane, createActions = {
listOf(object : AbstractAction(CommonBundle.getCloseButtonText()) {
init {
putValue(DEFAULT_ACTION, true)
}
override fun actionPerformed(event: ActionEvent) {
val wrapper = findInstance(event.source as? Component)
wrapper?.close(OK_EXIT_CODE)
}
})
}).show()
}
override fun getOKAction(): Action {
return object : DialogWrapper.OkAction() {
init {
putValue(Action.NAME, ApplicationBundle.message("feedback.form.ok"))
}
override fun doAction(e: ActionEvent) {
val ratingComponent = ratingComponent
missingRatingTooltip?.isVisible = ratingComponent?.myRating == 0
if (ratingComponent == null || ratingComponent.myRating != 0) {
super.doAction(e)
}
else {
enabled = false
}
}
}
}
override fun getCancelAction(): Action {
return super.getCancelAction().apply {
if (isEvaluation) {
putValue(Action.NAME, ApplicationBundle.message("feedback.form.cancel"))
}
}
}
override fun doOKAction() {
super.doOKAction()
val systemInfo = if (shareSystemInformation) AboutDialog(project).extendedAboutText else ""
ApplicationManager.getApplication().executeOnPooledThread {
ZenDeskRequests().submit(
zenDeskFormData,
email,
ApplicationNamesInfo.getInstance().fullProductName + " Feedback",
details.ifEmpty { "No details" },
CustomFieldValues(systemInfo, needSupport, ratingComponent?.myRating, topic?.id),
onDone = {
ApplicationManager.getApplication().invokeLater {
var message = ApplicationBundle.message("feedback.form.thanks", ApplicationNamesInfo.getInstance().fullProductName)
if (isEvaluation) {
message += "<br/>" + ApplicationBundle.message("feedback.form.share.later")
}
Notification("feedback.form",
ApplicationBundle.message("feedback.form.thanks.title"),
message,
NotificationType.INFORMATION).notify(project)
}
}, onError = {
ApplicationManager.getApplication().invokeLater {
Notification("feedback.form",
ApplicationBundle.message("feedback.form.error.title"),
ApplicationBundle.message("feedback.form.error.text"),
NotificationType.ERROR
)
.setListener(object : NotificationListener.Adapter() {
override fun hyperlinkActivated(notification: Notification, e: HyperlinkEvent) {
SendFeedbackAction.submit(project)
}
})
.notify(project)
}
})
}
}
override fun doCancelAction() {
super.doCancelAction()
if (isEvaluation) {
Notification("feedback.form", ApplicationBundle.message("feedback.form.share.later"), NotificationType.INFORMATION).notify(project)
}
}
}

View File

@@ -1,109 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ide.feedback
import com.fasterxml.jackson.databind.ObjectMapper
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormData
import com.intellij.ui.LicensingFacade
import com.intellij.util.io.HttpRequests
import java.io.IOException
import java.net.HttpURLConnection
class ZenDeskRequests {
private val objectMapper by lazy { ObjectMapper() }
internal fun submit(zenDeskFormData: ZenDeskFeedbackFormData, email: String, subject: String, text: String,
customFieldValues: CustomFieldValues, onDone: () -> Unit, onError: () -> Unit) {
val fieldIds = zenDeskFormData.fieldIds
val customFields = listOfNotNull(
ZenDeskCustomField(fieldIds.product, zenDeskFormData.productId),
ZenDeskCustomField(fieldIds.country, "sl_unknown"),
customFieldValues.rating?.let { rating ->
ZenDeskCustomField(fieldIds.rating, rating)
},
ZenDeskCustomField(fieldIds.build, ApplicationInfo.getInstance().build.asString()),
ZenDeskCustomField(fieldIds.os, getOsForZenDesk()),
ZenDeskCustomField(fieldIds.timezone, System.getProperty("user.timezone")),
LicensingFacade.getInstance()?.isEvaluationLicense?.let { isEval ->
ZenDeskCustomField(fieldIds.eval, isEval.toString())
},
ZenDeskCustomField(fieldIds.systemInfo, customFieldValues.systemInfo),
ZenDeskCustomField(fieldIds.needSupport,
if (customFieldValues.needSupport) "share_problems_or_ask_about_missing_features" else "provide_a_feedback"),
customFieldValues.topicId?.let { topicId ->
ZenDeskCustomField(fieldIds.topic, topicId)
}
)
val request = ZenDeskRequest(
ZenDeskRequester("anonymous", email),
subject,
ZenDeskComment(text),
zenDeskFormData.formId,
customFields
)
val requestData = objectMapper.writeValueAsString(mapOf("request" to request))
try {
HttpRequests
.post("${zenDeskFormData.formUrl}/api/v2/requests", "application/json")
.productNameAsUserAgent()
.accept("application/json")
.connect {
try {
it.write(requestData)
val bytes = it.inputStream.readAllBytes()
LOG.info(bytes.toString(Charsets.UTF_8))
}
catch (e: IOException) {
val errorResponse = (it.connection as HttpURLConnection).errorStream?.readAllBytes()?.toString(Charsets.UTF_8)
LOG.info("Failed to submit feedback. Feedback data:\n$requestData\n" +
"Server response:\n$errorResponse\n" +
"Exception:\n${e.stackTraceToString()}")
onError()
return@connect
}
onDone()
}
} catch (e: IOException) {
LOG.info("Failed to submit feedback. Feedback data:\n$requestData\nError message:\n${e.message}")
onError()
return
}
}
private fun getOsForZenDesk(): String {
return when {
SystemInfo.isWindows -> "ij_win"
SystemInfo.isMac -> "ij_mac"
SystemInfo.isLinux -> "ij_linux"
else -> "ij_other-os"
}
}
companion object {
private val LOG = Logger.getInstance(ZenDeskRequests::class.java)
}
}
internal data class CustomFieldValues(
val systemInfo: String,
val needSupport: Boolean,
val rating: Int?,
val topicId: String?
)
private class ZenDeskComment(val body: String)
private class ZenDeskRequester(val name: String, val email: String)
private class ZenDeskCustomField(val id: Long, val value: Any)
private class ZenDeskRequest(
val requester: ZenDeskRequester,
val subject: String,
val comment: ZenDeskComment,
val ticket_form_id: Long,
val custom_fields: List<ZenDeskCustomField>
)

View File

@@ -50,16 +50,11 @@ abstract class BaseJetBrainsExternalProductResourceUrls : ExternalProductResourc
open val intellijSupportFormId: Int
get() = 66731
/**
* Return a non-null value from this property to enable the in-product form for "Submit Feedback" action and evaluation feedback
*/
open val zenDeskFeedbackFormData: ZenDeskFeedbackFormData?
get() = null
/**
* Whether to use the new Evaluation Feedback Form to collect evaluation feedback from users.
* Use in-product forms for Help | Submit Feedback... and evaluation feedback
*/
open val useNewEvaluationFeedbackForm: Boolean
open val useInIdeFeedback: Boolean
get() = false
override val updateMetadataUrl: Url
@@ -99,7 +94,7 @@ abstract class BaseJetBrainsExternalProductResourceUrls : ExternalProductResourc
override val feedbackReporter: FeedbackReporter?
get() = shortProductNameUsedInForms?.let { productName ->
JetBrainsFeedbackReporter(productName, useNewEvaluationFeedbackForm, zenDeskFeedbackFormData)
JetBrainsFeedbackReporter(productName, useInIdeFeedback)
}
override val downloadPageUrl: Url?

View File

@@ -1,11 +1,10 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ide.impl.customization
import com.intellij.ide.feedback.FeedbackForm
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.feedback.evaluation.dialog.EvaluationFeedbackDialog
import com.intellij.platform.feedback.general.dialog.GeneralFeedbackDialog
import com.intellij.platform.ide.customization.FeedbackReporter
import com.intellij.ui.LicensingFacade
import com.intellij.util.Url
@@ -14,8 +13,7 @@ import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class JetBrainsFeedbackReporter(private val productName: String,
private val useNewEvaluationFeedbackForm: Boolean,
private val zenDeskFormData: ZenDeskFeedbackFormData?) : FeedbackReporter {
private val useInIdeFeedback: Boolean) : FeedbackReporter {
override val destinationDescription: String
get() = "jetbrains.com"
@@ -33,47 +31,16 @@ class JetBrainsFeedbackReporter(private val productName: String,
}
override fun showFeedbackForm(project: Project?, requestedForEvaluation: Boolean): Boolean {
if (!Registry.`is`("ide.in.product.feedback")) {
return false
}
if (useInIdeFeedback) {
if (requestedForEvaluation) {
EvaluationFeedbackDialog(project, false).show()
return true
}
if (requestedForEvaluation && useNewEvaluationFeedbackForm) {
EvaluationFeedbackDialog(project, false).show()
return true
}
if (zenDeskFormData != null) {
FeedbackForm(project, zenDeskFormData, requestedForEvaluation).show()
GeneralFeedbackDialog(project, false).show()
return true
}
return false
}
}
/**
* Provides information about ZenDesk form used to send feedback.
*/
interface ZenDeskFeedbackFormData {
val formUrl: String
val formId: Long
val productId: String
val fieldIds: ZenDeskFeedbackFormFieldIds
}
/**
* IDs of elements in ZenDesk feedback form.
* They are used in JSON sent to zendesk.com, see [ZenDeskRequests][com.intellij.ide.feedback.ZenDeskRequests] for implementation details.
*/
interface ZenDeskFeedbackFormFieldIds {
val product: Long
val country: Long
val rating: Long
val build: Long
val os: Long
val timezone: Long
val eval: Long
val systemInfo: Long
val needSupport: Long
val topic: Long
}

View File

@@ -2275,9 +2275,6 @@ run.index.rescanning.on.plugin.load.unload.description=Run force index rescannin
general.project.type=true
general.project.type.description=Add support for general project type
ide.in.product.feedback=true
ide.in.product.feedback.description=Use in-product forms for Help | Submit Feedback... and evaluation feedback
new.inlay.settings=true
new.inlay.settings.description=Enable new inlay settings UI