diff --git a/CHANGELOG.md b/CHANGELOG.md index 1641d67..d51de89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ track of all your notifications. - Close distracting apps with the Makerflow desktop app - Added a tool window to see items in Unified Task List - See all your notifications from GitHub/Bitbucket in your IDE + - Add new tasks + - Start Flow Mode for a task to focus on it ### Changed diff --git a/src/main/kotlin/co/makerflow/client/apis/TasksApi.kt b/src/main/kotlin/co/makerflow/client/apis/TasksApi.kt index 51ec904..2a0788c 100644 --- a/src/main/kotlin/co/makerflow/client/apis/TasksApi.kt +++ b/src/main/kotlin/co/makerflow/client/apis/TasksApi.kt @@ -15,124 +15,165 @@ package co.makerflow.client.apis +import co.makerflow.client.infrastructure.ApiClient +import co.makerflow.client.infrastructure.HttpResponse +import co.makerflow.client.infrastructure.RequestConfig +import co.makerflow.client.infrastructure.RequestMethod +import co.makerflow.client.infrastructure.wrap +import co.makerflow.client.models.AddCustomTask200Response import co.makerflow.client.models.CalendarEvent +import co.makerflow.client.models.CustomTask import co.makerflow.client.models.MarkDoneRequest import co.makerflow.client.models.TypedTodo - -import co.makerflow.client.infrastructure.* +import com.fasterxml.jackson.databind.ObjectMapper import io.ktor.client.HttpClientConfig -import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine -import io.ktor.http.ParametersBuilder -import com.fasterxml.jackson.databind.ObjectMapper - open class TasksApi( +open class TasksApi( 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) + * @param customTask Task to be added (optional) + * @return AddCustomTask200Response + */ + @Suppress("UNCHECKED_CAST") + open suspend fun addCustomTask( + source: kotlin.String?, + customTask: CustomTask? + ): HttpResponse { + + val localVariableAuthNames = listOf("api_token") + + val localVariableBody = customTask + + val localVariableQuery = mutableMapOf>() + source?.apply { localVariableQuery["source"] = listOf("$source") } + + val localVariableHeaders = mutableMapOf() + + val localVariableConfig = RequestConfig( + RequestMethod.POST, + "/custom_tasks", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = true, + ) + + return jsonRequest( + localVariableConfig, + localVariableBody, + localVariableAuthNames + ).wrap() + } + - /** - * - * - * @param source To specify source of request (optional) - * @return kotlin.collections.List - */ - @Suppress("UNCHECKED_CAST") - open suspend fun getTodos(source: kotlin.String?): HttpResponse> { + /** + * + * + * @param source To specify source of request (optional) + * @return kotlin.collections.List + */ + @Suppress("UNCHECKED_CAST") + open suspend fun getTodos(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, "/tasks/todo", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return request( + return request( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } + ).wrap() + } - /** - * - * - * @param source To specify source of request (optional) - * @param markDoneRequest Task to be marked as completed (optional) - * @return TypedTodo - */ - @Suppress("UNCHECKED_CAST") - open suspend fun markDone(source: kotlin.String?, markDoneRequest: MarkDoneRequest?): HttpResponse { + /** + * + * + * @param source To specify source of request (optional) + * @param markDoneRequest Task to be marked as completed (optional) + * @return TypedTodo + */ + @Suppress("UNCHECKED_CAST") + open suspend fun markDone(source: kotlin.String?, markDoneRequest: MarkDoneRequest?): HttpResponse { - val localVariableAuthNames = listOf("api_token") + val localVariableAuthNames = listOf("api_token") - val localVariableBody = markDoneRequest + val localVariableBody = markDoneRequest - 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, "/tasks/todo/done", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return jsonRequest( + return jsonRequest( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } + ).wrap() + } - /** - * - * - * @param source To specify source of request (optional) - * @return kotlin.collections.List - */ - @Suppress("UNCHECKED_CAST") - open suspend fun upcomingCalendarEvents(source: kotlin.String?): HttpResponse> { + /** + * + * + * @param source To specify source of request (optional) + * @return kotlin.collections.List + */ + @Suppress("UNCHECKED_CAST") + open suspend fun upcomingCalendarEvents(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, "/tasks/calendar/events", query = localVariableQuery, headers = localVariableHeaders, requiresAuthentication = true, - ) + ) - return request( + return request( localVariableConfig, localVariableBody, localVariableAuthNames - ).wrap() - } + ).wrap() + } - } +} diff --git a/src/main/kotlin/co/makerflow/client/models/AddCustomTask200Response.kt b/src/main/kotlin/co/makerflow/client/models/AddCustomTask200Response.kt new file mode 100644 index 0000000..d5ce8f2 --- /dev/null +++ b/src/main/kotlin/co/makerflow/client/models/AddCustomTask200Response.kt @@ -0,0 +1,41 @@ +/** + * + * 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 com.fasterxml.jackson.annotation.JsonProperty + +/** + * + * + * @param `data` + * @param message + * @param success + */ + + +data class AddCustomTask200Response( + + @field:JsonProperty("data") + val `data`: CustomTask? = null, + + @field:JsonProperty("message") + val message: kotlin.String? = null, + + @field:JsonProperty("success") + val success: kotlin.Boolean? = null + +) + diff --git a/src/main/kotlin/co/makerflow/client/models/TypedTodo.kt b/src/main/kotlin/co/makerflow/client/models/TypedTodo.kt index 62bf605..f464d1f 100644 --- a/src/main/kotlin/co/makerflow/client/models/TypedTodo.kt +++ b/src/main/kotlin/co/makerflow/client/models/TypedTodo.kt @@ -38,6 +38,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo * @param pr * @param meta */ +@Suppress("ConvertSecondaryConstructorToPrimary") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true) @JsonSubTypes( JsonSubTypes.Type(value = PullRequestTodo::class, name = "bitbucket"), @@ -50,13 +51,13 @@ open class TypedTodo { /* The type of source that the todo is from */ @get:JsonProperty("sourceType") - val sourceType: TypedTodo.SourceType? = null + var sourceType: TypedTodo.SourceType? = null /* The type of todo */ @get:JsonProperty("type") - val type: kotlin.String? = null + var type: kotlin.String? = null /* Timestamp for when the todo was created */ @get:JsonProperty("createdAt") - val createdAt: kotlin.String? = null + var createdAt: kotlin.String? = null /* Whether the todo has been completed */ @get:JsonProperty("done") var done: kotlin.Boolean? = null diff --git a/src/main/kotlin/co/makerflow/intellijplugin/actions/tasks/AddCustomTaskAction.kt b/src/main/kotlin/co/makerflow/intellijplugin/actions/tasks/AddCustomTaskAction.kt new file mode 100644 index 0000000..785c602 --- /dev/null +++ b/src/main/kotlin/co/makerflow/intellijplugin/actions/tasks/AddCustomTaskAction.kt @@ -0,0 +1,16 @@ +package co.makerflow.intellijplugin.actions.tasks + +import co.makerflow.intellijplugin.dialogs.AddTaskDialog +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent + +class AddCustomTaskAction : AnAction( + "Makerflow: Add Task", + "Add a new task to the Makerflow task list", + AllIcons.Actions.AddList +) { + override fun actionPerformed(e: AnActionEvent) { + AddTaskDialog().showAndGet() + } +} diff --git a/src/main/kotlin/co/makerflow/intellijplugin/dialogs/AddTaskDialog.kt b/src/main/kotlin/co/makerflow/intellijplugin/dialogs/AddTaskDialog.kt new file mode 100644 index 0000000..3a239b8 --- /dev/null +++ b/src/main/kotlin/co/makerflow/intellijplugin/dialogs/AddTaskDialog.kt @@ -0,0 +1,63 @@ +package co.makerflow.intellijplugin.dialogs + +import co.makerflow.intellijplugin.services.TasksService +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.swing.JComponent + +class AddTaskDialog : DialogWrapper(false) { + + private val propertyGraph = PropertyGraph() + private val taskName = propertyGraph.property("") + private lateinit var taskField: Cell + + init { + title = "Makerflow: Add Task" + init() + } + + override fun createCenterPanel(): JComponent { + return panel { + row { + textField() + .bindText(taskName) + .apply { taskField = this } + .comment("What do you want to accomplish?") + } + } + } + + override fun getPreferredFocusedComponent(): JComponent = taskField.component + + override fun doValidate(): ValidationInfo? { + if (taskName.get().isBlank()) { + return ValidationInfo("Task cannot be empty", taskField.component) + } + return null + } + + private val addTaskCoroutineScope = CoroutineScope(Dispatchers.IO) + + override fun doOKAction() { + super.doOKAction() + ApplicationManager.getApplication().invokeLater { + addTaskCoroutineScope.launch { + service().addTask(taskName.get())?.let { + // send a message on the message bus so the panel knows to reload + ApplicationManager.getApplication().messageBus.syncPublisher(TasksService.TASKS_ADDED_TOPIC) + .taskAdded(it) + } + } + } + } +} diff --git a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt index 05078e2..a17a5a0 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksPanel.kt @@ -6,6 +6,7 @@ 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.TaskAdded import co.makerflow.intellijplugin.services.TasksService import co.makerflow.intellijplugin.services.TodoUtil import co.makerflow.intellijplugin.services.toFlow @@ -98,7 +99,6 @@ class TasksPanel : SimpleToolWindowPanel(true) { init { - super.setContent(loadingMessageContent) ApplicationManager.getApplication().invokeLater { @@ -111,6 +111,16 @@ class TasksPanel : SimpleToolWindowPanel(true) { } }, INITIAL_DELAY_TO_RELOAD_TASKS, DELAY_TO_RELOAD_TASKS, java.util.concurrent.TimeUnit.SECONDS) + // Listen for TasksAdded messages on the message bus + ApplicationManager.getApplication().messageBus.connect().subscribe( + TasksService.TASKS_ADDED_TOPIC, + TaskAdded { + ApplicationManager.getApplication().invokeLater { + loadTasks() + } + } + ) + } private fun loadTasks() { @@ -243,47 +253,47 @@ class TaskPresentationComponent( panel { row { 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 - } + 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" + } + .applyToComponent { + this.toolTipText = "Stop flow mode" }.apply { visible(showStopButton.get()) showStopButton.afterChange { visible(it) } - } - label("Stopping...") + } + label("Stopping...") .align(AlignX.LEFT) - .customize(Gaps()) + .customize(Gaps()) .apply { visible(stoppingFlowMode.get()) stoppingFlowMode.afterChange { visible(it) } } - startFlowModeDropdown(value, showDropdown) + startFlowModeDropdown(value, showDropdown) } icon(getIconForType(value.type!!)).align(AlignX.CENTER) if (link != null) { @@ -317,41 +327,41 @@ class TaskPresentationComponent( 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, duration = null) - } - - FLOW_MODE_DROPDOWN_25_MINUTES -> { - flowModeService.startFlowMode(value, duration = 25) - } - - FLOW_MODE_DROPDOWN_50_MINUTES -> { - flowModeService.startFlowMode(value, duration = 50) - } + 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, duration = null) + } - FLOW_MODE_DROPDOWN_75_MINUTES -> { - flowModeService.startFlowMode(value, duration = 75) - } + FLOW_MODE_DROPDOWN_25_MINUTES -> { + flowModeService.startFlowMode(value, duration = 25) + } - else -> null + FLOW_MODE_DROPDOWN_50_MINUTES -> { + flowModeService.startFlowMode(value, duration = 50) } - if (flowMode != null) { - FlowState.instance.currentFlow = flowMode.toFlow(value) + + FLOW_MODE_DROPDOWN_75_MINUTES -> { + flowModeService.startFlowMode(value, duration = 75) } - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - thisLogger().error("Error starting flow mode", e) - showDropdown.set(true) - throw e + + else -> null } - reloadAction.reload() + 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 { diff --git a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksToolWindowFactory.kt b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksToolWindowFactory.kt index 70745af..b35feb8 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksToolWindowFactory.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/panels/TasksToolWindowFactory.kt @@ -1,5 +1,6 @@ package co.makerflow.intellijplugin.panels +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow @@ -11,6 +12,11 @@ class TasksToolWindowFactory : ToolWindowFactory, DumbAware { val tasksPanel = TasksPanel() val content = contentManager.factory.createContent(tasksPanel, null, true) contentManager.addContent(content) - toolWindow.setTitleActions(listOf(tasksPanel.reloadAction)) + toolWindow.setTitleActions( + listOf( + tasksPanel.reloadAction, + ActionManager.getInstance().getAction("co.makerflow.intellijplugin.actions.tasks.AddCustomTaskAction") + ) + ) } } diff --git a/src/main/kotlin/co/makerflow/intellijplugin/services/TasksService.kt b/src/main/kotlin/co/makerflow/intellijplugin/services/TasksService.kt index baff03b..7df6409 100644 --- a/src/main/kotlin/co/makerflow/intellijplugin/services/TasksService.kt +++ b/src/main/kotlin/co/makerflow/intellijplugin/services/TasksService.kt @@ -2,17 +2,22 @@ package co.makerflow.intellijplugin.services import co.makerflow.client.apis.TasksApi import co.makerflow.client.infrastructure.ApiClient +import co.makerflow.client.models.CustomTask +import co.makerflow.client.models.CustomTaskTodo import co.makerflow.client.models.MarkDoneRequest import co.makerflow.client.models.TypedTodo import co.makerflow.intellijplugin.settings.SettingsState import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.util.messages.Topic import com.squareup.moshi.JsonEncodingException import io.ktor.client.call.NoTransformationFoundException import io.ktor.serialization.JsonConvertException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +private const val SOURCE_TYPE = "jetbrains" + @Service class TasksService { @@ -33,7 +38,7 @@ class TasksService { launch { @Suppress("TooGenericExceptionCaught") try { - val res = tasksApi().getTodos("jetbrains") + val res = tasksApi().getTodos(SOURCE_TYPE) if (res.success) { tasks = res.body() } @@ -73,11 +78,41 @@ class TasksService { return coroutineScope { var success = false launch { - val response = tasksApi().markDone("jetbrains", MarkDoneRequest(todo = task, done)) + val response = tasksApi().markDone(SOURCE_TYPE, MarkDoneRequest(todo = task, done)) success = response.success }.join() return@coroutineScope success } } + suspend fun addTask(task: String): CustomTaskTodo? { + return coroutineScope { + var ret: CustomTaskTodo? = null + launch { + val data = CustomTask(title = task, done = false) + val response = tasksApi().addCustomTask(SOURCE_TYPE, data) + if (response.success) { + val customTask = response.body().data + ret = customTask?.let { + CustomTaskTodo(task = it) + }?.apply { + done = false + sourceType = TypedTodo.SourceType.makerflow + createdAt = customTask.createdAt + type = "makerflow" + } + } + }.join() + return@coroutineScope ret + } + } + + companion object { + val TASKS_ADDED_TOPIC: Topic = Topic.create("Tasks", TaskAdded::class.java) + } + +} + +fun interface TaskAdded { + fun taskAdded(task: CustomTaskTodo) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 9aac5c4..5a3b87b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -46,6 +46,9 @@ class="co.makerflow.intellijplugin.actions.flowmode.SeventyFiveMinutesFlowModeAction" text="Flow Mode (75 Minutes)" description="Begin a timed Flow Mode session for 75 minutes"/> +