mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[github] Ability to edit PR reviewers, assignees, labels
This commit is contained in:
@@ -173,6 +173,11 @@ public final class HttpRequests {
|
||||
return new RequestBuilderImpl(url, connection -> ((HttpURLConnection)connection).setRequestMethod("DELETE"));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RequestBuilder delete(@NotNull String url, @Nullable String contentType) {
|
||||
return requestWithBody(url, "DELETE", contentType, null);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RequestBuilder post(@NotNull String url, @Nullable String contentType) {
|
||||
return requestWithBody(url, "POST", contentType, null);
|
||||
@@ -525,8 +530,8 @@ public final class HttpRequests {
|
||||
|
||||
if (builder.myThrowStatusCodeException) {
|
||||
URLConnection connection = request.myConnection;
|
||||
if (connection instanceof HttpURLConnection && ((HttpURLConnection)connection).getRequestMethod().equals("POST")) {
|
||||
// getResponseCode is not checked on connect for POST, because write must be performed before read
|
||||
if (connection.getDoOutput()) {
|
||||
// getResponseCode is not checked on connect, because write must be performed before read
|
||||
HttpURLConnection urlConnection = (HttpURLConnection)connection;
|
||||
int responseCode = urlConnection.getResponseCode();
|
||||
if (responseCode >= 400) {
|
||||
@@ -596,12 +601,13 @@ public final class HttpRequests {
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpURLConnection httpURLConnection = (HttpURLConnection)connection;
|
||||
String method = httpURLConnection.getRequestMethod();
|
||||
if (method.equals("POST") || method.equals("PUT")) {
|
||||
if (connection.getDoOutput()) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
HttpURLConnection httpURLConnection = (HttpURLConnection)connection;
|
||||
String method = httpURLConnection.getRequestMethod();
|
||||
|
||||
LOG.assertTrue(method.equals("GET") || method.equals("HEAD") || method.equals("DELETE"),
|
||||
"'" + method + "' not supported; please use GET, HEAD, DELETE, PUT or POST");
|
||||
|
||||
|
||||
@@ -48,9 +48,6 @@ sealed class GithubApiRequest<T>(val url: String) {
|
||||
inline fun <reified T> json(url: String, acceptMimeType: String? = null): Get<T> =
|
||||
Json(url, T::class.java, acceptMimeType)
|
||||
|
||||
inline fun <reified T> jsonList(url: String, acceptMimeType: String? = null): Get<List<T>> =
|
||||
JsonList(url, T::class.java, acceptMimeType)
|
||||
|
||||
inline fun <reified T> jsonPage(url: String, acceptMimeType: String? = null): Get<GithubResponsePage<T>> =
|
||||
JsonPage(url, T::class.java, acceptMimeType)
|
||||
|
||||
@@ -133,6 +130,8 @@ sealed class GithubApiRequest<T>(val url: String) {
|
||||
|
||||
companion object {
|
||||
inline fun <reified T> json(url: String, body: Any? = null): Put<T> = Json(url, body, T::class.java)
|
||||
|
||||
inline fun <reified T> jsonList(url: String, body: Any): Put<List<T>> = JsonList(url, T::class.java, body)
|
||||
}
|
||||
|
||||
open class Json<T>(url: String, body: Any?, clazz: Class<T>) : Put<T>(body?.let { GithubApiContentHelper.toJson(it) },
|
||||
@@ -143,6 +142,17 @@ sealed class GithubApiRequest<T>(val url: String) {
|
||||
|
||||
override fun extractResult(response: GithubApiResponse): T = parseJsonResponse(response, typeToken)
|
||||
}
|
||||
|
||||
open class JsonList<T>(url: String, clazz: Class<T>, body: Any?)
|
||||
: Put<List<T>>(body?.let { GithubApiContentHelper.toJson(it) },
|
||||
GithubApiContentHelper.JSON_MIME_TYPE,
|
||||
url,
|
||||
GithubApiContentHelper.V3_JSON_MIME_TYPE) {
|
||||
|
||||
private val typeToken = TypeToken.getParameterized(List::class.java, clazz) as TypeToken<List<T>>
|
||||
|
||||
override fun extractResult(response: GithubApiResponse): List<T> = parseJsonResponse(response, typeToken)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Patch<T> @JvmOverloads constructor(override val body: String,
|
||||
@@ -163,10 +173,23 @@ sealed class GithubApiRequest<T>(val url: String) {
|
||||
}
|
||||
}
|
||||
|
||||
open class Delete(url: String) : GithubApiRequest<Unit>(url) {
|
||||
override val acceptMimeType: String? = null
|
||||
abstract class Delete<T> @JvmOverloads constructor(override val body: String?,
|
||||
override val bodyMimeType: String,
|
||||
url: String,
|
||||
override val acceptMimeType: String? = null) : GithubApiRequest.WithBody<T>(url) {
|
||||
|
||||
override fun extractResult(response: GithubApiResponse) {}
|
||||
companion object {
|
||||
inline fun <reified T> json(url: String, body: Any? = null): Delete<T> = Json(url, body, T::class.java)
|
||||
}
|
||||
|
||||
open class Json<T>(url: String, body: Any? = null, clazz: Class<T>) : Delete<T>(body?.let { GithubApiContentHelper.toJson(it) },
|
||||
GithubApiContentHelper.JSON_MIME_TYPE,
|
||||
url,
|
||||
GithubApiContentHelper.V3_JSON_MIME_TYPE) {
|
||||
private val typeToken = TypeToken.get(clazz)
|
||||
|
||||
override fun extractResult(response: GithubApiResponse): T = parseJsonResponse(response, typeToken)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -149,7 +149,9 @@ sealed class GithubApiRequestExecutor {
|
||||
is GithubApiRequest.Put -> HttpRequests.put(request.url, request.bodyMimeType)
|
||||
is GithubApiRequest.Patch -> HttpRequests.patch(request.url, request.bodyMimeType)
|
||||
is GithubApiRequest.Head -> HttpRequests.head(request.url)
|
||||
is GithubApiRequest.Delete -> HttpRequests.delete(request.url)
|
||||
is GithubApiRequest.Delete -> {
|
||||
if (request.body == null) HttpRequests.delete(request.url) else HttpRequests.delete(request.url, request.bodyMimeType)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("${request.javaClass} is not supported")
|
||||
}
|
||||
.connectTimeout(githubSettings.connectionTimeout)
|
||||
|
||||
@@ -100,7 +100,7 @@ object GithubApiRequests {
|
||||
delete(getUrl(server, urlSuffix, "/$username/$repoName")).withOperationName("delete repository $username/$repoName")
|
||||
|
||||
@JvmStatic
|
||||
fun delete(url: String) = Delete(url).withOperationName("delete repository at $url")
|
||||
fun delete(url: String) = Delete.json<Unit>(url).withOperationName("delete repository at $url")
|
||||
|
||||
object Branches : Entity("/branches") {
|
||||
@JvmStatic
|
||||
@@ -136,8 +136,50 @@ object GithubApiRequests {
|
||||
fun get(url: String) = Get.jsonPage<GithubRepo>(url).withOperationName("get forks")
|
||||
}
|
||||
|
||||
object Assignees : Entity("/assignees") {
|
||||
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, username: String, repoName: String) =
|
||||
GithubApiPagesLoader.Request(get(server, username, repoName), ::get)
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(server: GithubServerPath, username: String, repoName: String, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, getQuery(pagination?.toString().orEmpty())))
|
||||
|
||||
@JvmStatic
|
||||
fun get(url: String) = Get.jsonPage<GithubUser>(url).withOperationName("get assignees")
|
||||
}
|
||||
|
||||
object Labels : Entity("/labels") {
|
||||
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, username: String, repoName: String) =
|
||||
GithubApiPagesLoader.Request(get(server, username, repoName), ::get)
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(server: GithubServerPath, username: String, repoName: String, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, getQuery(pagination?.toString().orEmpty())))
|
||||
|
||||
@JvmStatic
|
||||
fun get(url: String) = Get.jsonPage<GithubIssueLabel>(url).withOperationName("get assignees")
|
||||
}
|
||||
|
||||
object Collaborators : Entity("/collaborators") {
|
||||
|
||||
@JvmStatic
|
||||
fun pages(server: GithubServerPath, username: String, repoName: String) =
|
||||
GithubApiPagesLoader.Request(get(server, username, repoName), ::get)
|
||||
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(server: GithubServerPath, username: String, repoName: String, pagination: GithubRequestPagination? = null) =
|
||||
get(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, getQuery(pagination?.toString().orEmpty())))
|
||||
|
||||
@JvmStatic
|
||||
fun get(url: String) = Get.jsonPage<GithubUserWithPermissions>(url).withOperationName("get collaborators")
|
||||
|
||||
@JvmStatic
|
||||
fun add(server: GithubServerPath, username: String, repoName: String, collaborator: String) =
|
||||
Put.json<Any>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, "/", collaborator))
|
||||
@@ -180,6 +222,11 @@ object GithubApiRequests {
|
||||
Patch.json<GithubIssue>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, "/", id),
|
||||
GithubChangeIssueStateRequest(if (open) "open" else "closed"))
|
||||
|
||||
@JvmStatic
|
||||
fun updateAssignees(server: GithubServerPath, username: String, repoName: String, id: String, assignees: Collection<String>) =
|
||||
Patch.json<GithubIssue>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", urlSuffix, "/", id),
|
||||
GithubAssigneesCollectionRequest(assignees))
|
||||
|
||||
object Comments : Entity("/comments") {
|
||||
@JvmStatic
|
||||
fun create(server: GithubServerPath, username: String, repoName: String, issueId: String, body: String) =
|
||||
@@ -203,6 +250,13 @@ object GithubApiRequests {
|
||||
fun get(url: String) = Get.jsonPage<GithubIssueCommentWithHtml>(url, GithubApiContentHelper.V3_HTML_JSON_MIME_TYPE)
|
||||
.withOperationName("get comments for issue")
|
||||
}
|
||||
|
||||
object Labels : Entity("/labels") {
|
||||
@JvmStatic
|
||||
fun replace(server: GithubServerPath, username: String, repoName: String, issueId: String, labels: Collection<String>) =
|
||||
Put.jsonList<GithubIssueLabel>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", Issues.urlSuffix, "/", issueId, urlSuffix),
|
||||
GithubLabelsCollectionRequest(labels))
|
||||
}
|
||||
}
|
||||
|
||||
object PullRequests : Entity("/pulls") {
|
||||
@@ -265,6 +319,18 @@ object GithubApiRequests {
|
||||
.withOperationName("rebase and merge pull request ${pullRequest.number}")
|
||||
|
||||
private fun getMergeUrl(pullRequest: GithubPullRequest) = pullRequest.url + "/merge"
|
||||
|
||||
object Reviewers : Entity("/requested_reviewers") {
|
||||
@JvmStatic
|
||||
fun add(server: GithubServerPath, username: String, repoName: String, number: Long, reviewers: Collection<String>) =
|
||||
Post.json<Unit>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", PullRequests.urlSuffix, "/$number", urlSuffix),
|
||||
GithubReviewersCollectionRequest(reviewers, listOf<String>()))
|
||||
|
||||
@JvmStatic
|
||||
fun remove(server: GithubServerPath, username: String, repoName: String, number: Long, reviewers: Collection<String>) =
|
||||
Delete.json<Unit>(getUrl(server, Repos.urlSuffix, "/$username/$repoName", PullRequests.urlSuffix, "/$number", urlSuffix),
|
||||
GithubReviewersCollectionRequest(reviewers, listOf<String>()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +346,7 @@ object GithubApiRequests {
|
||||
.withOperationName("get gist $id")
|
||||
|
||||
@JvmStatic
|
||||
fun delete(server: GithubServerPath, id: String) = Delete(getUrl(server, urlSuffix, "/$id"))
|
||||
fun delete(server: GithubServerPath, id: String) = Delete.json<Unit>(getUrl(server, urlSuffix, "/$id"))
|
||||
.withOperationName("delete gist $id")
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.jetbrains.io.mandatory.RestModel;
|
||||
@RestModel
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public class GithubIssueLabel {
|
||||
private Long id;
|
||||
@Mandatory private Long id;
|
||||
private String nodeId;
|
||||
private String url;
|
||||
@Mandatory private String name;
|
||||
@@ -35,4 +35,21 @@ public class GithubIssueLabel {
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof GithubIssueLabel)) return false;
|
||||
|
||||
GithubIssueLabel label = (GithubIssueLabel)o;
|
||||
|
||||
if (!id.equals(label.id)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.api.data;
|
||||
|
||||
import org.jetbrains.io.mandatory.Mandatory;
|
||||
import org.jetbrains.io.mandatory.RestModel;
|
||||
|
||||
@RestModel
|
||||
public class GithubPermissions {
|
||||
@Mandatory private Boolean admin;
|
||||
@Mandatory private Boolean pull;
|
||||
@Mandatory private Boolean push;
|
||||
|
||||
public boolean isAdmin() {
|
||||
return admin;
|
||||
}
|
||||
|
||||
public boolean isPull() {
|
||||
return pull;
|
||||
}
|
||||
|
||||
public boolean isPush() {
|
||||
return push;
|
||||
}
|
||||
}
|
||||
+2
-22
@@ -5,33 +5,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.io.mandatory.Mandatory;
|
||||
import org.jetbrains.io.mandatory.RestModel;
|
||||
|
||||
//example/GithubRepoWithPermissions.json
|
||||
@RestModel
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public class GithubRepoWithPermissions extends GithubRepo {
|
||||
@Mandatory private Permissions permissions;
|
||||
@Mandatory private GithubPermissions permissions;
|
||||
|
||||
@NotNull
|
||||
public Permissions getPermissions() {
|
||||
public GithubPermissions getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
@RestModel
|
||||
public static class Permissions {
|
||||
@Mandatory private Boolean admin;
|
||||
@Mandatory private Boolean pull;
|
||||
@Mandatory private Boolean push;
|
||||
|
||||
public boolean isAdmin() {
|
||||
return admin;
|
||||
}
|
||||
|
||||
public boolean isPull() {
|
||||
return pull;
|
||||
}
|
||||
|
||||
public boolean isPush() {
|
||||
return push;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.api.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.io.mandatory.Mandatory;
|
||||
import org.jetbrains.io.mandatory.RestModel;
|
||||
|
||||
@RestModel
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public class GithubUserWithPermissions extends GithubUser {
|
||||
@Mandatory private GithubPermissions permissions;
|
||||
|
||||
@NotNull
|
||||
public GithubPermissions getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.api.requests;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
|
||||
public class GithubAssigneesCollectionRequest {
|
||||
@NotNull private final Collection<String> assignees;
|
||||
|
||||
public GithubAssigneesCollectionRequest(@NotNull Collection<String> assignees) {
|
||||
this.assignees = assignees;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.api.requests;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
|
||||
public class GithubLabelsCollectionRequest {
|
||||
@NotNull private final Collection<String> labels;
|
||||
|
||||
public GithubLabelsCollectionRequest(@NotNull Collection<String> labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.api.requests;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"})
|
||||
public class GithubReviewersCollectionRequest {
|
||||
@NotNull private final Collection<String> reviewers;
|
||||
@NotNull private final Collection<String> team_reviewers;
|
||||
|
||||
public GithubReviewersCollectionRequest(@NotNull Collection<String> reviewers,
|
||||
@NotNull Collection<String> team_reviewers) {
|
||||
this.reviewers = reviewers;
|
||||
this.team_reviewers = team_reviewers;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -25,6 +25,7 @@ import org.jetbrains.plugins.github.pullrequest.config.GithubPullRequestsProject
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTrackerImpl
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsMetadataServiceImpl
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsSecurityServiceImpl
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsStateServiceImpl
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.GithubPullRequestChangesComponent
|
||||
@@ -73,13 +74,16 @@ internal class GithubPullRequestsComponentFactory(private val project: Project,
|
||||
private val securityService = GithubPullRequestsSecurityServiceImpl(sharedProjectSettings, accountDetails, repoDetails)
|
||||
private val busyStateTracker = GithubPullRequestsBusyStateTrackerImpl()
|
||||
|
||||
private val metadataService = GithubPullRequestsMetadataServiceImpl(project, progressManager, dataLoader, busyStateTracker,
|
||||
requestExecutor,
|
||||
avatarIconsProviderFactory, account.server, repoDetails.fullPath)
|
||||
private val stateService = GithubPullRequestsStateServiceImpl(project, progressManager, dataLoader, busyStateTracker, requestExecutor,
|
||||
account.server, repoDetails.fullPath)
|
||||
|
||||
private val changes = GithubPullRequestChangesComponent(project, pullRequestUiSettings).apply {
|
||||
diffAction.registerCustomShortcutSet(this@GithubPullRequestsComponent, this@GithubPullRequestsComponent)
|
||||
}
|
||||
private val details = GithubPullRequestDetailsComponent(dataLoader, securityService, busyStateTracker, stateService,
|
||||
private val details = GithubPullRequestDetailsComponent(dataLoader, securityService, busyStateTracker, metadataService, stateService,
|
||||
avatarIconsProviderFactory)
|
||||
private val preview = GithubPullRequestPreviewComponent(changes, details)
|
||||
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.pullrequest.data.service
|
||||
|
||||
import org.jetbrains.annotations.CalledInAwt
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface GithubPullRequestsMetadataService {
|
||||
@CalledInAwt
|
||||
fun adjustReviewers(pullRequest: Long, parentComponent: JComponent)
|
||||
|
||||
@CalledInAwt
|
||||
fun adjustAssignees(pullRequest: Long, parentComponent: JComponent)
|
||||
|
||||
@CalledInAwt
|
||||
fun adjustLabels(pullRequest: Long, parentComponent: JComponent)
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.pullrequest.data.service
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.popup.JBPopupListener
|
||||
import com.intellij.openapi.ui.popup.LightweightWindowEvent
|
||||
import com.intellij.openapi.ui.popup.PopupChooserBuilder
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.ui.CollectionListModel
|
||||
import com.intellij.ui.ColorUtil
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.util.ui.ColorIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.StatusText
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequestExecutor
|
||||
import org.jetbrains.plugins.github.api.GithubApiRequests
|
||||
import org.jetbrains.plugins.github.api.GithubFullPath
|
||||
import org.jetbrains.plugins.github.api.GithubServerPath
|
||||
import org.jetbrains.plugins.github.api.data.GithubIssueLabel
|
||||
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed
|
||||
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
|
||||
import org.jetbrains.plugins.github.api.data.GithubUser
|
||||
import org.jetbrains.plugins.github.api.util.GithubApiPagesLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTracker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
|
||||
import org.jetbrains.plugins.github.util.*
|
||||
import java.awt.Component
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Function
|
||||
import javax.swing.*
|
||||
|
||||
class GithubPullRequestsMetadataServiceImpl internal constructor(private val project: Project,
|
||||
private val progressManager: ProgressManager,
|
||||
private val dataLoader: GithubPullRequestsDataLoader,
|
||||
private val busyStateTracker: GithubPullRequestsBusyStateTracker,
|
||||
private val requestExecutor: GithubApiRequestExecutor,
|
||||
private val avatarIconsProviderFactory: CachingGithubAvatarIconsProvider.Factory,
|
||||
private val serverPath: GithubServerPath,
|
||||
private val repoPath: GithubFullPath)
|
||||
: GithubPullRequestsMetadataService {
|
||||
|
||||
private val repoCollaboratorsWithPushAccess: List<GithubUser> by lazy {
|
||||
GithubApiPagesLoader
|
||||
.loadAll(requestExecutor, progressManager.progressIndicator,
|
||||
GithubApiRequests.Repos.Collaborators.pages(serverPath, repoPath.user, repoPath.repository))
|
||||
.filter { it.permissions.isPush }
|
||||
}
|
||||
|
||||
private val repoIssuesAssignees: List<GithubUser> by lazy {
|
||||
GithubApiPagesLoader.loadAll(requestExecutor, progressManager.progressIndicator,
|
||||
GithubApiRequests.Repos.Assignees.pages(serverPath, repoPath.user, repoPath.repository))
|
||||
}
|
||||
|
||||
private val repoIssuesLabels: List<GithubIssueLabel> by lazy {
|
||||
GithubApiPagesLoader.loadAll(requestExecutor, progressManager.progressIndicator,
|
||||
GithubApiRequests.Repos.Labels.pages(serverPath, repoPath.user, repoPath.repository))
|
||||
}
|
||||
|
||||
override fun adjustReviewers(pullRequest: Long, parentComponent: JComponent) {
|
||||
showUsersChooser(pullRequest, "Reviewers", parentComponent,
|
||||
{ _, details -> repoCollaboratorsWithPushAccess.filter { details.user != it } }) { it.requestedReviewers }
|
||||
.handleOnEdt(getAdjustmentHandler(pullRequest, "reviewer") { delta, indicator ->
|
||||
if (delta.removedItems.isNotEmpty()) {
|
||||
indicator.text2 = "Removing reviewers"
|
||||
requestExecutor.execute(indicator,
|
||||
GithubApiRequests.Repos.PullRequests.Reviewers
|
||||
.remove(serverPath, repoPath.user, repoPath.repository, pullRequest,
|
||||
delta.removedItems.map { it.login }))
|
||||
}
|
||||
if (delta.newItems.isNotEmpty()) {
|
||||
indicator.text2 = "Adding reviewers"
|
||||
requestExecutor.execute(indicator,
|
||||
GithubApiRequests.Repos.PullRequests.Reviewers
|
||||
.add(serverPath, repoPath.user, repoPath.repository, pullRequest,
|
||||
delta.newItems.map { it.login }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun adjustAssignees(pullRequest: Long, parentComponent: JComponent) {
|
||||
showUsersChooser(pullRequest, "Assignees", parentComponent, { _, _ -> repoIssuesAssignees }) { it.assignees }
|
||||
.handleOnEdt(getAdjustmentHandler(pullRequest, "assignee") { delta, indicator ->
|
||||
requestExecutor.execute(indicator,
|
||||
GithubApiRequests.Repos.Issues
|
||||
.updateAssignees(serverPath, repoPath.user, repoPath.repository, pullRequest.toString(),
|
||||
delta.newCollection.map { it.login }))
|
||||
})
|
||||
}
|
||||
|
||||
override fun adjustLabels(pullRequest: Long, parentComponent: JComponent) {
|
||||
showChooser(pullRequest, "Labels", parentComponent,
|
||||
{ SelectionListCellRenderer.Labels() }, { it.name },
|
||||
{ _, _ -> repoIssuesLabels }, { it.labels.orEmpty() })
|
||||
.handleOnEdt(getAdjustmentHandler(pullRequest, "label") { delta, indicator ->
|
||||
requestExecutor.execute(indicator,
|
||||
GithubApiRequests.Repos.Issues.Labels
|
||||
.replace(serverPath, repoPath.user, repoPath.repository, pullRequest.toString(),
|
||||
delta.newCollection.map { it.name }))
|
||||
})
|
||||
}
|
||||
|
||||
private fun showUsersChooser(pullRequest: Long,
|
||||
popupTitle: String,
|
||||
parentComponent: JComponent,
|
||||
availableListProvider: (ProgressIndicator, GithubPullRequestDetailed) -> List<GithubUser>,
|
||||
currentListExtractor: (GithubPullRequestDetailed) -> List<GithubUser>)
|
||||
: CompletableFuture<CollectionDelta<GithubUser>> {
|
||||
return showChooser(pullRequest, popupTitle, parentComponent, { list ->
|
||||
val avatarIconsProvider = avatarIconsProviderFactory.create(JBUI.uiIntValue("GitHub.Avatars.Size", 20), list)
|
||||
SelectionListCellRenderer.Users(avatarIconsProvider)
|
||||
}, { it.login }, availableListProvider, currentListExtractor)
|
||||
}
|
||||
|
||||
private fun <T> showChooser(pullRequest: Long,
|
||||
popupTitle: String,
|
||||
parentComponent: JComponent,
|
||||
cellRendererFactory: (JList<SelectableWrapper<T>>) -> ListCellRenderer<SelectableWrapper<T>>,
|
||||
speedSearchNamer: (T) -> String,
|
||||
availableListProvider: (ProgressIndicator, GithubPullRequestDetailed) -> List<T>,
|
||||
currentListExtractor: (GithubPullRequestDetailed) -> List<T>)
|
||||
: CompletableFuture<CollectionDelta<T>> {
|
||||
|
||||
val listModel = CollectionListModel<SelectableWrapper<T>>()
|
||||
val list = JBList<SelectableWrapper<T>>(listModel)
|
||||
|
||||
val builder = PopupChooserBuilder<SelectableWrapper<T>>(list)
|
||||
.setTitle(popupTitle)
|
||||
.setResizable(true)
|
||||
.setMovable(true)
|
||||
.setNamerForFiltering { speedSearchNamer(it.value) }
|
||||
.setAutoSelectIfEmpty(false)
|
||||
.setCloseOnEnter(false)
|
||||
.setRenderer(cellRendererFactory(list))
|
||||
.setItemsChosenCallback {
|
||||
for (item in it) {
|
||||
item.selected = !item.selected
|
||||
}
|
||||
list.repaint()
|
||||
}
|
||||
val popup = builder.createPopup()
|
||||
|
||||
val updater = builder.backgroundUpdater
|
||||
updater.paintBusy(true)
|
||||
list.emptyText.text = "Loading..."
|
||||
|
||||
var originalSelection: Set<T> = setOf()
|
||||
|
||||
val progressIndicator = EmptyProgressIndicator()
|
||||
Disposer.register(popup, Disposable { progressIndicator.cancel() })
|
||||
|
||||
val loadingFuture = GithubAsyncUtil
|
||||
.futureOfMutable { dataLoader.getDataProvider(pullRequest).detailsRequest }
|
||||
.thenComposeAsync(Function { details: GithubPullRequestDetailedWithHtml ->
|
||||
originalSelection = currentListExtractor(details).toHashSet()
|
||||
progressManager.submitBackgroundTask(project, "Load List Of Possibilities", true, progressIndicator) {
|
||||
availableListProvider(it, details)
|
||||
}
|
||||
})
|
||||
.thenAcceptAsync(Consumer { possibilities ->
|
||||
listModel.replaceAll(possibilities
|
||||
.map { SelectableWrapper(it, originalSelection.contains(it)) }
|
||||
.sortedBy { !it.selected })
|
||||
|
||||
updater.paintBusy(false)
|
||||
list.emptyText.text = StatusText.DEFAULT_EMPTY_TEXT
|
||||
|
||||
popup.pack(true, true)
|
||||
}, EDT_EXECUTOR)
|
||||
.exceptionally {
|
||||
updater.paintBusy(false)
|
||||
list.emptyText.clear()
|
||||
list.emptyText.appendText("Can't load the list", SimpleTextAttributes.ERROR_ATTRIBUTES)
|
||||
list.emptyText.appendSecondaryText(it.message.orEmpty(), SimpleTextAttributes.ERROR_ATTRIBUTES, null)
|
||||
throw it
|
||||
}
|
||||
|
||||
Disposer.register(popup, Disposable {
|
||||
loadingFuture.cancel(true)
|
||||
})
|
||||
|
||||
val result = CompletableFuture<CollectionDelta<T>>()
|
||||
popup.addListener(object : JBPopupListener {
|
||||
override fun onClosed(event: LightweightWindowEvent) {
|
||||
if (!loadingFuture.isDone || loadingFuture.isCancelled || loadingFuture.isCompletedExceptionally) {
|
||||
result.cancel(true)
|
||||
return
|
||||
}
|
||||
|
||||
val selected = listModel.items.filter { it.selected }.map { it.value }
|
||||
result.complete(CollectionDelta(originalSelection, selected))
|
||||
}
|
||||
})
|
||||
popup.showUnderneathOf(parentComponent)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun <T> getAdjustmentHandler(pullRequest: Long,
|
||||
entityName: String,
|
||||
adjuster: (CollectionDelta<T>, ProgressIndicator) -> Unit): (CollectionDelta<T>?, Throwable?) -> Unit {
|
||||
return handler@{ delta, error ->
|
||||
if (error != null) {
|
||||
if (!GithubAsyncUtil.isCancellation(error))
|
||||
GithubNotifications.showError(project, "Failed to adjust list of ${StringUtil.pluralize(entityName)}", error)
|
||||
return@handler
|
||||
}
|
||||
if (delta == null || delta.isEmpty) {
|
||||
return@handler
|
||||
}
|
||||
|
||||
if (!busyStateTracker.acquire(pullRequest)) return@handler
|
||||
progressManager.run(object : Task.Backgroundable(project, "Adjusting List Of ${StringUtil.pluralize(entityName).capitalize()}",
|
||||
true) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
adjuster(delta, indicator)
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
GithubNotifications.showError(project, "Failed to adjust list of ${StringUtil.pluralize(entityName)}", error)
|
||||
}
|
||||
|
||||
override fun onFinished() {
|
||||
busyStateTracker.release(pullRequest)
|
||||
dataLoader.reloadDetails(pullRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private data class SelectableWrapper<T>(val value: T, var selected: Boolean = false)
|
||||
|
||||
private sealed class SelectionListCellRenderer<T>
|
||||
: ListCellRenderer<SelectableWrapper<T>>, BorderLayoutPanel() {
|
||||
|
||||
private val mainLabel = JLabel()
|
||||
private val checkIconLabel = JLabel()
|
||||
|
||||
init {
|
||||
checkIconLabel.iconTextGap = JBUI.scale(UIUtil.DEFAULT_VGAP)
|
||||
|
||||
addToLeft(checkIconLabel)
|
||||
addToCenter(mainLabel)
|
||||
|
||||
border = JBUI.Borders.empty(2)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(list: JList<out SelectableWrapper<T>>,
|
||||
value: SelectableWrapper<T>,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean): Component {
|
||||
font = list.font
|
||||
foreground = UIUtil.getListForeground(isSelected, list.hasFocus())
|
||||
background = UIUtil.getListBackground(isSelected, list.hasFocus())
|
||||
|
||||
mainLabel.foreground = foreground
|
||||
mainLabel.font = font
|
||||
|
||||
mainLabel.text = getText(value.value)
|
||||
mainLabel.icon = getIcon(value.value)
|
||||
|
||||
if (value.selected) {
|
||||
checkIconLabel.icon = AllIcons.Actions.Checked
|
||||
checkIconLabel.border = JBUI.Borders.empty(0, 4)
|
||||
}
|
||||
else {
|
||||
checkIconLabel.icon = null
|
||||
checkIconLabel.border = JBUI.Borders.empty(0, 10)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
abstract fun getText(value: T): String
|
||||
abstract fun getIcon(value: T): Icon
|
||||
|
||||
class Users(private val iconsProvider: CachingGithubAvatarIconsProvider)
|
||||
: SelectionListCellRenderer<GithubUser>() {
|
||||
|
||||
override fun getText(value: GithubUser) = value.login
|
||||
override fun getIcon(value: GithubUser) = iconsProvider.getIcon(value)
|
||||
|
||||
}
|
||||
|
||||
class Labels
|
||||
: SelectionListCellRenderer<GithubIssueLabel>() {
|
||||
|
||||
override fun getText(value: GithubIssueLabel) = value.name
|
||||
override fun getIcon(value: GithubIssueLabel) = ColorIcon(16, ColorUtil.fromHex(value.color))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class CollectionDelta<out T>(oldCollection: Collection<T>, val newCollection: Collection<T>) {
|
||||
val newItems: Collection<T> = newCollection - oldCollection
|
||||
val removedItems: Collection<T> = oldCollection - newCollection
|
||||
|
||||
val isEmpty = newItems.isEmpty() && removedItems.isEmpty()
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -14,6 +14,7 @@ import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailedWithHtml
|
||||
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTracker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsDataLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsMetadataService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsSecurityService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsStateService
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.details.GithubPullRequestDetailsModel
|
||||
@@ -23,12 +24,13 @@ import java.awt.BorderLayout
|
||||
internal class GithubPullRequestDetailsComponent(private val dataLoader: GithubPullRequestsDataLoader,
|
||||
securityService: GithubPullRequestsSecurityService,
|
||||
busyStateTracker: GithubPullRequestsBusyStateTracker,
|
||||
metadataService: GithubPullRequestsMetadataService,
|
||||
stateService: GithubPullRequestsStateService,
|
||||
iconProviderFactory: CachingGithubAvatarIconsProvider.Factory)
|
||||
: GithubDataLoadingComponent<GithubPullRequestDetailedWithHtml>(), Disposable {
|
||||
|
||||
private val detailsModel = GithubPullRequestDetailsModel()
|
||||
private val detailsPanel = GithubPullRequestDetailsPanel(detailsModel, securityService, busyStateTracker, stateService,
|
||||
private val detailsPanel = GithubPullRequestDetailsPanel(detailsModel, securityService, busyStateTracker, metadataService, stateService,
|
||||
iconProviderFactory)
|
||||
|
||||
private val loadingPanel = JBLoadingPanel(BorderLayout(), this, ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS).apply {
|
||||
@@ -62,6 +64,8 @@ internal class GithubPullRequestDetailsComponent(private val dataLoader: GithubP
|
||||
dataLoader.reloadDetails(result.number)
|
||||
}
|
||||
}
|
||||
validate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
override fun handleError(error: Throwable) {
|
||||
|
||||
+3
-1
@@ -17,6 +17,7 @@ import net.miginfocom.layout.LC
|
||||
import net.miginfocom.swing.MigLayout
|
||||
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTracker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsMetadataService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsSecurityService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsStateService
|
||||
import java.awt.Graphics
|
||||
@@ -28,6 +29,7 @@ import javax.swing.JPanel
|
||||
internal class GithubPullRequestDetailsPanel(model: GithubPullRequestDetailsModel,
|
||||
securityService: GithubPullRequestsSecurityService,
|
||||
busyStateTracker: GithubPullRequestsBusyStateTracker,
|
||||
metadataService: GithubPullRequestsMetadataService,
|
||||
stateService: GithubPullRequestsStateService,
|
||||
iconProviderFactory: CachingGithubAvatarIconsProvider.Factory)
|
||||
: JPanel(), ComponentWithEmptyText, Disposable {
|
||||
@@ -37,7 +39,7 @@ internal class GithubPullRequestDetailsPanel(model: GithubPullRequestDetailsMode
|
||||
}
|
||||
private val iconsProvider = iconProviderFactory.create(JBValue.UIInteger("Profile.Icon.Size", 20), this)
|
||||
|
||||
private val metaPanel = GithubPullRequestMetadataPanel(model, iconsProvider).apply {
|
||||
private val metaPanel = GithubPullRequestMetadataPanel(model, securityService, busyStateTracker, metadataService, iconsProvider).apply {
|
||||
border = JBUI.Borders.empty(4, 8, 4, 8)
|
||||
}
|
||||
private val descriptionPanel = GithubPullRequestDescriptionPanel(model).apply {
|
||||
|
||||
+52
-8
@@ -2,6 +2,7 @@
|
||||
package org.jetbrains.plugins.github.pullrequest.ui.details
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
@@ -9,21 +10,28 @@ import net.miginfocom.layout.CC
|
||||
import net.miginfocom.layout.LC
|
||||
import net.miginfocom.swing.MigLayout
|
||||
import org.jetbrains.plugins.github.api.data.GithubIssueLabel
|
||||
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed
|
||||
import org.jetbrains.plugins.github.api.data.GithubUser
|
||||
import org.jetbrains.plugins.github.pullrequest.avatars.CachingGithubAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTracker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsMetadataService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsSecurityService
|
||||
import org.jetbrains.plugins.github.util.GithubUIUtil
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
internal class GithubPullRequestMetadataPanel(private val model: GithubPullRequestDetailsModel,
|
||||
private val securityService: GithubPullRequestsSecurityService,
|
||||
private val busyStateTracker: GithubPullRequestsBusyStateTracker,
|
||||
private val metadataService: GithubPullRequestsMetadataService,
|
||||
private val iconsProvider: CachingGithubAvatarIconsProvider)
|
||||
: JPanel(), Disposable {
|
||||
|
||||
private val directionPanel = GithubPullRequestDirectionPanel()
|
||||
private val reviewersHandle = LabeledListPanelHandle.create("No Reviewers", "Reviewers:", ::createUserLabel)
|
||||
private val assigneesHandle = LabeledListPanelHandle.create("Unassigned", "Assignees:", ::createUserLabel)
|
||||
private val labelsHandle = LabeledListPanelHandle.create("No Labels", "Labels:", ::createLabelLabel)
|
||||
private val reviewersHandle = ReviewersListPanelHandle()
|
||||
private val assigneesHandle = AssigneesListPanelHandle()
|
||||
private val labelsHandle = LabelsListPanelHandle()
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
@@ -42,10 +50,11 @@ internal class GithubPullRequestMetadataPanel(private val model: GithubPullReque
|
||||
|
||||
model.addDetailsChangedListener(this) {
|
||||
directionPanel.direction = model.details?.let { it.head to it.base }
|
||||
reviewersHandle.list = model.details?.requestedReviewers
|
||||
assigneesHandle.list = model.details?.assignees
|
||||
labelsHandle.list = model.details?.labels
|
||||
}
|
||||
|
||||
Disposer.register(this, reviewersHandle)
|
||||
Disposer.register(this, assigneesHandle)
|
||||
Disposer.register(this, labelsHandle)
|
||||
}
|
||||
|
||||
private fun addListPanel(handle: LabeledListPanelHandle<*>) {
|
||||
@@ -53,13 +62,48 @@ internal class GithubPullRequestMetadataPanel(private val model: GithubPullReque
|
||||
add(handle.panel, CC().minWidth("0").growX().pushX().wrap())
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
|
||||
private inner class ReviewersListPanelHandle
|
||||
: LabeledListPanelHandle<GithubUser>(model, securityService, busyStateTracker, "No Reviewers", "Reviewers:") {
|
||||
override fun extractItems(details: GithubPullRequestDetailed): List<GithubUser> = details.requestedReviewers
|
||||
|
||||
override fun getItemComponent(item: GithubUser) = createUserLabel(item)
|
||||
|
||||
override fun editList() {
|
||||
model.details?.run { metadataService.adjustReviewers(number, editButton) }
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AssigneesListPanelHandle
|
||||
: LabeledListPanelHandle<GithubUser>(model, securityService, busyStateTracker, "Unassigned", "Assignees:") {
|
||||
|
||||
override fun extractItems(details: GithubPullRequestDetailed): List<GithubUser> = details.assignees
|
||||
|
||||
override fun getItemComponent(item: GithubUser) = createUserLabel(item)
|
||||
|
||||
override fun editList() {
|
||||
model.details?.run { metadataService.adjustAssignees(number, editButton) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUserLabel(user: GithubUser) = JLabel(user.login, iconsProvider.getIcon(user), SwingConstants.LEFT).apply {
|
||||
border = JBUI.Borders.empty(UIUtil.DEFAULT_VGAP, UIUtil.DEFAULT_HGAP / 2, UIUtil.DEFAULT_VGAP, UIUtil.DEFAULT_HGAP / 2)
|
||||
}
|
||||
|
||||
private inner class LabelsListPanelHandle
|
||||
: LabeledListPanelHandle<GithubIssueLabel>(model, securityService, busyStateTracker, "No Labels", "Labels:") {
|
||||
|
||||
override fun extractItems(details: GithubPullRequestDetailed): List<GithubIssueLabel>? = details.labels
|
||||
|
||||
override fun getItemComponent(item: GithubIssueLabel) = createLabelLabel(item)
|
||||
|
||||
override fun editList() {
|
||||
model.details?.run { metadataService.adjustLabels(number, editButton) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLabelLabel(label: GithubIssueLabel) = Wrapper(GithubUIUtil.createIssueLabelLabel(label)).apply {
|
||||
border = JBUI.Borders.empty(UIUtil.DEFAULT_VGAP + 1, UIUtil.DEFAULT_HGAP / 2, UIUtil.DEFAULT_VGAP + 2, UIUtil.DEFAULT_HGAP / 2)
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
+68
-11
@@ -1,39 +1,96 @@
|
||||
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package org.jetbrains.plugins.github.pullrequest.ui.details
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.ui.popup.IconButton
|
||||
import com.intellij.ui.InplaceButton
|
||||
import com.intellij.ui.components.panels.NonOpaquePanel
|
||||
import com.intellij.util.IconUtil
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.JBUI.Panels.simplePanel
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.jetbrains.plugins.github.api.data.GithubPullRequestDetailed
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GithubPullRequestsBusyStateTracker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GithubPullRequestsSecurityService
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.WrapLayout
|
||||
import org.jetbrains.plugins.github.util.GithubUtil.Delegates.equalVetoingObservable
|
||||
import java.awt.Cursor
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.event.ActionListener
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
|
||||
internal abstract class LabeledListPanelHandle<T>(emptyText: String, notEmptyText: String) {
|
||||
internal abstract class LabeledListPanelHandle<T>(private val model: GithubPullRequestDetailsModel,
|
||||
private val securityService: GithubPullRequestsSecurityService,
|
||||
private val busyStateTracker: GithubPullRequestsBusyStateTracker,
|
||||
emptyText: String, notEmptyText: String)
|
||||
: Disposable {
|
||||
|
||||
val label = JLabel().apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
border = JBUI.Borders.empty(UIUtil.DEFAULT_VGAP + 2, 0, UIUtil.DEFAULT_VGAP + 2, UIUtil.DEFAULT_HGAP / 2)
|
||||
}
|
||||
val panel = NonOpaquePanel(WrapLayout(FlowLayout.LEADING, 0, 0))
|
||||
|
||||
var list: List<T>? by equalVetoingObservable<List<T>?>(null) { newList ->
|
||||
protected val editButton = InplaceButton(IconButton(null,
|
||||
resizeSquareIcon(AllIcons.General.Inline_edit),
|
||||
resizeSquareIcon(AllIcons.General.Inline_edit_hovered)),
|
||||
ActionListener { (::editList)() }).apply {
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
isVisible = securityService.isCurrentUserWithPushAccess()
|
||||
}
|
||||
|
||||
private fun resizeSquareIcon(icon: Icon): Icon {
|
||||
val scale = 20f / icon.iconHeight
|
||||
return IconUtil.scale(icon, editButton, scale)
|
||||
}
|
||||
|
||||
private var list: List<T>? by equalVetoingObservable<List<T>?>(null) { newList ->
|
||||
label.text = newList?.let { if (it.isEmpty()) emptyText else notEmptyText }
|
||||
label.isVisible = newList != null
|
||||
|
||||
panel.removeAll()
|
||||
panel.isVisible = newList != null
|
||||
if (newList != null) for (item in newList) {
|
||||
panel.add(getListItemComponent(item))
|
||||
if (newList != null) {
|
||||
if (newList.isEmpty()) {
|
||||
panel.add(editButton)
|
||||
}
|
||||
else {
|
||||
for (item in newList.dropLast(1)) {
|
||||
panel.add(getListItemComponent(item))
|
||||
}
|
||||
panel.add(getListItemComponent(newList.last(), true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getListItemComponent(item: T): JComponent
|
||||
|
||||
companion object {
|
||||
inline fun <T> create(emptyText: String, notEmptyText: String, crossinline componentProvider: (T) -> JComponent) =
|
||||
object : LabeledListPanelHandle<T>(emptyText, notEmptyText) {
|
||||
override fun getListItemComponent(item: T) = componentProvider(item)
|
||||
}
|
||||
init {
|
||||
model.addDetailsChangedListener(this) {
|
||||
list = model.details?.let(::extractItems)
|
||||
updateButton()
|
||||
}
|
||||
busyStateTracker.addPullRequestBusyStateListener(this) {
|
||||
updateButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButton() {
|
||||
editButton.isEnabled = !(model.details?.number?.let(busyStateTracker::isBusy) ?: true)
|
||||
}
|
||||
|
||||
private fun getListItemComponent(item: T, last: Boolean = false) =
|
||||
if (!last) getItemComponent(item)
|
||||
else simplePanel(getItemComponent(item)).addToRight(editButton).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
abstract fun extractItems(details: GithubPullRequestDetailed): List<T>?
|
||||
|
||||
abstract fun getItemComponent(item: T): JComponent
|
||||
|
||||
abstract fun editList()
|
||||
|
||||
override fun dispose() {}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ package org.jetbrains.plugins.github.util
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.Project
|
||||
import java.util.concurrent.*
|
||||
import java.util.function.BiFunction
|
||||
|
||||
@@ -33,6 +36,23 @@ object GithubAsyncUtil {
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T> futureOfMutable(futureSupplier: () -> CompletableFuture<T>): CompletableFuture<T> {
|
||||
val result = CompletableFuture<T>()
|
||||
handleToOtherIfCancelled(futureSupplier, result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun <T> handleToOtherIfCancelled(futureSupplier: () -> CompletableFuture<T>, other: CompletableFuture<T>) {
|
||||
futureSupplier().handle { result, error ->
|
||||
if (result != null) other.complete(result)
|
||||
if (error != null) {
|
||||
if (isCancellation(error)) handleToOtherIfCancelled(futureSupplier, other)
|
||||
other.completeExceptionally(error.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isCancellation(error: Throwable): Boolean {
|
||||
return error is ProcessCanceledException
|
||||
|| error is CancellationException
|
||||
@@ -41,6 +61,28 @@ object GithubAsyncUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> ProgressManager.submitBackgroundTask(project: Project,
|
||||
title: String,
|
||||
canBeCancelled: Boolean,
|
||||
progressIndicator: ProgressIndicator,
|
||||
process: (indicator: ProgressIndicator) -> T): CompletableFuture<T> {
|
||||
val future = CompletableFuture<T>()
|
||||
runProcessWithProgressAsynchronously(object : Task.Backgroundable(project, title, canBeCancelled) {
|
||||
override fun run(indicator: ProgressIndicator) {
|
||||
future.complete(process(indicator))
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
future.cancel(true)
|
||||
}
|
||||
|
||||
override fun onThrowable(error: Throwable) {
|
||||
future.completeExceptionally(error)
|
||||
}
|
||||
}, progressIndicator)
|
||||
return future
|
||||
}
|
||||
|
||||
fun <T> CompletableFuture<T>.handleOnEdt(handler: (T?, Throwable?) -> Unit): CompletableFuture<Unit> =
|
||||
handleAsync(BiFunction<T?, Throwable?, Unit> { result: T?, error: Throwable? ->
|
||||
handler(result, error)
|
||||
|
||||
@@ -41,7 +41,7 @@ object GithubUIUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun createIssueLabelLabel(label: GithubIssueLabel): JBLabel = JBLabel(" ${label.name} ", UIUtil.ComponentStyle.MINI).apply {
|
||||
fun createIssueLabelLabel(label: GithubIssueLabel): JBLabel = JBLabel(" ${label.name} ", UIUtil.ComponentStyle.SMALL).apply {
|
||||
val apiColor = ColorUtil.fromHex(label.color)
|
||||
background = JBColor(apiColor, ColorUtil.darker(apiColor, 3))
|
||||
foreground = computeForeground(background)
|
||||
|
||||
Reference in New Issue
Block a user