From 2f1f11283ca2d4af95b409c13f51cfa792225ece Mon Sep 17 00:00:00 2001 From: Vivek Ranjan Date: Mon, 3 Jul 2023 09:52:21 -0400 Subject: [PATCH] MAK-57 IJ: Add button to start flow mode for task (#101) * MAK-57 IJ: Add button to start flow mode for task * Upgrading intellij platform --- .run/Run Plugin.run.xml | 2 +- ...tion.run.xml => Run Verifications.run.xml} | 27 +-- build.gradle.kts | 22 ++- gradle.properties | 4 +- .../co/makerflow/client/apis/FlowModeApi.kt | 159 +++++++-------- .../client/infrastructure/ApiClient.kt | 29 ++- .../models/FetchOngoingFlowMode200Response.kt | 14 +- .../FetchOngoingFlowMode200ResponseAnyOf.kt | 40 ++++ .../client/models/StartFlowMode200Response.kt | 7 +- .../models/StopOngoingFlowMode200Response.kt | 7 +- .../actions/ToggleFlowModeAction.kt | 6 +- ...ener.kt => FlowModePostStartupActivity.kt} | 15 +- .../intellijplugin/panels/TasksPanel.kt | 182 +++++++++++++++--- .../services/FlowModeService.kt | 48 +++-- .../intellijplugin/services/TodoUtil.kt | 32 +++ .../co/makerflow/intellijplugin/state/Flow.kt | 3 + .../status/FlowStatusBarWidgetFactory.kt | 8 +- src/main/resources/META-INF/plugin.xml | 7 +- 18 files changed, 432 insertions(+), 180 deletions(-) rename .run/{Run Plugin Verification.run.xml => Run Verifications.run.xml} (61%) create mode 100644 src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200ResponseAnyOf.kt rename src/main/kotlin/co/makerflow/intellijplugin/listeners/{FlowModeProjectManagerListener.kt => FlowModePostStartupActivity.kt} (75%) create mode 100644 src/main/kotlin/co/makerflow/intellijplugin/services/TodoUtil.kt diff --git a/.run/Run Plugin.run.xml b/.run/Run Plugin.run.xml index 7069c04..39d6243 100644 --- a/.run/Run Plugin.run.xml +++ b/.run/Run Plugin.run.xml @@ -35,4 +35,4 @@ false - \ No newline at end of file + diff --git a/.run/Run Plugin Verification.run.xml b/.run/Run Verifications.run.xml similarity index 61% rename from .run/Run Plugin Verification.run.xml rename to .run/Run Verifications.run.xml index 46f21e7..79c92fb 100644 --- a/.run/Run Plugin Verification.run.xml +++ b/.run/Run Verifications.run.xml @@ -18,19 +18,20 @@ true true - - + + + + false - - + false + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f7b1693..132f8d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,13 +12,13 @@ plugins { kotlin("jvm") version "1.8.20" kotlin("plugin.serialization") version "1.8.20" // Gradle IntelliJ Plugin - id("org.jetbrains.intellij") version "1.14.1" + id("org.jetbrains.intellij") version "1.14.2" // Gradle Changelog Plugin id("org.jetbrains.changelog") version "2.1.0" // Gradle Qodana Plugin id("org.jetbrains.qodana") version "0.1.13" // Dokka - id("org.jetbrains.dokka") version "1.8.10" + id("org.jetbrains.dokka") version "1.8.20" // IDEA support id("idea") } @@ -31,7 +31,7 @@ repositories { mavenCentral() } -val ktorVersion = "2.1.3" +val ktorVersion = "2.3.2" dependencies { implementation("com.github.kittinunf.fuel:fuel:2.3.1") implementation("com.github.kittinunf.fuel:fuel-jackson:2.3.1") @@ -42,13 +42,17 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.3") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3") - implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-client-jackson:$ktorVersion") - implementation("io.ktor:ktor-client-java:$ktorVersion") - implementation("io.ktor:ktor-serialization-jackson:$ktorVersion") + val excludeFromKtor: (ExternalModuleDependency).() -> Unit = { + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-jdk8") + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation("io.ktor:ktor-client-core:$ktorVersion", excludeFromKtor) + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion", excludeFromKtor) + implementation("io.ktor:ktor-client-jackson:$ktorVersion", excludeFromKtor) + implementation("io.ktor:ktor-client-cio:$ktorVersion", excludeFromKtor) + implementation("io.ktor:ktor-serialization-jackson:$ktorVersion", excludeFromKtor) implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") } // Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. diff --git a/gradle.properties b/gradle.properties index 66a8cae..dab0ece 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,12 +8,12 @@ pluginVersion = 0.0.1 # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html # for insight into build numbers and IntelliJ Platform versions. -pluginSinceBuild = 222 +pluginSinceBuild = 231 pluginUntilBuild = 232.* # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties platformType = IC -platformVersion = 2022.2.5 +platformVersion = 2023.1.1 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 diff --git a/src/main/kotlin/co/makerflow/client/apis/FlowModeApi.kt b/src/main/kotlin/co/makerflow/client/apis/FlowModeApi.kt index 753ab4e..a1858e3 100644 --- a/src/main/kotlin/co/makerflow/client/apis/FlowModeApi.kt +++ b/src/main/kotlin/co/makerflow/client/apis/FlowModeApi.kt @@ -26,123 +26,130 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.http.ParametersBuilder import com.fasterxml.jackson.databind.ObjectMapper - open class FlowModeApi( +open class FlowModeApi( baseUrl: String = ApiClient.BASE_URL, httpClientEngine: HttpClientEngine? = null, httpClientConfig: ((HttpClientConfig<*>) -> Unit)? = null, jsonBlock: ObjectMapper.() -> Unit = ApiClient.JSON_DEFAULT, - ) : ApiClient(baseUrl, httpClientEngine, httpClientConfig, jsonBlock) { +) : ApiClient(baseUrl, httpClientEngine, httpClientConfig, jsonBlock) { - /** - * - * - * @param source To specify source of request (optional) - * @return FetchOngoingFlowMode200Response - */ - @Suppress("UNCHECKED_CAST") - open suspend fun fetchOngoingFlowMode(source: kotlin.String?): HttpResponse { + /** + * + * + * @param source To specify source of request (optional) + * @return FetchOngoingFlowMode200Response + */ + @Suppress("UNCHECKED_CAST") + open suspend fun fetchOngoingFlowMode(source: kotlin.String?): HttpResponse { - val localVariableAuthNames = listOf("api_token") + val localVariableAuthNames = listOf("api_token") - val localVariableBody = - io.ktor.client.utils.EmptyContent + val localVariableBody = + io.ktor.client.utils.EmptyContent - val localVariableQuery = mutableMapOf>() - source?.apply { localVariableQuery["source"] = listOf("$source") } + val localVariableQuery = mutableMapOf>() + source?.apply { localVariableQuery["source"] = listOf("$source") } - val localVariableHeaders = mutableMapOf() + val localVariableHeaders = mutableMapOf() - val localVariableConfig = RequestConfig( + val localVariableConfig = RequestConfig( RequestMethod.GET, "/flow-mode/ongoing", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return request( + return request( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } - - /** - * - * - * @param source To specify source of request (optional) - * @param pairing (optional) - * @param duration (optional) - * @param taskIntegrationType (optional) - * @param taskIntegrationId (optional) - * @param taskId (optional) - * @return StartFlowMode200Response - */ - @Suppress("UNCHECKED_CAST") - open suspend fun startFlowMode(source: kotlin.String?, pairing: kotlin.Boolean?, duration: kotlin.Int?, taskIntegrationType: kotlin.String?, taskIntegrationId: kotlin.String?, taskId: kotlin.String?): HttpResponse { - - val localVariableAuthNames = listOf("api_token") - - val localVariableBody = - io.ktor.client.utils.EmptyContent - - val localVariableQuery = mutableMapOf>() - source?.apply { localVariableQuery["source"] = listOf("$source") } - pairing?.apply { localVariableQuery["pairing"] = listOf("$pairing") } - duration?.apply { localVariableQuery["duration"] = listOf("$duration") } - taskIntegrationType?.apply { localVariableQuery["taskIntegrationType"] = listOf("$taskIntegrationType") } - taskIntegrationId?.apply { localVariableQuery["taskIntegrationId"] = listOf("$taskIntegrationId") } - taskId?.apply { localVariableQuery["taskId"] = listOf("$taskId") } - - val localVariableHeaders = mutableMapOf() - - val localVariableConfig = RequestConfig( + ).wrap() + } + + /** + * + * + * @param source To specify source of request (optional) + * @param pairing (optional) + * @param duration (optional) + * @param taskIntegrationType (optional) + * @param taskIntegrationId (optional) + * @param taskId (optional) + * @return StartFlowMode200Response + */ + @Suppress("UNCHECKED_CAST") + open suspend fun startFlowMode( + source: kotlin.String?, + pairing: kotlin.Boolean?, + duration: kotlin.Int?, + taskIntegrationType: kotlin.String?, + taskType: kotlin.String?, + taskId: kotlin.String? + ): HttpResponse { + + val localVariableAuthNames = listOf("api_token") + + val localVariableBody = + io.ktor.client.utils.EmptyContent + + val localVariableQuery = mutableMapOf>() + source?.apply { localVariableQuery["source"] = listOf("$source") } + pairing?.apply { localVariableQuery["pairing"] = listOf("$pairing") } + duration?.apply { localVariableQuery["duration"] = listOf("$duration") } + taskIntegrationType?.apply { localVariableQuery["taskIntegrationType"] = listOf("$taskIntegrationType") } + taskType?.apply { localVariableQuery["taskType"] = listOf("$taskType") } + taskId?.apply { localVariableQuery["taskId"] = listOf("$taskId") } + + val localVariableHeaders = mutableMapOf() + + val localVariableConfig = RequestConfig( RequestMethod.POST, "/flow-mode/start", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return request( + return request( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } + ).wrap() + } - /** - * - * - * @param source To specify source of request (optional) - * @return StopOngoingFlowMode200Response - */ - @Suppress("UNCHECKED_CAST") - open suspend fun stopOngoingFlowMode(source: kotlin.String?): HttpResponse { + /** + * + * + * @param source To specify source of request (optional) + * @return StopOngoingFlowMode200Response + */ + @Suppress("UNCHECKED_CAST") + open suspend fun stopOngoingFlowMode(source: kotlin.String?): HttpResponse { - val localVariableAuthNames = listOf("api_token") + val localVariableAuthNames = listOf("api_token") - val localVariableBody = - io.ktor.client.utils.EmptyContent + val localVariableBody = + io.ktor.client.utils.EmptyContent - val localVariableQuery = mutableMapOf>() - source?.apply { localVariableQuery["source"] = listOf("$source") } + val localVariableQuery = mutableMapOf>() + source?.apply { localVariableQuery["source"] = listOf("$source") } - val localVariableHeaders = mutableMapOf() + val localVariableHeaders = mutableMapOf() - val localVariableConfig = RequestConfig( + val localVariableConfig = RequestConfig( RequestMethod.POST, "/flow-mode/stop", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return request( + return request( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } + ).wrap() + } - } +} diff --git a/src/main/kotlin/co/makerflow/client/infrastructure/ApiClient.kt b/src/main/kotlin/co/makerflow/client/infrastructure/ApiClient.kt index 5ce8d67..dd77627 100644 --- a/src/main/kotlin/co/makerflow/client/infrastructure/ApiClient.kt +++ b/src/main/kotlin/co/makerflow/client/infrastructure/ApiClient.kt @@ -1,6 +1,16 @@ package co.makerflow.client.infrastructure +import co.makerflow.client.auth.ApiKeyAuth +import co.makerflow.client.auth.Authentication +import co.makerflow.client.auth.HttpBasicAuth +import co.makerflow.client.auth.HttpBearerAuth +import co.makerflow.client.auth.OAuth +import com.fasterxml.jackson.core.util.DefaultIndenter +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.HttpClientEngine @@ -12,16 +22,17 @@ import io.ktor.client.request.parameter import io.ktor.client.request.request import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.Parameters +import io.ktor.http.URLBuilder import io.ktor.http.content.PartData -import io.ktor.serialization.jackson.* -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import co.makerflow.client.auth.* -import io.ktor.http.* +import io.ktor.http.contentType +import io.ktor.http.encodeURLQueryComponent +import io.ktor.http.encodedPath +import io.ktor.http.takeFrom +import io.ktor.serialization.jackson.jackson open class ApiClient( private val baseUrl: String, diff --git a/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200Response.kt b/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200Response.kt index 0b4c6c7..12c75bc 100644 --- a/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200Response.kt +++ b/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200Response.kt @@ -15,19 +15,27 @@ package co.makerflow.client.models +import co.makerflow.client.models.FetchOngoingFlowMode200ResponseAnyOf +import co.makerflow.client.models.FlowMode +import co.makerflow.client.models.TypedTodo + import com.fasterxml.jackson.annotation.JsonProperty /** + * * - * - * @param `data` + * @param `data` + * @param todo */ data class FetchOngoingFlowMode200Response ( @field:JsonProperty("data") - val `data`: FlowMode? = null + val `data`: FlowMode? = null, + + @field:JsonProperty("todo") + val todo: TypedTodo? = null ) diff --git a/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200ResponseAnyOf.kt b/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200ResponseAnyOf.kt new file mode 100644 index 0000000..01e883d --- /dev/null +++ b/src/main/kotlin/co/makerflow/client/models/FetchOngoingFlowMode200ResponseAnyOf.kt @@ -0,0 +1,40 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package co.makerflow.client.models + +import co.makerflow.client.models.FlowMode +import co.makerflow.client.models.TypedTodo + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * + * + * @param `data` + * @param todo + */ + + +data class FetchOngoingFlowMode200ResponseAnyOf ( + + @field:JsonProperty("data") + val `data`: FlowMode? = null, + + @field:JsonProperty("todo") + val todo: TypedTodo? = null + +) + diff --git a/src/main/kotlin/co/makerflow/client/models/StartFlowMode200Response.kt b/src/main/kotlin/co/makerflow/client/models/StartFlowMode200Response.kt index 4ef602b..ccddcce 100644 --- a/src/main/kotlin/co/makerflow/client/models/StartFlowMode200Response.kt +++ b/src/main/kotlin/co/makerflow/client/models/StartFlowMode200Response.kt @@ -16,6 +16,7 @@ package co.makerflow.client.models import co.makerflow.client.models.FlowMode +import co.makerflow.client.models.TypedTodo import com.fasterxml.jackson.annotation.JsonProperty @@ -23,13 +24,17 @@ import com.fasterxml.jackson.annotation.JsonProperty * * * @param `data` + * @param todo */ data class StartFlowMode200Response ( @field:JsonProperty("data") - val `data`: FlowMode? = null + val `data`: FlowMode? = null, + + @field:JsonProperty("todo") + val todo: TypedTodo? = null ) diff --git a/src/main/kotlin/co/makerflow/client/models/StopOngoingFlowMode200Response.kt b/src/main/kotlin/co/makerflow/client/models/StopOngoingFlowMode200Response.kt index d0357c4..ee1b3ec 100644 --- a/src/main/kotlin/co/makerflow/client/models/StopOngoingFlowMode200Response.kt +++ b/src/main/kotlin/co/makerflow/client/models/StopOngoingFlowMode200Response.kt @@ -16,6 +16,7 @@ package co.makerflow.client.models import co.makerflow.client.models.EndedFlowMode +import co.makerflow.client.models.TypedTodo import com.fasterxml.jackson.annotation.JsonProperty @@ -23,13 +24,17 @@ import com.fasterxml.jackson.annotation.JsonProperty * * * @param `data` + * @param todo */ data class StopOngoingFlowMode200Response ( @field:JsonProperty("data") - val `data`: EndedFlowMode? = null + val `data`: EndedFlowMode? = null, + + @field:JsonProperty("todo") + val todo: TypedTodo? = null ) diff --git a/src/main/kotlin/co/makerflow/intellijplugin/actions/ToggleFlowModeAction.kt b/src/main/kotlin/co/makerflow/intellijplugin/actions/ToggleFlowModeAction.kt index e6aa4d1..20da6f0 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/actions/ToggleFlowModeAction.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/actions/ToggleFlowModeAction.kt @@ -2,14 +2,14 @@ package co.makerflow.intellijplugin.actions import co.makerflow.intellijplugin.services.FlowModeService import co.makerflow.intellijplugin.services.toFlow -import co.makerflow.intellijplugin.state.Flow import co.makerflow.intellijplugin.state.FlowState import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service -import kotlinx.coroutines.* -import kotlinx.datetime.Instant +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class ToggleFlowModeAction : AnAction("Toggle Flow Mode", "Begin or end flow mode based on current status", null) { diff --git a/src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModeProjectManagerListener.kt b/src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModePostStartupActivity.kt similarity index 75% rename from src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModeProjectManagerListener.kt rename to src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModePostStartupActivity.kt index 64fcb8b..f58cde4 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModeProjectManagerListener.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/listeners/FlowModePostStartupActivity.kt @@ -8,23 +8,22 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManagerListener +import com.intellij.openapi.startup.ProjectActivity import com.intellij.util.concurrency.AppExecutorUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch private const val DELAY_BETWEEN_FETCHES = 10L -class FlowModeProjectManagerListener : ProjectManagerListener { +class FlowModePostStartupActivity : ProjectActivity { // A new Job to fetch the ongoing flow mode private val coroutineScope = CoroutineScope(Dispatchers.IO) - override fun projectOpened(project: Project) { + override suspend fun execute(project: Project) { service().heartbeat() @@ -35,17 +34,13 @@ class FlowModeProjectManagerListener : ProjectManagerListener { coroutineScope.launch { val flowModeService = service() val ongoingFlowMode = flowModeService.fetchOngoingFlowMode() - ongoingFlowMode?.let { flowMode -> - FlowState.instance.currentFlow = flowMode.toFlow() + ongoingFlowMode.let { pair -> + FlowState.instance.currentFlow = pair.first?.toFlow(pair.second) } } } }, 0, DELAY_BETWEEN_FETCHES, java.util.concurrent.TimeUnit.SECONDS) } - override fun projectClosing(project: Project) { - super.projectClosing(project) - coroutineScope.cancel() - } } diff --git a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt index f576da7..9a895f5 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt @@ -5,28 +5,33 @@ import co.makerflow.client.models.OnboardingTask import co.makerflow.client.models.OnboardingTask.Step import co.makerflow.client.models.PullRequestTodo import co.makerflow.client.models.TypedTodo +import co.makerflow.intellijplugin.services.FlowModeService import co.makerflow.intellijplugin.services.TasksService +import co.makerflow.intellijplugin.services.TodoUtil +import co.makerflow.intellijplugin.services.toFlow +import co.makerflow.intellijplugin.state.FlowState import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.ActionPlaces -import com.intellij.openapi.actionSystem.ActionToolbar import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.impl.ActionButton import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.ui.JBColor import com.intellij.ui.RoundedLineBorder import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.gridLayout.HorizontalAlign -import com.intellij.ui.dsl.gridLayout.VerticalAlign +import com.intellij.ui.dsl.gridLayout.Gaps import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.ui.AsyncProcessIcon import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -35,7 +40,6 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime import org.ocpsoft.prettytime.PrettyTime -import java.awt.FlowLayout import java.awt.event.ItemEvent import javax.swing.BoxLayout import javax.swing.Icon @@ -44,6 +48,7 @@ import javax.swing.JComponent import javax.swing.border.CompoundBorder private const val DELAY_TO_RELOAD_TASKS = 30L +private const val INITIAL_DELAY_TO_RELOAD_TASKS = 60L private const val CONTENT_TOP_OFFSET = 10 private const val LIST_ITEM_PADDING_MARGIN = 5 private const val ROUNDED_CORNER_RADIUS = 5 @@ -58,13 +63,11 @@ class TasksPanel : SimpleToolWindowPanel(true) { cell( AsyncProcessIcon.BigCentered("Loading tasks...") ) - .horizontalAlign(HorizontalAlign.CENTER) - .verticalAlign(VerticalAlign.CENTER) + .align(Align.CENTER) } row { label("Please wait, loading tasks...") - .horizontalAlign(HorizontalAlign.CENTER) - .verticalAlign(VerticalAlign.CENTER) + .align(Align.CENTER) } }.apply { border = JBUI.Borders.empty() @@ -83,6 +86,10 @@ class TasksPanel : SimpleToolWindowPanel(true) { class ReloadAction(private val tasksPanel: TasksPanel) : AnAction("Reload", "Reload tasks", AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { + reload() + } + + fun reload() { tasksPanel.loadTasks() } } @@ -102,16 +109,13 @@ class TasksPanel : SimpleToolWindowPanel(true) { ApplicationManager.getApplication().invokeLater { loadTasks() } - }, DELAY_TO_RELOAD_TASKS + 30, DELAY_TO_RELOAD_TASKS, java.util.concurrent.TimeUnit.SECONDS) + }, INITIAL_DELAY_TO_RELOAD_TASKS, DELAY_TO_RELOAD_TASKS, java.util.concurrent.TimeUnit.SECONDS) } private fun loadTasks() { val service = service() fetchTasksCoroutineScope.launch { - UIUtil.invokeLaterIfNeeded { - super.setContent(loadingMessageContent) - } checkboxes.clear() val tasks = service.fetchTasks().sortedBy { it.createdAt } if (tasks.isEmpty()) { @@ -119,8 +123,7 @@ class TasksPanel : SimpleToolWindowPanel(true) { val noTasksMessage = panel { row { label("No new tasks, you are a free bird!") - .horizontalAlign(HorizontalAlign.CENTER) - .verticalAlign(VerticalAlign.CENTER) + .align(Align.CENTER) } }.apply { border = JBUI.Borders.empty() @@ -137,7 +140,7 @@ class TasksPanel : SimpleToolWindowPanel(true) { checkboxes.add(JCheckBox()) } ApplicationManager.getApplication().invokeLater { - val taskPresentationComponent = TaskPresentationComponent(checkboxes, service) + val taskPresentationComponent = TaskPresentationComponent(reloadAction, checkboxes, service) val updatedContainer = JBPanel>() // Set layout to BoxLayout to stack components vertically updatedContainer.layout = BoxLayout(updatedContainer, BoxLayout.Y_AXIS) @@ -172,11 +175,11 @@ private class TaskDone(val task: TypedTodo, val service: TasksService) { ApplicationManager.getApplication().invokeLater { if (value) { markDoneCoroutineScope.launch { - service.markTaskDone(task) + service.markTaskDone(task) } } else { unmarkDoneCoroutineScope.launch { - service.markTaskUndone(task) + service.markTaskUndone(task) } } } @@ -185,9 +188,16 @@ private class TaskDone(val task: TypedTodo, val service: TasksService) { } } +private const val FLOW_MODE_DROPDOWN_WITHOUT_TIMER = "Flow Mode without timer" +private const val FLOW_MODE_DROPDOWN_25_MINUTES = "Flow Mode for 25 minutes" +private const val FLOW_MODE_DROPDOWN_50_MINUTES = "Flow Mode for 50 minutes" +private const val FLOW_MODE_DROPDOWN_75_MINUTES = "Flow Mode for 75 minutes" + class TaskPresentationComponent( + private val reloadAction: TasksPanel.ReloadAction, private var checkboxes: ArrayList, - private val service: TasksService + private val service: TasksService, + private val todoUtil: TodoUtil = service() ) { private val border = CompoundBorder( @@ -200,6 +210,9 @@ class TaskPresentationComponent( ) ) + private val beginFlowModeCoroutineScope = CoroutineScope(Dispatchers.IO) + private val endFlowModeCoroutineScope = CoroutineScope(Dispatchers.IO) + fun getComponent(value: TypedTodo?, index: Int): JComponent { val taskTitle = getTaskTitle(value) val taskSubtitle = getSubtitle(value!!) @@ -222,26 +235,70 @@ class TaskPresentationComponent( } } checkboxes[index] = checkbox + todoUtil.isTodoInFlow(value) val panel = panel { row { cell(checkbox) panel { row { - icon(getIconForType(value.type!!)) + if (value !is OnboardingTask) { + val isTodoInFlow = todoUtil.isTodoInFlow(value) + val showDropdown = AtomicBooleanProperty(!isTodoInFlow) + val showStopButton = AtomicBooleanProperty(isTodoInFlow) + val stoppingFlowMode = AtomicBooleanProperty(false) + link("Stop") { + stoppingFlowMode.set(true) + showStopButton.set(false) + endFlowModeCoroutineScope.launch { + try { + service().stopFlowMode() + FlowState.instance.currentFlow = null + stoppingFlowMode.set(false) + showStopButton.set(false) + showDropdown.set(true) + reloadAction.reload() + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + thisLogger().error("Error stopping flow mode", e) + stoppingFlowMode.set(false) + showStopButton.set(true) + throw e + } + } + } + .applyToComponent { + this.toolTipText = "Stop flow mode" + }.apply { + visible(showStopButton.get()) + showStopButton.afterChange { + visible(it) + } + } + label("Stopping...") + .align(AlignX.LEFT) + .customize(Gaps()) + .apply { + visible(stoppingFlowMode.get()) + stoppingFlowMode.afterChange { + visible(it) + } + } + startFlowModeDropdown(value, showDropdown) + } + icon(getIconForType(value.type!!)).align(AlignX.CENTER) if (link != null) { browserLink(taskTitle!!, link) } else { label(taskTitle!!) } } - separator() - row { - comment(taskSubtitle) - }.bottomGap(BottomGap.NONE) } + }.bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) + separator() + row { + cell() + comment(taskSubtitle) }.bottomGap(BottomGap.NONE) - } panel.border = border @@ -249,6 +306,72 @@ class TaskPresentationComponent( return panel } + private fun Row.startFlowModeDropdown(value: TypedTodo?, showDropdown: AtomicBooleanProperty) { + val startingFlowMode = AtomicBooleanProperty(false) + dropDownLink( + "Start", + listOf( + FLOW_MODE_DROPDOWN_WITHOUT_TIMER, + FLOW_MODE_DROPDOWN_25_MINUTES, + FLOW_MODE_DROPDOWN_50_MINUTES, + FLOW_MODE_DROPDOWN_75_MINUTES, + ) + ).onChanged { + val flowModeService = service() + beginFlowModeCoroutineScope.launch { + showDropdown.set(false) + startingFlowMode.set(true) + try { + val flowMode = when (it.selectedItem) { + FLOW_MODE_DROPDOWN_WITHOUT_TIMER -> { + flowModeService.startFlowMode(value, time = null) + } + + FLOW_MODE_DROPDOWN_25_MINUTES -> { + flowModeService.startFlowMode(value, time = 25) + } + + FLOW_MODE_DROPDOWN_50_MINUTES -> { + flowModeService.startFlowMode(value, time = 50) + } + + FLOW_MODE_DROPDOWN_75_MINUTES -> { + flowModeService.startFlowMode(value, time = 75) + } + + else -> null + } + if (flowMode != null) { + FlowState.instance.currentFlow = flowMode.toFlow(value) + } + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + thisLogger().error("Error starting flow mode", e) + showDropdown.set(true) + throw e + } + reloadAction.reload() + } + } + .apply { + visible(showDropdown.get()) + showDropdown.afterChange { + visible(it) + } + } + .applyToComponent { + this.toolTipText = "Start a flow mode session for this task" + } + label("Starting...") + .align(AlignX.LEFT) + .customize(Gaps()) + .apply { + visible(startingFlowMode.get()) + startingFlowMode.afterChange { + visible(it) + } + } + } + private fun getTaskTitle(value: TypedTodo?) = when (value) { is PullRequestTodo -> { "PR #${value.pr?.pullrequestId}: ${value.pr?.pullrequestTitle}" @@ -339,7 +462,10 @@ class TaskPresentationComponent( if (sourceDescription.isEmpty()) { return createdAt } - return "$sourceDescription$createdAt" + if (createdAt.isEmpty()) { + return sourceDescription + } + return "$sourceDescription | $createdAt" } } diff --git a/src/main/kotlin/co/makerflow/intellijplugin/services/FlowModeService.kt b/src/main/kotlin/co/makerflow/intellijplugin/services/FlowModeService.kt index 9e4ec2d..b45c191 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/services/FlowModeService.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/services/FlowModeService.kt @@ -4,9 +4,11 @@ import co.makerflow.client.apis.FlowModeApi import co.makerflow.client.infrastructure.ApiClient import co.makerflow.client.models.EndedFlowMode import co.makerflow.client.models.FlowMode +import co.makerflow.client.models.TypedTodo import co.makerflow.intellijplugin.settings.SettingsState import co.makerflow.intellijplugin.state.Flow import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.squareup.moshi.JsonEncodingException import io.ktor.client.call.NoTransformationFoundException import io.ktor.http.HttpStatusCode @@ -18,15 +20,25 @@ import kotlinx.datetime.Instant @Service class FlowModeService { private val baseUrl = System.getenv("MAKERFLOW_API_URL") ?: ApiClient.BASE_URL + private val todoUtil = service() + suspend fun startFlowMode(): FlowMode? { + return startFlowMode(null, null) + } + suspend fun startFlowMode(todo: TypedTodo?, time: Int?): FlowMode? { return coroutineScope { var flowMode: FlowMode? = null launch { val flowModeApi = flowModeApi() - val startFlowModeResponse = flowModeApi.startFlowMode("jetbrains", false, null, null, null, null) + val startFlowModeResponse = flowModeApi.startFlowMode("jetbrains", + false, + time, + todo?.sourceType?.name, + todo?.type, + todo?.let { todoUtil.determineTodoId(todo) }) flowMode = if (startFlowModeResponse.status == HttpStatusCode.Conflict.value) { - fetchOngoingFlowMode() + fetchOngoingFlowMode().first } else { startFlowModeResponse.body().data } @@ -52,37 +64,41 @@ class FlowModeService { private fun getApiToken() = System.getenv("MAKERFLOW_API_TOKEN") ?: SettingsState.instance.apiToken - suspend fun fetchOngoingFlowMode(): FlowMode? = coroutineScope { - var flowMode: FlowMode? = null + suspend fun fetchOngoingFlowMode(): Pair = coroutineScope { + var pair = Pair(null, null) launch { val flowModeApi = flowModeApi() - val fetchOngoingFlowMode = flowModeApi.fetchOngoingFlowMode("jetbrains") - if (fetchOngoingFlowMode.success) { + val response = flowModeApi.fetchOngoingFlowMode("jetbrains") + if (response.success) { @Suppress("TooGenericExceptionCaught") try { - flowMode = fetchOngoingFlowMode.body().data + val body = response.body() + pair = Pair(body.data, body.todo) } catch (e: Exception) { - when(e) { + @Suppress("kotlin:S125") + when (e) { is JsonConvertException, is NoTransformationFoundException, is JsonEncodingException -> { - /* - Intentionally left blank - thisLogger().error(e) - thisLogger().error("Error converting ongoing flow mode: ${e.message}") - */ + /* + Intentionally left blank */ +// thisLogger().error(e) +// thisLogger().error("Error converting ongoing flow mode: ${e.message}") } + else -> throw e } } } }.join() - return@coroutineScope flowMode + return@coroutineScope pair } + + } // extension function to convert from FlowMode to Flow -fun FlowMode.toFlow(): Flow { +fun FlowMode.toFlow(todo: TypedTodo? = null): Flow { val scheduledEnd = this.scheduledEnd?.let { Instant.parse(it) } - return Flow(this.id, Instant.parse(this.start), this.pairing ?: false, scheduledEnd) + return Flow(this.id, Instant.parse(this.start), this.pairing ?: false, scheduledEnd, todo) } diff --git a/src/main/kotlin/co/makerflow/intellijplugin/services/TodoUtil.kt b/src/main/kotlin/co/makerflow/intellijplugin/services/TodoUtil.kt new file mode 100644 index 0000000..59f9c01 --- /dev/null +++ b/src/main/kotlin/co/makerflow/intellijplugin/services/TodoUtil.kt @@ -0,0 +1,32 @@ +package co.makerflow.intellijplugin.services + +import co.makerflow.client.models.CustomTaskTodo +import co.makerflow.client.models.PullRequestTodo +import co.makerflow.client.models.TypedTodo +import co.makerflow.intellijplugin.state.FlowState +import com.intellij.openapi.components.Service + +@Service +class TodoUtil { + fun determineTodoId(todo: TypedTodo): String { + return when (todo.sourceType) { + TypedTodo.SourceType.makerflow -> { + if (todo is CustomTaskTodo) todo.task.id.toString() + else throw UnsupportedOperationException("Unknown makerflow todo type") + } + + TypedTodo.SourceType.github -> (todo as PullRequestTodo).pr?.id.toString() + TypedTodo.SourceType.bitbucket -> (todo as PullRequestTodo).pr?.id.toString() + TypedTodo.SourceType.slack -> (todo as PullRequestTodo).pr?.id.toString() + else -> { + throw UnsupportedOperationException("Unknown todo type") + } + } + } + + fun isTodoInFlow(todo: TypedTodo): Boolean { + return FlowState.instance.currentFlow?.todo?.let { + todo.sourceType == it.sourceType && todo.type == it.type && determineTodoId(todo) == determineTodoId(it) + } ?: false + } +} diff --git a/src/main/kotlin/co/makerflow/intellijplugin/state/Flow.kt b/src/main/kotlin/co/makerflow/intellijplugin/state/Flow.kt index 3a7e2bc..24f52e0 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/state/Flow.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/state/Flow.kt @@ -1,6 +1,8 @@ package co.makerflow.intellijplugin.state +import co.makerflow.client.models.TypedTodo import kotlinx.datetime.Instant +import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable @Serializable @@ -9,4 +11,5 @@ data class Flow( val start: Instant, val pairing: Boolean, val scheduledEnd: Instant?, + @Contextual val todo: TypedTodo?, ) diff --git a/src/main/kotlin/co/makerflow/intellijplugin/status/FlowStatusBarWidgetFactory.kt b/src/main/kotlin/co/makerflow/intellijplugin/status/FlowStatusBarWidgetFactory.kt index 0d9a0ad..8ca0c00 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/status/FlowStatusBarWidgetFactory.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/status/FlowStatusBarWidgetFactory.kt @@ -8,16 +8,20 @@ import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFact @Suppress("UnstableApiUsage") class FlowStatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory(), LightEditCompatible { + + private val id = "co.makerflow.intellijplugin" + override fun getId(): String { - return "MakerflowFlowStatus" + return id } override fun getDisplayName(): String { + @Suppress("DialogTitleCapitalization") return "Flow Mode status" } override fun createWidget(project: Project): StatusBarWidget { - return co.makerflow.intellijplugin.status.FlowStatusBarWidget(project) + return FlowStatusBarWidget(project) } override fun disposeWidget(widget: StatusBarWidget) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d09a25c..0484821 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -25,6 +25,7 @@ + @@ -32,10 +33,4 @@ - - - - -