mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJ-CR-161866 Decouple action-based and notification-based feedback APIs
According to the code review request, introducing separate interfaces for configuring feedbacks submitted via in-IDE notifications and feedbacks submitted via explicit user actions. There is some code duplication now, as some stuff is common. If needed, we can extract it into a common base interface later. GitOrigin-RevId: ce271dd6cdb4b2630035d5e423ce4635eab49d51
This commit is contained in:
committed by
intellij-monorepo-bot
parent
07c333c46e
commit
4f83f58b6e
@@ -0,0 +1,119 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.feedback
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
|
||||
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
|
||||
import com.intellij.platform.feedback.impl.checkIsFeedbackCollectionDeadlineNotPast
|
||||
import com.intellij.platform.feedback.impl.checkIsIdeEAPIfRequired
|
||||
import com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Represents the config of a feedback action that can be explicitly invoked by the user.
|
||||
*
|
||||
* Similar to [InIdeFeedbackSurveyConfig], but instead of asking the user to submit feedback,
|
||||
* this config is intended to be used in cases when feedback is submitted by an explicit user
|
||||
* action (e.g., an action button, an action link, etc.).
|
||||
*/
|
||||
interface ExplicitUserFeedbackConfig {
|
||||
|
||||
/**
|
||||
* Unique identifier reflecting the survey.
|
||||
*
|
||||
* If the same feedback can be provided both by an in-IDE notification using [InIdeFeedbackSurveyConfig]
|
||||
* and by an explicit action, then this survey ID must be equal to that of the in-IDE survey config
|
||||
* to prevent showing survey notifications if the user has already sent feedback explicitly.
|
||||
*/
|
||||
val surveyId: String
|
||||
|
||||
/**
|
||||
* Date of the last day of feedback collection.
|
||||
*
|
||||
* Used to not letting the user submit feedback when it's no longer relevant.
|
||||
*
|
||||
* Often set to something like the planned date of the next release.
|
||||
*/
|
||||
val lastDayOfFeedbackCollection: LocalDate
|
||||
|
||||
/**
|
||||
* Whether the IDE must be of a EAP version.
|
||||
*/
|
||||
val requireIdeEAP: Boolean
|
||||
|
||||
/**
|
||||
* Checks whether the IDE is suitable for the feedback survey.
|
||||
*
|
||||
* Usually needed when you want to show a survey only to users of a particular IDE.
|
||||
*/
|
||||
fun checkIdeIsSuitable(): Boolean
|
||||
|
||||
/**
|
||||
* Checks whether the extra conditions for submitting feedback are satisfied.
|
||||
*
|
||||
* Normally it imposes fewer restrictions than [InIdeFeedbackSurveyConfig.checkExtraConditionSatisfied],
|
||||
* as, for example, there may be a check whether the user has been using a specific feature long enough
|
||||
* before showing a survey notification.
|
||||
* But when the user explicitly invoked an action to submit feedback, such checks are not needed.
|
||||
*
|
||||
* This check might include, for example, a check that the feature to submit feedback about is currently enabled.
|
||||
*/
|
||||
fun checkExtraConditionSatisfied(project: Project): Boolean = true
|
||||
|
||||
/**
|
||||
* Returns a dialog with the feedback survey.
|
||||
*
|
||||
* @see BlockBasedFeedbackDialog
|
||||
*/
|
||||
fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable>
|
||||
|
||||
/**
|
||||
* Performs additional custom state updates after the feedback dialog is closed by the ok button.
|
||||
*/
|
||||
fun updateStateAfterDialogClosedOk(project: Project)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether it's possible for the user to submit feedback now.
|
||||
*
|
||||
* Intended to be used in [com.intellij.openapi.actionSystem.AnAction.update].
|
||||
*
|
||||
* @see showFeedbackDialog
|
||||
*/
|
||||
@RequiresBackgroundThread
|
||||
fun ExplicitUserFeedbackConfig.isSuitableToShow(project: Project): Boolean {
|
||||
val commonConditionsForAllSurveys = if (Registry.`is`("platform.feedback.ignore.common.conditions.for.all.surveys", false)) {
|
||||
true
|
||||
}
|
||||
else {
|
||||
// Only a subset is checked as compared to com.intellij.platform.feedback.impl.FeedbackSurveyUtilsKt.isSuitableToShow.
|
||||
// This is because if the user wants to explicitly submit feedback, we shouldn't prevent it just because the feedback is already sent,
|
||||
// or because a feedback notification was shown several times.
|
||||
// But we still want feedback submission to be available only for the right IDE, right EAP/release mode
|
||||
// and don't want to allow outdated feedback.
|
||||
checkIdeIsSuitable() &&
|
||||
checkIsFeedbackCollectionDeadlineNotPast(lastDayOfFeedbackCollection) &&
|
||||
checkIsIdeEAPIfRequired(requireIdeEAP)
|
||||
}
|
||||
return commonConditionsForAllSurveys && checkExtraConditionSatisfied(project)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the feedback dialog for the user to submit feedback.
|
||||
*
|
||||
* Intended to be used in [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
|
||||
*
|
||||
* @see isSuitableToShow
|
||||
*/
|
||||
@RequiresEdt
|
||||
fun ExplicitUserFeedbackConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
|
||||
val dialog = createFeedbackDialog(project, forTest)
|
||||
val isOk = dialog.showAndGet()
|
||||
if (isOk && !forTest) {
|
||||
updateStateAfterDialogClosedOk(project)
|
||||
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(surveyId)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,8 @@
|
||||
package com.intellij.platform.feedback
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.feedback.impl.bundle.CommonFeedbackBundle
|
||||
import com.intellij.platform.feedback.impl.checkIsFeedbackCollectionDeadlineNotPast
|
||||
import com.intellij.platform.feedback.impl.checkIsIdeEAPIfRequired
|
||||
import com.intellij.platform.feedback.impl.notification.RequestFeedbackNotification
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
@@ -59,18 +55,6 @@ interface FeedbackSurveyConfig {
|
||||
*/
|
||||
fun checkExtraConditionSatisfied(project: Project): Boolean
|
||||
|
||||
/**
|
||||
* Checks whether the extra conditions for showing the survey by an explicit user action are satisfied.
|
||||
*
|
||||
* The same as [checkExtraConditionSatisfied] but for the case when the user intentionally submits
|
||||
* feedback through an explicit feature-specific action and not through a regular survey notification.
|
||||
*
|
||||
* Normally it imposes less restrictions, as, for example, [checkExtraConditionSatisfied] might check whether
|
||||
* the user has been using a specific feature long enough before showing a survey notification,
|
||||
* but when the user explicitly wants to provide feedback already, such conditions are usually irrelevant.
|
||||
*/
|
||||
fun checkExtraConditionSatisfiedForExplicitUserAction(project: Project): Boolean = true
|
||||
|
||||
/**
|
||||
* Returns a notification encouraging the user to leave feedback, which will be shown to the user.
|
||||
*/
|
||||
@@ -107,29 +91,3 @@ interface FeedbackSurveyConfig {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether it's possible for the user to explicitly submit feedback.
|
||||
*
|
||||
* Intended to be used by feature-specific feedback actions,
|
||||
* where this function is used for [com.intellij.openapi.actionSystem.AnAction.update]
|
||||
* and [com.intellij.platform.feedback.InIdeFeedbackSurveyConfig.showFeedbackDialog]
|
||||
* is used for [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
|
||||
*/
|
||||
@RequiresBackgroundThread
|
||||
fun FeedbackSurveyConfig.isSuitableToShowByExplicitUserAction(project: Project): Boolean {
|
||||
val commonConditionsForAllSurveys = if (Registry.`is`("platform.feedback.ignore.common.conditions.for.all.surveys", false)) {
|
||||
true
|
||||
}
|
||||
else {
|
||||
// Only a subset is checked as compared to com.intellij.platform.feedback.impl.FeedbackSurveyUtilsKt.isSuitableToShow.
|
||||
// This is because if the user wants to explicitly submit feedback, we shouldn't prevent it just because the feedback is already sent,
|
||||
// or because a feedback notification was shown several times.
|
||||
// But we still want feedback submission to be available only for the right IDE, right EAP/release mode
|
||||
// and don't want to allow outdated feedback.
|
||||
checkIdeIsSuitable() &&
|
||||
checkIsFeedbackCollectionDeadlineNotPast() &&
|
||||
checkIsIdeEAPIfRequired()
|
||||
}
|
||||
return commonConditionsForAllSurveys && checkExtraConditionSatisfiedForExplicitUserAction(project)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ package com.intellij.platform.feedback
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
|
||||
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
|
||||
import com.intellij.platform.feedback.impl.updateCommonFeedbackSurveysStateAfterSent
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
|
||||
/**
|
||||
* Represents the config for In-IDE feedback survey.
|
||||
@@ -26,22 +24,3 @@ interface InIdeFeedbackSurveyConfig : FeedbackSurveyConfig {
|
||||
*/
|
||||
fun updateStateAfterDialogClosedOk(project: Project)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the feedback dialog for the user to submit feedback.
|
||||
*
|
||||
* Normally the dialog is shown when the user clicks a survey notification.
|
||||
* This function is intended for feature-specific feedback actions,
|
||||
* where [com.intellij.platform.feedback.FeedbackSurveyConfig.isSuitableToShowByExplicitUserAction]
|
||||
* is used for [com.intellij.openapi.actionSystem.AnAction.update]
|
||||
* and this function is used for [com.intellij.openapi.actionSystem.AnAction.actionPerformed].
|
||||
*/
|
||||
@RequiresEdt
|
||||
fun InIdeFeedbackSurveyConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
|
||||
val dialog = createFeedbackDialog(project, forTest)
|
||||
val isOk = dialog.showAndGet()
|
||||
if (isOk && !forTest) {
|
||||
updateStateAfterDialogClosedOk(project)
|
||||
updateCommonFeedbackSurveysStateAfterSent(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,25 @@ import com.intellij.platform.feedback.impl.state.CommonFeedbackSurveyService
|
||||
import com.intellij.platform.feedback.impl.state.DontShowAgainFeedbackService
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.todayIn
|
||||
|
||||
internal const val MAX_FEEDBACK_SURVEY_NUMBER_SHOWS: Int = 2
|
||||
|
||||
internal fun FeedbackSurveyConfig.checkIsFeedbackCollectionDeadlineNotPast(): Boolean {
|
||||
private fun FeedbackSurveyConfig.checkIsFeedbackCollectionDeadlineNotPast(): Boolean {
|
||||
return checkIsFeedbackCollectionDeadlineNotPast(this.lastDayOfFeedbackCollection)
|
||||
}
|
||||
|
||||
internal fun checkIsFeedbackCollectionDeadlineNotPast(lastDayOfFeedbackCollection: LocalDate): Boolean {
|
||||
return Clock.System.todayIn(TimeZone.currentSystemDefault()) < lastDayOfFeedbackCollection
|
||||
}
|
||||
|
||||
internal fun FeedbackSurveyConfig.checkIsIdeEAPIfRequired(): Boolean {
|
||||
private fun FeedbackSurveyConfig.checkIsIdeEAPIfRequired(): Boolean {
|
||||
return checkIsIdeEAPIfRequired(this.requireIdeEAP)
|
||||
}
|
||||
|
||||
internal fun checkIsIdeEAPIfRequired(requireIdeEAP: Boolean): Boolean {
|
||||
if (requireIdeEAP) {
|
||||
return ApplicationInfo.getInstance().isEAP
|
||||
}
|
||||
@@ -90,7 +99,16 @@ private fun invokeRespondNotificationAction(feedbackSurveyType: FeedbackSurveyTy
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateCommonFeedbackSurveysStateAfterSent(feedbackSurveyConfig: FeedbackSurveyConfig) {
|
||||
private fun InIdeFeedbackSurveyConfig.showFeedbackDialog(project: Project, forTest: Boolean) {
|
||||
val dialog = createFeedbackDialog(project, forTest)
|
||||
val isOk = dialog.showAndGet()
|
||||
if (isOk && !forTest) {
|
||||
updateStateAfterDialogClosedOk(project)
|
||||
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(surveyId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCommonFeedbackSurveysStateAfterSent(feedbackSurveyConfig: FeedbackSurveyConfig) {
|
||||
CommonFeedbackSurveyService.feedbackSurveyAnswerSent(feedbackSurveyConfig.surveyId)
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -4,20 +4,20 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.remoting.ActionRemoteBehaviorSpecification
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.platform.feedback.isSuitableToShowByExplicitUserAction
|
||||
import com.intellij.platform.feedback.isSuitableToShow
|
||||
import com.intellij.platform.feedback.showFeedbackDialog
|
||||
import org.jetbrains.plugins.terminal.block.feedback.ReworkedTerminalSurveyConfig
|
||||
import org.jetbrains.plugins.terminal.block.feedback.ReworkedTerminalFeedbackActionConfig
|
||||
|
||||
internal class TerminalFeedbackAction : DumbAwareAction(), ActionRemoteBehaviorSpecification.Frontend {
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val project = e.project
|
||||
e.presentation.isEnabledAndVisible = project != null && ReworkedTerminalSurveyConfig.isSuitableToShowByExplicitUserAction(project)
|
||||
e.presentation.isEnabledAndVisible = project != null && ReworkedTerminalFeedbackActionConfig.isSuitableToShow(project)
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
ReworkedTerminalSurveyConfig.showFeedbackDialog(project, forTest = false)
|
||||
ReworkedTerminalFeedbackActionConfig.showFeedbackDialog(project, forTest = false)
|
||||
}
|
||||
}
|
||||
+54
-20
@@ -5,10 +5,7 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.platform.feedback.FeedbackSurvey
|
||||
import com.intellij.platform.feedback.FeedbackSurveyType
|
||||
import com.intellij.platform.feedback.InIdeFeedbackSurveyConfig
|
||||
import com.intellij.platform.feedback.InIdeFeedbackSurveyType
|
||||
import com.intellij.platform.feedback.*
|
||||
import com.intellij.platform.feedback.dialog.BlockBasedFeedbackDialog
|
||||
import com.intellij.platform.feedback.dialog.SystemDataJsonSerializable
|
||||
import com.intellij.platform.feedback.impl.OnDemandFeedbackResolver
|
||||
@@ -54,24 +51,25 @@ internal fun getFeedbackMoment(project: Project): TerminalFeedbackMoment {
|
||||
}
|
||||
|
||||
internal class ReworkedTerminalFeedbackSurvey : FeedbackSurvey() {
|
||||
override val feedbackSurveyType: FeedbackSurveyType<*> = InIdeFeedbackSurveyType(ReworkedTerminalSurveyConfig)
|
||||
override val feedbackSurveyType: FeedbackSurveyType<*> = InIdeFeedbackSurveyType(ReworkedTerminalFeedbackNotificationConfig)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
override val surveyId: String = "reworked_terminal"
|
||||
object ReworkedTerminalFeedbackNotificationConfig : InIdeFeedbackSurveyConfig {
|
||||
override val surveyId: String
|
||||
get() = ReworkedTerminalSurveyConfig.SURVEY_ID
|
||||
|
||||
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
|
||||
return ReworkedTerminalFeedbackDialog(project, forTest)
|
||||
}
|
||||
override val lastDayOfFeedbackCollection: LocalDate
|
||||
get() = ReworkedTerminalSurveyConfig.lastDayOfFeedbackCollection
|
||||
|
||||
override fun updateStateAfterDialogClosedOk(project: Project) { }
|
||||
override val requireIdeEAP: Boolean
|
||||
get() = ReworkedTerminalSurveyConfig.REQUIRE_EAP
|
||||
|
||||
override val lastDayOfFeedbackCollection: LocalDate = LocalDate(2025, 7, 15)
|
||||
override fun checkIdeIsSuitable(): Boolean =
|
||||
ReworkedTerminalSurveyConfig.checkIdeIsSuitable()
|
||||
|
||||
override val requireIdeEAP: Boolean = false
|
||||
|
||||
override fun checkIdeIsSuitable(): Boolean = PlatformUtils.isJetBrainsProduct()
|
||||
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> =
|
||||
ReworkedTerminalSurveyConfig.createFeedbackDialog(project, forTest)
|
||||
|
||||
override fun checkExtraConditionSatisfied(project: Project): Boolean {
|
||||
val usageStorage = TerminalUsageLocalStorage.getInstance()
|
||||
@@ -83,11 +81,6 @@ object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
)
|
||||
}
|
||||
|
||||
override fun checkExtraConditionSatisfiedForExplicitUserAction(project: Project): Boolean {
|
||||
// Explicitly sending feedback is only enabled when the reworked terminal is enabled.
|
||||
return TerminalOptionsProvider.instance.terminalEngine == TerminalEngine.REWORKED
|
||||
}
|
||||
|
||||
override fun createNotification(project: Project, forTest: Boolean): RequestFeedbackNotification {
|
||||
return RequestFeedbackNotification("Feedback In IDE",
|
||||
TerminalBundle.message("feedback.notification.title"),
|
||||
@@ -97,4 +90,45 @@ object ReworkedTerminalSurveyConfig : InIdeFeedbackSurveyConfig {
|
||||
override fun updateStateAfterNotificationShowed(project: Project) {
|
||||
TerminalUsageLocalStorage.getInstance().recordFeedbackNotificationShown()
|
||||
}
|
||||
|
||||
override fun updateStateAfterDialogClosedOk(project: Project) { }
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
object ReworkedTerminalFeedbackActionConfig : ExplicitUserFeedbackConfig {
|
||||
override val surveyId: String
|
||||
get() = ReworkedTerminalSurveyConfig.SURVEY_ID
|
||||
|
||||
override val lastDayOfFeedbackCollection: LocalDate
|
||||
get() = ReworkedTerminalSurveyConfig.lastDayOfFeedbackCollection
|
||||
|
||||
override val requireIdeEAP: Boolean
|
||||
get() = ReworkedTerminalSurveyConfig.REQUIRE_EAP
|
||||
|
||||
override fun checkIdeIsSuitable(): Boolean =
|
||||
ReworkedTerminalSurveyConfig.checkIdeIsSuitable()
|
||||
|
||||
override fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> =
|
||||
ReworkedTerminalSurveyConfig.createFeedbackDialog(project, forTest)
|
||||
|
||||
override fun updateStateAfterDialogClosedOk(project: Project) { }
|
||||
|
||||
override fun checkExtraConditionSatisfied(project: Project): Boolean {
|
||||
// Explicitly sending feedback is only enabled when the reworked terminal is enabled.
|
||||
return TerminalOptionsProvider.instance.terminalEngine == TerminalEngine.REWORKED
|
||||
}
|
||||
}
|
||||
|
||||
private object ReworkedTerminalSurveyConfig {
|
||||
const val SURVEY_ID: String = "reworked_terminal"
|
||||
|
||||
val lastDayOfFeedbackCollection: LocalDate = LocalDate(2025, 7, 15)
|
||||
|
||||
const val REQUIRE_EAP: Boolean = false
|
||||
|
||||
fun checkIdeIsSuitable(): Boolean = PlatformUtils.isJetBrainsProduct()
|
||||
|
||||
fun createFeedbackDialog(project: Project, forTest: Boolean): BlockBasedFeedbackDialog<out SystemDataJsonSerializable> {
|
||||
return ReworkedTerminalFeedbackDialog(project, forTest)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user