[platform] refactoring: configuration of the in-product ZenDesk feedback form moved to ExternalProductResourceUrls (IJPL-204)

IntelliJ IDEA Community and Ultimate are migrated to use the new approach. Since other JetBrains IDEs didn't use this feature, and the corresponding method wasn't part of public API, no fallback implementation in LegacyExternalProductResourceUrls is provided.

GitOrigin-RevId: a50912b65d4fc44a3fcf7e59d615092c0372b581
This commit is contained in:
Nikolay Chashnikov
2023-08-21 10:37:01 +02:00
committed by intellij-monorepo-bot
parent 8149d16d1b
commit 48f0103a52
10 changed files with 124 additions and 95 deletions

View File

@@ -18,18 +18,6 @@
<help webhelp-url="https://www.jetbrains.com/help/idea/"/>
<documentation url="https://www.jetbrains.com/idea/resources/"/>
<feedback zendesk-form-id="360001912739" zendesk-url="https://jbsintellij.zendesk.com">
<field id="28147552" value="ij_idea"/>
<field id="28102551" value="sl_unknown"/> <!-- country -->
<field id="29444529" type="rating"/>
<field id="28500325" type="build"/>
<field id="28151042" type="os"/>
<field id="28500645" type="timezone"/>
<field id="28351649" type="eval"/>
<field id="360021010939" type="systeminfo"/>
<field id="22996310" type="needsupport"/>
<field id="28116681" type="topic"/>
</feedback>
<whatsnew url="https://www.jetbrains.com/idea/whatsnew/" show-on-update="true"/>
<keymap win="https://www.jetbrains.com/idea/docs/IntelliJIDEA_ReferenceCard.pdf"
mac="https://www.jetbrains.com/idea/docs/IntelliJIDEA_ReferenceCard_Mac.pdf"/>

View File

@@ -2,6 +2,8 @@
package com.intellij.idea.customization.base
import com.intellij.platform.ide.impl.customization.BaseJetBrainsExternalProductResourceUrls
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormData
import com.intellij.platform.ide.impl.customization.ZenDeskFeedbackFormFieldIds
class IntelliJIdeaExternalResourceUrls : BaseJetBrainsExternalProductResourceUrls() {
override val basePatchDownloadUrl: String
@@ -12,4 +14,23 @@ 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
}
}
}

View File

@@ -88,7 +88,6 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
private String mySubscriptionFormId;
private boolean mySubscriptionTipsAvailable;
private XmlElement myFeedbackForm;
private String myDefaultLightLaf;
private String myDefaultClassicLightLaf;
@@ -201,9 +200,6 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
case "feedback": {
myFeedbackUrl = child.getAttributeValue("url");
if (child.getAttributeValue("zendesk-form-id") != null) {
myFeedbackForm = child;
}
}
break;
@@ -748,11 +744,6 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
return override != null ? override : myDefaultClassicDarkLaf;
}
public @Nullable ZenDeskForm getFeedbackForm() {
XmlElement v = myFeedbackForm;
return v == null ? null : ZenDeskForm.parse(v);
}
private static final class UpdateUrlsImpl implements UpdateUrls {
private final String myCheckingUrl;
private final String myPatchesUrl;

View File

@@ -1,32 +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.openapi.application.impl
import com.intellij.util.xml.dom.XmlElement
data class ZenDeskField(val id: Long, val type: String?, val value: String?) {
companion object {
fun parse(element: XmlElement): ZenDeskField {
val id = element.getAttributeValue("id")!!.toLong()
val type = element.getAttributeValue("type")
val value = element.getAttributeValue("value")
return ZenDeskField(id, type, value)
}
}
}
class ZenDeskForm(val id: Long, val url: String, val fields: List<ZenDeskField>) {
companion object {
@JvmStatic fun parse(element: XmlElement): ZenDeskForm {
val id = element.getAttributeValue("zendesk-form-id")!!.toLong()
val url = element.getAttributeValue("zendesk-url")!!
val fields = mutableListOf<ZenDeskField>()
for (child in element.children) {
if (child.name == "field") {
fields.add(ZenDeskField.parse(child))
}
}
return ZenDeskForm(id, url, fields)
}
}
}

View File

@@ -2,7 +2,9 @@
package com.intellij.platform.ide.customization
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.util.Url
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.ApiStatus
/**
@@ -88,4 +90,15 @@ interface FeedbackReporter {
* extension point.
*/
fun feedbackFormUrl(description: String): Url
/**
* Override this function to show a custom form when "Submit Feedback" action is invoked or when the IDE requests a user to provide
* feedback during the evaluation period.
* @param requestedForEvaluation `true` if the form is shown by the IDE during the evaluation period and `false` if user explicitly
* invoked "Submit Feedback" action.
* @return `true` if the custom form was shown, and `false` otherwise;
* in the latter case, the default way with opening [feedbackFormUrl] in the browser will be used.
*/
@RequiresEdt
fun showFeedbackForm(project: Project?, requestedForEvaluation: Boolean): Boolean = false
}

View File

@@ -4,15 +4,11 @@ package com.intellij.ide.actions;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.FeedbackDescriptionProvider;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.feedback.FeedbackForm;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.application.impl.ZenDeskForm;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
@@ -20,7 +16,6 @@ import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.platform.ide.customization.ExternalProductResourceUrls;
import com.intellij.platform.ide.customization.FeedbackReporter;
import com.intellij.ui.LicensingFacade;
@@ -55,12 +50,12 @@ public class SendFeedbackAction extends AnAction implements DumbAware {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
ZenDeskForm feedbackForm = ((ApplicationInfoImpl)ApplicationInfo.getInstance()).getFeedbackForm();
if (Registry.is("ide.in.product.feedback") && feedbackForm != null) {
new FeedbackForm(e.getProject(), feedbackForm, false).show();
}
else {
submit(e.getProject());
FeedbackReporter feedbackReporter = ExternalProductResourceUrls.getInstance().getFeedbackReporter();
if (feedbackReporter != null) {
boolean formShown = feedbackReporter.showFeedbackForm(e.getProject(), false);
if (!formShown) {
submit(e.getProject());
}
}
}

View File

@@ -12,10 +12,9 @@ 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.application.ex.ApplicationInfoEx
import com.intellij.openapi.application.impl.ZenDeskForm
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
@@ -24,8 +23,7 @@ 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.dsl.builder.panel
import com.intellij.ui.layout.*
import com.intellij.ui.layout.selectedValueMatches
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import org.jetbrains.annotations.Nls
@@ -54,7 +52,7 @@ private val topicOptions = listOf(
class FeedbackForm(
private val project: Project?,
val form: ZenDeskForm,
private val zenDeskFormData: ZenDeskFeedbackFormData,
val isEvaluation: Boolean
) : DialogWrapper(project, false) {
private var details = ""
@@ -230,15 +228,12 @@ class FeedbackForm(
val systemInfo = if (shareSystemInformation) AboutDialog(project).extendedAboutText else ""
ApplicationManager.getApplication().executeOnPooledThread {
ZenDeskRequests().submit(
form,
zenDeskFormData,
email,
ApplicationNamesInfo.getInstance().fullProductName + " Feedback",
details.ifEmpty { "No details" },
mapOf(
"systeminfo" to systemInfo,
"needsupport" to needSupport
) + (ratingComponent?.let { mapOf("rating" to it.myRating) } ?: mapOf()) + (topic?.let { mapOf("topic" to it.id) } ?: emptyMap())
, onDone = {
CustomFieldValues(systemInfo, needSupport, ratingComponent?.myRating, topic?.id),
onDone = {
ApplicationManager.getApplication().invokeLater {
var message = ApplicationBundle.message("feedback.form.thanks", ApplicationNamesInfo.getInstance().fullProductName)
if (isEvaluation) {

View File

@@ -3,9 +3,9 @@ package com.intellij.ide.feedback
import com.fasterxml.jackson.databind.ObjectMapper
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.impl.ZenDeskForm
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
@@ -14,34 +14,40 @@ import java.net.HttpURLConnection
class ZenDeskRequests {
private val objectMapper by lazy { ObjectMapper() }
fun submit(form: ZenDeskForm, email: String, subject: String, text: String, fieldData: Map<String, Any>, onDone: () -> Unit, onError: () -> Unit) {
val customFields = mutableListOf<ZenDeskCustomField>()
for (field in form.fields) {
val value = field.value
?: when (field.type) {
"build" -> ApplicationInfo.getInstance().build.asString()
"os" -> getOsForZenDesk()
"timezone" -> System.getProperty("user.timezone")
"eval" -> LicensingFacade.getInstance()?.isEvaluationLicense?.toString()
"needsupport" -> if (fieldData[field.type] == true) "share_problems_or_ask_about_missing_features" else "provide_a_feedback"
else -> fieldData[field.type]
}
value?.let {
customFields.add(ZenDeskCustomField(field.id, it))
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),
form.id,
zenDeskFormData.formId,
customFields
)
val requestData = objectMapper.writeValueAsString(mapOf("request" to request))
try {
HttpRequests
.post("${form.url}/api/v2/requests", "application/json")
.post("${zenDeskFormData.formUrl}/api/v2/requests", "application/json")
.productNameAsUserAgent()
.accept("application/json")
.connect {
@@ -81,6 +87,13 @@ class ZenDeskRequests {
}
}
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)

View File

@@ -31,6 +31,12 @@ 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
override val updatesMetadataXmlUrl: String
get() = "https://www.jetbrains.com/updates/updates.xml"
@@ -56,7 +62,7 @@ abstract class BaseJetBrainsExternalProductResourceUrls : ExternalProductResourc
}
override val feedbackReporter: FeedbackReporter?
get() = JetBrainsFeedbackReporter(shortProductNameUsedInForms)
get() = JetBrainsFeedbackReporter(shortProductNameUsedInForms, zenDeskFeedbackFormData)
}
/**

View File

@@ -1,13 +1,17 @@
// 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.ex.ApplicationInfoEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.ide.customization.FeedbackReporter
import com.intellij.ui.LicensingFacade
import com.intellij.util.Url
import com.intellij.util.Urls
internal class JetBrainsFeedbackReporter(private val productName: String) : FeedbackReporter {
internal class JetBrainsFeedbackReporter(private val productName: String,
private val zenDeskFormData: ZenDeskFeedbackFormData?) : FeedbackReporter {
override val destinationDescription: String
get() = "jetbrains.com"
@@ -23,4 +27,39 @@ internal class JetBrainsFeedbackReporter(private val productName: String) : Feed
"eval" to (LicensingFacade.getInstance()?.isEvaluationLicense == true).toString(),
))
}
override fun showFeedbackForm(project: Project?, requestedForEvaluation: Boolean): Boolean {
if (Registry.`is`("ide.in.product.feedback") && zenDeskFormData != null) {
FeedbackForm(project, zenDeskFormData, requestedForEvaluation).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
}