diff --git a/platform/lang-impl/src/com/intellij/ide/actions/TemplateKindCombo.java b/platform/lang-impl/src/com/intellij/ide/actions/TemplateKindCombo.java index 5a76d4bd52c7..46521faff6c5 100644 --- a/platform/lang-impl/src/com/intellij/ide/actions/TemplateKindCombo.java +++ b/platform/lang-impl/src/com/intellij/ide/actions/TemplateKindCombo.java @@ -99,8 +99,8 @@ public class TemplateKindCombo extends ComboboxWithBrowseButton { } private void scrollBy(int delta) { - if (delta == 0) return; final int size = getComboBox().getModel().getSize(); + if (delta == 0 || size == 0) return; int next = getComboBox().getSelectedIndex() + delta; if (next < 0 || next >= size) { if (!UISettings.getInstance().CYCLE_SCROLLING) { diff --git a/plugins/tasks/tasks-api/src/com/intellij/tasks/CustomTaskState.java b/plugins/tasks/tasks-api/src/com/intellij/tasks/CustomTaskState.java new file mode 100644 index 000000000000..0fced6c7af52 --- /dev/null +++ b/plugins/tasks/tasks-api/src/com/intellij/tasks/CustomTaskState.java @@ -0,0 +1,98 @@ +package com.intellij.tasks; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class CustomTaskState { + private String myId; + private String myPresentableName; + private boolean myPredefined; + + /** + * For serialization purposes only. + */ + public CustomTaskState() { + } + + public CustomTaskState(@NotNull String id, @NotNull String name) { + myId = id; + myPresentableName = name; + } + + @NotNull + public String getId() { + return myId; + } + + /** + * For serialization purposes only. + */ + public void setId(String id) { + myId = id; + } + + @NotNull + public String getPresentableName() { + return myPresentableName; + } + + /** + * For serialization purposes only. + */ + public void setPresentableName(@NotNull String name) { + myPresentableName = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CustomTaskState)) return false; + + final CustomTaskState state = (CustomTaskState)o; + + return myId.equals(state.myId); + } + + @Override + public int hashCode() { + return myId.hashCode(); + } + + @NotNull + public static CustomTaskState fromPredefined(@NotNull TaskState state) { + final CustomTaskState result = new CustomTaskState(state.name(), state.getPresentableName()); + result.setPredefined(true); + return result; + } + + @Nullable + public TaskState asPredefined() { + if (isPredefined()) { + try { + return TaskState.valueOf(getId()); + } + catch (IllegalArgumentException ignored) { + } + } + return null; + } + + private boolean isPredefined() { + return myPredefined; + } + + /** + * For serialization purposes only. + */ + public void setPredefined(boolean predefined) { + myPredefined = predefined; + } + + @Override + public String toString() { + return "CustomTaskState(id='" + myId + '\'' + ", name='" + myPresentableName + '\'' + ", myPredefined=" + myPredefined + ')'; + } +} diff --git a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepository.java b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepository.java index 416a7158eb26..c6e270fdef4e 100644 --- a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepository.java +++ b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepository.java @@ -18,6 +18,10 @@ package com.intellij.tasks; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.impl.CancellableRunnable; +import com.intellij.tasks.impl.BaseRepository; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.annotations.Attribute; import com.intellij.util.xmlb.annotations.Tag; import com.intellij.util.xmlb.annotations.Transient; @@ -26,6 +30,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import java.util.Set; import java.util.concurrent.Callable; /** @@ -34,7 +39,7 @@ import java.util.concurrent.Callable; * * @author Dmitry Avdeev * @see TaskRepositoryType - * @see com.intellij.tasks.impl.BaseRepository + * @see BaseRepository */ @Tag("server") public abstract class TaskRepository { @@ -109,7 +114,7 @@ public abstract class TaskRepository { /** * Returns an object that can test connection. - * {@link com.intellij.openapi.vcs.impl.CancellableRunnable#cancel()} should cancel the process. + * {@link CancellableRunnable#cancel()} should cancel the process. * * @return null if not supported */ @@ -164,6 +169,48 @@ public abstract class TaskRepository { return getIssues(query, offset, limit, withClosed); } + /** + * Retrieve states available for task from server. One of these states will be passed later to {@link #setTaskState(Task, TaskState)}. + * @param task task to update + * @return set of available states + */ + @NotNull + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + //noinspection unchecked + return ContainerUtil.map2Set(getRepositoryType().getPossibleTaskStates(), new Function() { + @Override + public CustomTaskState fun(TaskState state) { + return CustomTaskState.fromPredefined(state); + } + }); + } + + /** + * Remember state used when opening task most recently. + * @param state preferred task state + */ + public abstract void setPreferredOpenTaskState(@Nullable CustomTaskState state); + + /** + * Task state that was used last time when opening task. + * @return preferred task state + */ + @Nullable + public abstract CustomTaskState getPreferredOpenTaskState(); + + /** + * Remember state used when closing task most recently. + * @param state preferred task state + */ + public abstract void setPreferredCloseTaskState(@Nullable CustomTaskState state); + + /** + * Task state that was used last time when closing task. + * @return preferred task state + */ + @Nullable + public abstract CustomTaskState getPreferredCloseTaskState(); + /** * @param id task ID. Don't forget to define {@link #extractId(String)}, if your server uses not PROJECT-123 format for task IDs. * @return found task or {@code null} otherwise. Basically you should return {@code null} on e.g. 404 error and throw exception with @@ -179,19 +226,39 @@ public abstract class TaskRepository { @Nullable public abstract String extractId(@NotNull String taskName); + /** - * Update state of the task on server. Don't forget to add {@link #STATE_UPDATING} in {@link #getFeatures()} and - * supported states in {@link TaskRepositoryType#getPossibleTaskStates()}. - * - * @param task issue to update - * @param state new state of the issue - * @see com.intellij.tasks.TaskRepositoryType#getPossibleTaskStates() - * @see com.intellij.tasks.TaskRepository#getFeatures() + * @deprecated Use {@link #setTaskState(Task, CustomTaskState)} instead. */ + @Deprecated public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { throw new UnsupportedOperationException("Setting task to state " + state + " is not supported"); } + /** + * Update state of the task on server. It's guaranteed that only issues returned by {@link #getAvailableTaskStates(Task)} + * will be passed here. + *

+ * Don't forget to add {@link #STATE_UPDATING} in {@link #getFeatures()} and supported states in {@link #getAvailableTaskStates(Task)}. + * + * @param task issue to update + * @param state new state of the issue + * @see TaskRepositoryType#getPossibleTaskStates() + * @see TaskRepository#getFeatures() + */ + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { + TaskState predefinedState = null; + try { + predefinedState = TaskState.valueOf(state.getId()); + } + catch (IllegalArgumentException ignored) { + } + if (predefinedState != null) { + setTaskState(task, predefinedState); + } + } + + // for serialization public TaskRepository() { myType = null; diff --git a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepositoryType.java b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepositoryType.java index f348f4264c22..d4575cf3eb92 100644 --- a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepositoryType.java +++ b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepositoryType.java @@ -67,8 +67,10 @@ public abstract class TaskRepositoryType implements Ta public abstract Class getRepositoryClass(); /** - * @return states that can be set by {@link TaskRepository#setTaskState(Task, TaskState)} + * @return states that can be set by {@link TaskRepository#setTaskState(Task, CustomTaskState)} + * @deprecated Use {@link TaskRepository#getAvailableTaskStates(Task)} instead. */ + @Deprecated public EnumSet getPossibleTaskStates() { return EnumSet.noneOf(TaskState.class); } diff --git a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskState.java b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskState.java index c0c0a2628b9d..79c504a44a60 100644 --- a/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskState.java +++ b/plugins/tasks/tasks-api/src/com/intellij/tasks/TaskState.java @@ -15,16 +15,30 @@ */ package com.intellij.tasks; +import org.jetbrains.annotations.NotNull; + /** + * Predefined common task states were used before {@link CustomTaskState} was introduced. + * * @author Dmitry Avdeev */ public enum TaskState { + SUBMITTED("Submitted"), + OPEN("Open"), + IN_PROGRESS("In Progress"), + REOPENED("Reopened"), + RESOLVED("Resolved"), - SUBMITTED, - OPEN, - IN_PROGRESS, - REOPENED, - RESOLVED, + OTHER("Other"); - OTHER + private String myPresentableName; + + TaskState(@NotNull String presentableName) { + myPresentableName = presentableName; + } + + @NotNull + public String getPresentableName() { + return myPresentableName; + } } diff --git a/plugins/tasks/tasks-api/src/com/intellij/tasks/impl/BaseRepository.java b/plugins/tasks/tasks-api/src/com/intellij/tasks/impl/BaseRepository.java index 8423736c8c10..0f67d4bae909 100644 --- a/plugins/tasks/tasks-api/src/com/intellij/tasks/impl/BaseRepository.java +++ b/plugins/tasks/tasks-api/src/com/intellij/tasks/impl/BaseRepository.java @@ -18,6 +18,7 @@ package com.intellij.tasks.impl; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.PasswordUtil; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.TaskRepository; import com.intellij.tasks.TaskRepositoryType; import com.intellij.util.xmlb.annotations.Tag; @@ -40,6 +41,8 @@ public abstract class BaseRepository extends TaskRepository { protected boolean myUseProxy; protected boolean myUseHttpAuthentication; protected boolean myLoginAnonymously; + protected CustomTaskState myPreferredOpenTaskState; + private CustomTaskState myPreferredCloseTaskState; public BaseRepository(TaskRepositoryType type) { super(type); @@ -135,6 +138,28 @@ public abstract class BaseRepository extends TaskRepository { myLoginAnonymously = loginAnonymously; } + @Override + public void setPreferredOpenTaskState(@Nullable CustomTaskState state) { + myPreferredOpenTaskState = state; + } + + @Nullable + @Override + public CustomTaskState getPreferredOpenTaskState() { + return myPreferredOpenTaskState; + } + + @Override + public void setPreferredCloseTaskState(@Nullable CustomTaskState state) { + myPreferredCloseTaskState = state; + } + + @Nullable + @Override + public CustomTaskState getPreferredCloseTaskState() { + return myPreferredCloseTaskState; + } + @Nullable public String extractId(@NotNull String taskName) { Matcher matcher = PATTERN.matcher(taskName); diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRemoteApi.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRemoteApi.java index 8ee9c97512a1..7b760769a0e3 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRemoteApi.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRemoteApi.java @@ -1,8 +1,8 @@ package com.intellij.tasks.jira; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; import com.intellij.tasks.Task; -import com.intellij.tasks.TaskState; import com.intellij.tasks.jira.rest.api2.JiraRestApi2; import com.intellij.tasks.jira.rest.api20alpha1.JiraRestApi20Alpha1; import com.intellij.tasks.jira.soap.JiraLegacyApi; @@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Set; /** * Because of the number of available remote interfaces in JIRA, {@link JiraRepository} delegates @@ -29,7 +30,10 @@ public abstract class JiraRemoteApi { @Nullable public abstract Task findTask(@NotNull String key) throws Exception; - public abstract void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception; + @NotNull + public abstract Set getAvailableTaskStates(@NotNull Task task) throws Exception; + + public abstract void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception; public abstract void updateTimeSpend(@NotNull LocalTask task, @NotNull String timeSpent, String comment) throws Exception; @@ -80,6 +84,5 @@ public abstract class JiraRemoteApi { public String getVersionName() { return myVersionName; } - } } diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRepository.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRepository.java index 820bb22426e0..ec0e612f4e81 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRepository.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRepository.java @@ -22,10 +22,10 @@ import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.io.StreamUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; import com.intellij.tasks.Task; import com.intellij.tasks.TaskBundle; -import com.intellij.tasks.TaskState; import com.intellij.tasks.impl.BaseRepositoryImpl; import com.intellij.tasks.impl.gson.GsonUtil; import com.intellij.tasks.jira.rest.JiraRestApi; @@ -44,10 +44,7 @@ import org.jetbrains.annotations.Nullable; import java.io.InputStream; import java.net.URL; -import java.util.Collections; -import java.util.Hashtable; -import java.util.List; -import java.util.Vector; +import java.util.*; import java.util.regex.Pattern; /** @@ -352,10 +349,16 @@ public class JiraRepository extends BaseRepositoryImpl { } @Override - public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { myApiVersion.setTaskState(task, state); } + @NotNull + @Override + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + return myApiVersion.getAvailableTaskStates(task); + } + public void setSearchQuery(String searchQuery) { mySearchQuery = searchQuery; } @@ -371,8 +374,6 @@ public class JiraRepository extends BaseRepositoryImpl { /** * Used to preserve discovered API version for the next initialization. - * - * @return */ @SuppressWarnings("UnusedDeclaration") @Nullable diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/JiraRestApi.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/JiraRestApi.java index 502eed083e74..78919e390d5f 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/JiraRestApi.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/JiraRestApi.java @@ -2,8 +2,8 @@ package com.intellij.tasks.jira.rest; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.Task; -import com.intellij.tasks.TaskState; import com.intellij.tasks.jira.JiraRemoteApi; import com.intellij.tasks.jira.JiraRepository; import com.intellij.tasks.jira.JiraVersion; @@ -99,19 +99,16 @@ public abstract class JiraRestApi extends JiraRemoteApi { protected abstract JiraIssue parseIssue(String response); @Override - public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { String requestBody = getRequestForStateTransition(state); LOG.debug(String.format("Transition: %s -> %s, request: %s", task.getState(), state, requestBody)); - if (requestBody == null) { - return; - } PostMethod method = new PostMethod(myRepository.getRestUrl("issue", task.getId(), "transitions")); method.setRequestEntity(createJsonEntity(requestBody)); myRepository.executeMethod(method); } @Nullable - protected abstract String getRequestForStateTransition(@NotNull TaskState state); + protected abstract String getRequestForStateTransition(@NotNull CustomTaskState state); protected static RequestEntity createJsonEntity(String requestBody) { try { diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/JiraRestApi2.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/JiraRestApi2.java index 0c9b647ee720..b83607481fac 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/JiraRestApi2.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/JiraRestApi2.java @@ -3,11 +3,13 @@ package com.intellij.tasks.jira.rest.api2; import com.google.gson.reflect.TypeToken; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; -import com.intellij.tasks.TaskState; +import com.intellij.tasks.Task; import com.intellij.tasks.jira.JiraRepository; import com.intellij.tasks.jira.rest.JiraRestApi; import com.intellij.tasks.jira.rest.api2.model.JiraIssueApi2; +import com.intellij.tasks.jira.rest.api2.model.JiraTransitionsWrapperApi2; import com.intellij.tasks.jira.rest.model.JiraIssue; import com.intellij.tasks.jira.rest.model.JiraResponseWrapper; import org.apache.commons.httpclient.methods.GetMethod; @@ -18,6 +20,7 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * This REST API version is used in JIRA 5.1.8 and above (including JIRA 6.x.x). @@ -63,21 +66,31 @@ public class JiraRestApi2 extends JiraRestApi { return JiraRepository.GSON.fromJson(response, JiraIssueApi2.class); } - @Nullable + @NotNull @Override - protected String getRequestForStateTransition(@NotNull TaskState state) { - // REST API 2.0 require double quotes both around field names and values (even numbers) - switch (state) { - case IN_PROGRESS: - return "{\"transition\": {\"id\": \"4\"}}"; - case RESOLVED: - // 5 for "Resolved", 2 for "Closed" - return "{\"transition\": {\"id\": \"5\"}, \"fields\": {\"resolution\": {\"name\": \"Fixed\"}}}"; - case REOPENED: - return "{\"transition\": {\"id\": \"3\"}}"; - default: - return null; + protected String getRequestForStateTransition(@NotNull CustomTaskState state) { + assert StringUtil.isNotEmpty(state.getId()); + final String stateId = state.getId(); + final int index = stateId.indexOf(':'); + if (index >= 0) { + return "{" + + " \"transition\": {\"id\": \"" + stateId.substring(0, index) + "\"}, " + + " \"fields\": {\"resolution\": {\"name\": \"" + stateId.substring(index + 1) + "\"}}" + + "}"; } + else { + return "{\"transition\": {\"id\": \"" + stateId + "\"}}"; + } + } + + @NotNull + @Override + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + final GetMethod method = new GetMethod(myRepository.getRestUrl("issue", task.getId(), "transitions")); + method.setQueryString("expand=transitions.fields"); + final String response = myRepository.executeMethod(method); + final JiraTransitionsWrapperApi2 wrapper = JiraRepository.GSON.fromJson(response, JiraTransitionsWrapperApi2.class); + return wrapper.getTransitions(); } @Override diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/model/JiraTransitionsWrapperApi2.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/model/JiraTransitionsWrapperApi2.java new file mode 100644 index 000000000000..a1534ab81e8a --- /dev/null +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/model/JiraTransitionsWrapperApi2.java @@ -0,0 +1,68 @@ +package com.intellij.tasks.jira.rest.api2.model; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.CustomTaskState; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * @author Mikhail Golubev + */ +@SuppressWarnings({"unused", "FieldMayBeFinal"}) +public class JiraTransitionsWrapperApi2 { + private static final Logger LOG = Logger.getInstance(JiraTransitionsWrapperApi2.class); + private List transitions = Collections.emptyList(); + + static class JiraTransition { + private int id; + private String name; + @SerializedName("to") + private JiraTaskState target; + private JsonObject fields; + + static class JiraTaskState { + private int id; + private String name; + } + } + + @NotNull + public Set getTransitions() { + final Set result = new LinkedHashSet(); + nextTransition: + for (JiraTransition transition : transitions) { + final String stateName = transition.target.name; + final List resolutions = new ArrayList(); + if (transition.fields != null) { + for (Map.Entry field : transition.fields.entrySet()) { + final String fieldName = field.getKey(); + final JsonObject fieldInfo = field.getValue().getAsJsonObject(); + if (fieldInfo.get("required").getAsBoolean()) { + if (fieldName.equals("resolution")) { + for (JsonElement allowedValue : fieldInfo.getAsJsonArray("allowedValues")) { + resolutions.add(allowedValue.getAsJsonObject().get("name").getAsString()); + } + } + else { + LOG.info("Unknown required field '" + fieldName + "' for transition '" + stateName + "'"); + continue nextTransition; + } + } + } + } + if (resolutions.isEmpty()) { + result.add(new CustomTaskState(String.valueOf(transition.id), stateName)); + } + else { + for (String resolution : resolutions) { + result.add(new CustomTaskState(transition.id + ":" + resolution, stateName + " (" + resolution + ")")); + } + } + } + return result; + } +} diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api20alpha1/JiraRestApi20Alpha1.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api20alpha1/JiraRestApi20Alpha1.java index 7de17945ac31..bc70d239ce73 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api20alpha1/JiraRestApi20Alpha1.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api20alpha1/JiraRestApi20Alpha1.java @@ -2,9 +2,10 @@ package com.intellij.tasks.jira.rest.api20alpha1; import com.google.gson.reflect.TypeToken; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; +import com.intellij.tasks.Task; import com.intellij.tasks.TaskBundle; -import com.intellij.tasks.TaskState; import com.intellij.tasks.jira.JiraRepository; import com.intellij.tasks.jira.rest.JiraRestApi; import com.intellij.tasks.jira.rest.JiraRestTask; @@ -16,20 +17,35 @@ import org.jetbrains.annotations.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * This REST API is used in JIRA versions from 4.2 to 4.4. + * * @author Mikhail Golubev */ public class JiraRestApi20Alpha1 extends JiraRestApi { private static final Logger LOG = Logger.getInstance(JiraRestApi20Alpha1.class); - private static final Type ISSUES_WRAPPER_TYPE = new TypeToken>() { /* empty */ }.getType(); + private static final Type ISSUES_WRAPPER_TYPE = new TypeToken>() {/* empty */}.getType(); public JiraRestApi20Alpha1(JiraRepository repository) { super(repository); } + @NotNull + @Override + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + // REST API of JIRA 4.x for retrieving possible transitions is very limited: we can't fetch possible resolutions and + // names of transition destinations. So we have no other options than to hardcode them. + final HashSet result = new HashSet(); + result.add(new CustomTaskState("4", "In Progress")); + result.add(new CustomTaskState("5", "Resolved (Fixed)")); + result.add(new CustomTaskState("3", "Reopened")); + return result; + } + @Override protected JiraIssue parseIssue(String response) { return JiraRepository.GSON.fromJson(response, JiraIssueApi20Alpha1.class); @@ -57,20 +73,24 @@ public class JiraRestApi20Alpha1 extends JiraRestApi { @Nullable @Override - protected String getRequestForStateTransition(@NotNull TaskState state) { - switch (state) { - case IN_PROGRESS: - return "{\"transition\": \"4\"}"; - case RESOLVED: - // 5 for "Resolved", 2 for "Closed" - return "{\"transition\": \"5\", \"resolution\": \"Fixed\"}"; - case REOPENED: - return "{\"transition\": \"3\"}"; - default: - return null; + protected String getRequestForStateTransition(@NotNull CustomTaskState state) { + try { + switch (Integer.parseInt(state.getId())) { + case 4: // In Progress + return "{\"transition\": \"4\"}"; + case 5: // Resolved (2 for "Closed") + return "{\"transition\": \"5\", \"resolution\": \"Fixed\"}"; + case 3: // Reopened + return "{\"transition\": \"3\"}"; + } } + catch (NumberFormatException ignored) { + } + LOG.error("Unknown ID of predefined issue state: " + state.getId()); + return null; } + @Override public void updateTimeSpend(@NotNull LocalTask task, @NotNull String timeSpent, String comment) throws Exception { throw new Exception(TaskBundle.message("jira.failure.no.time.spent")); diff --git a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/soap/JiraLegacyApi.java b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/soap/JiraLegacyApi.java index e8cbee842cda..7f5ab27cc9a1 100644 --- a/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/soap/JiraLegacyApi.java +++ b/plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/soap/JiraLegacyApi.java @@ -1,10 +1,10 @@ package com.intellij.tasks.jira.soap; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; import com.intellij.tasks.Task; import com.intellij.tasks.TaskBundle; -import com.intellij.tasks.TaskState; import com.intellij.tasks.impl.TaskUtil; import com.intellij.tasks.jira.JiraRemoteApi; import com.intellij.tasks.jira.JiraRepository; @@ -19,7 +19,9 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collections; import java.util.List; +import java.util.Set; /** * Legacy integration restored due to IDEA-120595. @@ -102,8 +104,14 @@ public class JiraLegacyApi extends JiraRemoteApi { return ApiType.LEGACY; } + @NotNull @Override - public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + return Collections.emptySet(); + } + + @Override + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { throw new Exception(TaskBundle.message("jira.failure.no.state.update")); } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/TaskBundle.properties b/plugins/tasks/tasks-core/src/com/intellij/tasks/TaskBundle.properties index ce617cbf01ca..4d631b7c5d20 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/TaskBundle.properties +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/TaskBundle.properties @@ -23,3 +23,5 @@ youtrack.default.query=for: me sort by: updated #Unresolved bugzilla.failure.malformed.response=Cannot decode server response. Check that XML-RPC plugin is enabled. bugzilla.failure.no.version=Cannot find Bugzilla version. Check that URL ends with "xmlrpc.cgi". +## Trello +trello.failure.write.access.required=This action requires write access to your account. Please update authorization token in settings. diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskAction.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskAction.java index 6863eef78510..7bf2afa94b2f 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskAction.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskAction.java @@ -41,11 +41,13 @@ public class CloseTaskAction extends BaseTaskAction { LocalTask task = taskManager.getActiveTask(); CloseTaskDialog dialog = new CloseTaskDialog(project, task); if (dialog.showAndGet()) { - if (dialog.isCloseIssue()) { + final CustomTaskState taskState = dialog.getCloseIssueState(); + if (taskState != null) { try { TaskRepository repository = task.getRepository(); assert repository != null; - repository.setTaskState(task, TaskState.RESOLVED); + repository.setTaskState(task, taskState); + repository.setPreferredCloseTaskState(taskState); } catch (Exception e1) { Messages.showErrorDialog(project, e1.getMessage(), "Cannot Resolve Issue"); diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.form b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.form index 0c8600a9785b..7508f5a7dc34 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.form +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.form @@ -3,7 +3,7 @@ - + @@ -52,14 +52,6 @@ - - - - - - - - @@ -73,6 +65,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.java index fa9879c05fe0..73d14d23435a 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.java @@ -19,42 +19,51 @@ package com.intellij.tasks.actions; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.vcs.VcsType; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.LocalTask; import com.intellij.tasks.TaskManager; import com.intellij.tasks.TaskRepository; -import com.intellij.tasks.TaskState; import com.intellij.tasks.impl.TaskManagerImpl; +import com.intellij.tasks.impl.TaskStateCombo; import com.intellij.tasks.impl.TaskUtil; import com.intellij.ui.components.JBCheckBox; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; +import java.util.Collection; /** * @author Dmitry Avdeev */ public class CloseTaskDialog extends DialogWrapper { - + private final Project myProject; + private final LocalTask myTask; private JCheckBox myCommitChanges; - private JCheckBox myCloseIssue; private JPanel myPanel; private JLabel myTaskLabel; private JBCheckBox myMergeBranches; private JPanel myVcsPanel; + private JLabel myStateComboBoxLabel; + private TaskStateCombo myStateCombo; private final TaskManagerImpl myTaskManager; - public CloseTaskDialog(Project project, LocalTask task) { + public CloseTaskDialog(Project project, final LocalTask task) { super(project, false); + myProject = project; + myTask = task; setTitle("Close Task"); myTaskLabel.setText(TaskUtil.getTrimmedSummary(task)); myTaskLabel.setIcon(task.getIcon()); - TaskRepository repository = task.getRepository(); - boolean visible = task.isIssue() && TaskUtil.isStateSupported(repository, TaskState.RESOLVED); - myCloseIssue.setVisible(visible); + myStateComboBoxLabel.setLabelFor(myStateCombo); + if (!TaskStateCombo.isStateSupportedFor(task)) { + myStateComboBoxLabel.setVisible(false); + myStateCombo.setVisible(false); + } myTaskManager = (TaskManagerImpl)TaskManager.getManager(project); - myCloseIssue.setSelected(visible && myTaskManager.getState().closeIssue); if (myTaskManager.isVcsEnabled()) { myCommitChanges.setEnabled(!task.getChangeLists().isEmpty()); @@ -71,6 +80,11 @@ public class CloseTaskDialog extends DialogWrapper { else { myVcsPanel.setVisible(false); } + final JComponent preferredFocusedComponent = getPreferredFocusedComponent(); + if (preferredFocusedComponent != null) { + myStateCombo.registerUpDownAction(preferredFocusedComponent); + } + myStateCombo.scheduleUpdate(); init(); } @@ -78,8 +92,15 @@ public class CloseTaskDialog extends DialogWrapper { return myPanel; } - boolean isCloseIssue() { - return myCloseIssue.isSelected(); + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return myStateCombo.getComboBox(); + } + + @Nullable + CustomTaskState getCloseIssueState() { + return myStateCombo.getSelectedState(); } boolean isCommitChanges() { @@ -92,7 +113,6 @@ public class CloseTaskDialog extends DialogWrapper { @Override protected void doOKAction() { - myTaskManager.getState().closeIssue = isCloseIssue(); if (myCommitChanges.isEnabled()) { myTaskManager.getState().commitChanges = isCommitChanges(); } @@ -101,4 +121,14 @@ public class CloseTaskDialog extends DialogWrapper { } super.doOKAction(); } + + private void createUIComponents() { + myStateCombo = new TaskStateCombo(myProject, myTask) { + @Nullable + @Override + protected CustomTaskState getPreferredState(@NotNull TaskRepository repository, @NotNull Collection available) { + return repository.getPreferredCloseTaskState(); + } + }; + } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.form b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.form index fb7ab62bdaa8..64b57bd9d93b 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.form +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.form @@ -1,9 +1,9 @@

- + - + @@ -11,7 +11,7 @@ - + @@ -31,21 +31,12 @@ - - - - - - - - - - + @@ -76,7 +67,7 @@ - + @@ -124,7 +115,9 @@ - + + + @@ -137,6 +130,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.java index 66c3f80540c5..485161687d68 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.java @@ -30,6 +30,7 @@ import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.VcsTaskHandler; import com.intellij.tasks.*; import com.intellij.tasks.impl.TaskManagerImpl; +import com.intellij.tasks.impl.TaskStateCombo; import com.intellij.tasks.impl.TaskUtil; import com.intellij.ui.ColoredListCellRenderer; import com.intellij.ui.components.JBCheckBox; @@ -42,6 +43,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.Collection; /** * @author Dmitry Avdeev @@ -53,7 +55,6 @@ public class OpenTaskDialog extends DialogWrapper { private JPanel myPanel; @BindControl(value = "clearContext", instant = true) private JCheckBox myClearContext; - private JCheckBox myMarkAsInProgressBox; private JLabel myTaskNameLabel; private JPanel myVcsPanel; private JTextField myBranchName; @@ -62,6 +63,8 @@ public class OpenTaskDialog extends DialogWrapper { private JBCheckBox myCreateChangelist; private JBLabel myFromLabel; private ComboBox myBranchFrom; + private JLabel myTaskStateLabel; + private TaskStateCombo myTaskStateCombo; private final Project myProject; private final Task myTask; @@ -81,10 +84,10 @@ public class OpenTaskDialog extends DialogWrapper { binder.bindAnnotations(this); binder.reset(); - TaskRepository repository = task.getRepository(); - myMarkAsInProgressBox.setSelected(manager.getState().markAsInProgress); - if (!TaskUtil.isStateSupported(repository, TaskState.IN_PROGRESS)) { - myMarkAsInProgressBox.setVisible(false); + myTaskStateLabel.setLabelFor(myTaskStateCombo); + if (!TaskStateCombo.isStateSupportedFor(task)) { + myTaskStateLabel.setVisible(false); + myTaskStateCombo.setVisible(false); } TaskManagerImpl.Config state = taskManager.getState(); @@ -162,6 +165,11 @@ public class OpenTaskDialog extends DialogWrapper { myChangelistName.setText(taskManager.getChangelistName(task)); updateFields(true); } + final JComponent preferredFocusedComponent = getPreferredFocusedComponent(); + if (preferredFocusedComponent != null) { + myTaskStateCombo.registerUpDownAction(preferredFocusedComponent); + } + myTaskStateCombo.scheduleUpdate(); init(); } @@ -186,14 +194,15 @@ public class OpenTaskDialog extends DialogWrapper { public void createTask() { final TaskManagerImpl taskManager = (TaskManagerImpl)TaskManager.getManager(myProject); - taskManager.getState().markAsInProgress = isMarkAsInProgress(); taskManager.getState().createChangelist = myCreateChangelist.isSelected(); taskManager.getState().createBranch = myCreateBranch.isSelected(); - TaskRepository repository = myTask.getRepository(); - if (isMarkAsInProgress() && repository != null) { + final CustomTaskState taskState = myTaskStateCombo.getSelectedState(); + final TaskRepository repository = myTask.getRepository(); + if (repository != null && taskState != null) { try { - repository.setTaskState(myTask, TaskState.IN_PROGRESS); + repository.setTaskState(myTask, taskState); + repository.setPreferredOpenTaskState(taskState); } catch (Exception ex) { Messages.showErrorDialog(myProject, ex.getMessage(), "Cannot Set State For Issue"); @@ -252,10 +261,6 @@ public class OpenTaskDialog extends DialogWrapper { return myClearContext.isSelected(); } - private boolean isMarkAsInProgress() { - return myMarkAsInProgressBox.isSelected() && myMarkAsInProgressBox.isVisible(); - } - @NonNls protected String getDimensionServiceKey() { return "SimpleOpenTaskDialog"; @@ -270,11 +275,21 @@ public class OpenTaskDialog extends DialogWrapper { return myChangelistName; } else { - return null; + return myTaskStateCombo.getComboBox(); } } protected JComponent createCenterPanel() { return myPanel; } + + private void createUIComponents() { + myTaskStateCombo = new TaskStateCombo(myProject, myTask) { + @Nullable + @Override + protected CustomTaskState getPreferredState(@NotNull TaskRepository repository, @NotNull Collection available) { + return repository.getPreferredOpenTaskState(); + } + }; + } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskManagerImpl.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskManagerImpl.java index 3529dc1229e9..3a50bb8da92c 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskManagerImpl.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskManagerImpl.java @@ -1034,13 +1034,11 @@ public class TaskManagerImpl extends TaskManager implements ProjectComponent, Pe public boolean createBranch = true; // close task options - public boolean closeIssue = true; public boolean commitChanges = true; public boolean mergeBranch = true; public boolean saveContextOnCommit = true; public boolean trackContextForNewChangelist = false; - public boolean markAsInProgress = false; public String changelistNameFormat = "{id} {summary}"; public String branchNameFormat = "{id}"; diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskStateCombo.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskStateCombo.java new file mode 100644 index 000000000000..2126eece9902 --- /dev/null +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskStateCombo.java @@ -0,0 +1,170 @@ +package com.intellij.tasks.impl; + +import com.intellij.ide.actions.TemplateKindCombo; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Trinity; +import com.intellij.tasks.CustomTaskState; +import com.intellij.tasks.Task; +import com.intellij.tasks.TaskRepository; +import com.intellij.tasks.impl.TaskUiUtil.ComboBoxUpdater; +import com.intellij.ui.ListCellRendererWrapper; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.Function; +import com.intellij.util.PlatformIcons; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.Collection; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public abstract class TaskStateCombo extends JPanel { + private static final CustomTaskState DO_NOT_UPDATE_STATE = new CustomTaskState("", "-- do not update --"); + + public static boolean isStateSupportedFor(@Nullable Task task) { + if (task == null || !task.isIssue()) { + return false; + } + final TaskRepository repository = task.getRepository(); + return repository != null && repository.isSupported(TaskRepository.STATE_UPDATING); + } + + private final Project myProject; + private final Task myTask; + private final TemplateKindCombo myKindCombo = new TemplateKindCombo(); + + // For designer only + @SuppressWarnings("unused") + public TaskStateCombo() { + this(null, null); + } + + @SuppressWarnings({"GtkPreferredJComboBoxRenderer", "unchecked"}) + public TaskStateCombo(Project project, Task task) { + myProject = project; + myTask = task; + + final JBLabel hintButton = new JBLabel(); + hintButton.setIcon(PlatformIcons.UP_DOWN_ARROWS); + hintButton.setToolTipText("Pressing Up or Down arrows while in editor changes the state"); + final JComboBox comboBox = myKindCombo.getComboBox(); + comboBox.setPreferredSize(new Dimension(300, UIUtil.fixComboBoxHeight(comboBox.getPreferredSize().height))); + final ListCellRenderer defaultRenderer = comboBox.getRenderer(); + comboBox.setRenderer(new ListCellRenderer() { + @SuppressWarnings({"unchecked", "GtkPreferredJComboBoxRenderer"}) + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value == null) { + return new ListCellRendererWrapper() { + @Override + public void customize(JList list, CustomStateTrinityAdapter value, int index, boolean selected, boolean hasFocus) { + setText("-- no states available --"); + } + }.getListCellRendererComponent(list, null, index, isSelected, cellHasFocus); + } + return defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } + }); + + setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS)); + add(myKindCombo); + add(hintButton); + } + + public boolean scheduleUpdate() { + if (myProject != null && isStateSupportedFor(myTask)) { + final JComboBox comboBox = myKindCombo.getComboBox(); + final TaskRepository repository = myTask.getRepository(); + assert repository != null; + new ComboBoxUpdater(myProject, "Fetching available task states...", comboBox) { + @NotNull + @Override + protected List fetch(@NotNull ProgressIndicator indicator) throws Exception { + return CustomStateTrinityAdapter.wrapList(repository.getAvailableTaskStates(myTask)); + } + + @Nullable + @Override + public CustomStateTrinityAdapter getSelectedItem() { + final CustomTaskState state = getPreferredState(repository, CustomStateTrinityAdapter.unwrapList(myResult)); + return state != null ? new CustomStateTrinityAdapter(state) : null; + } + + @Nullable + @Override + public CustomStateTrinityAdapter getExtraItem() { + return new CustomStateTrinityAdapter(DO_NOT_UPDATE_STATE); + } + }.queue(); + return true; + } + return false; + } + + /** + * @return {@code null} if no state is available at the moment or special "do not update" state was selected + */ + @Nullable + public CustomTaskState getSelectedState() { + final CustomStateTrinityAdapter item = (CustomStateTrinityAdapter)myKindCombo.getComboBox().getSelectedItem(); + if (item == null) { + return null; + } + final CustomTaskState state = item.myState; + return state == DO_NOT_UPDATE_STATE ? null : state; + } + + public void registerUpDownAction(@NotNull JComponent focusable) { + myKindCombo.registerUpDownHint(focusable); + } + + @NotNull + public JComboBox getComboBox() { + return myKindCombo.getComboBox(); + } + + /** + * Determine what state should be initially selected in the list. + * @param repository task repository to communicate with + * @param available tasks states already downloaded from the repository + * @return task state to select + */ + @Nullable + protected abstract CustomTaskState getPreferredState(@NotNull TaskRepository repository, @NotNull Collection available); + + private static class CustomStateTrinityAdapter extends Trinity { + final CustomTaskState myState; + + public CustomStateTrinityAdapter(@NotNull CustomTaskState state) { + super(state.getPresentableName(), null, state.getId()); + myState = state; + } + + @NotNull + static List wrapList(@NotNull Collection states) { + return ContainerUtil.map(states, new Function() { + @Override + public CustomStateTrinityAdapter fun(CustomTaskState state) { + return new CustomStateTrinityAdapter(state); + } + }); + } + + @NotNull + static List unwrapList(@NotNull Collection wrapped) { + return ContainerUtil.map(wrapped, new Function() { + @Override + public CustomTaskState fun(CustomStateTrinityAdapter adapter) { + return adapter.myState; + } + }); + } + } +} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskUiUtil.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskUiUtil.java index d24ed38c9819..0f8e571a9e87 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskUiUtil.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskUiUtil.java @@ -6,14 +6,14 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.ComboBox; +import com.intellij.tasks.config.TaskRepositoryEditor; import com.intellij.ui.ListCellRendererWrapper; import com.intellij.util.ArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.List; +import java.util.Collection; /** * @author Mikhail Golubev @@ -33,13 +33,18 @@ public class TaskUiUtil { public abstract static class RemoteFetchTask extends Task.Backgroundable { protected T myResult; protected Exception myException; - private final ModalityState myModalityState = ModalityState.current(); + private final ModalityState myModalityState; /** * Should be called only from EDT, so current modality state can be captured. */ protected RemoteFetchTask(@Nullable Project project, @NotNull String title) { + this(project, title, ModalityState.current()); + } + + protected RemoteFetchTask(@Nullable Project project, @NotNull String title, @NotNull ModalityState modalityState) { super(project, title); + myModalityState = modalityState; } @Override @@ -55,7 +60,7 @@ public class TaskUiUtil { /** * {@link #onSuccess()} can't be used for this purpose, because it doesn't consider current modality state - * which will prevent UI updating in modal dialog (e.g. in {@link com.intellij.tasks.config.TaskRepositoryEditor}). + * which will prevent UI updating in modal dialog (e.g. in {@link TaskRepositoryEditor}). */ @Nullable @Override @@ -79,11 +84,11 @@ public class TaskUiUtil { * Auxiliary remote fetcher designed to simplify updating of combo boxes in repository editors, which is * indeed a rather common task. */ - public static abstract class ComboBoxUpdater extends RemoteFetchTask> { - protected final ComboBox myComboBox; + public static abstract class ComboBoxUpdater extends RemoteFetchTask> { + protected final JComboBox myComboBox; - public ComboBoxUpdater(@Nullable Project project, @NotNull String title, @NotNull ComboBox comboBox) { - super(project, title); + public ComboBoxUpdater(@Nullable Project project, @NotNull String title, @NotNull JComboBox comboBox) { + super(project, title, ModalityState.any()); myComboBox = comboBox; } @@ -99,14 +104,23 @@ public class TaskUiUtil { /** * Return item to select after every combo box update. Default implementation select item, returned by {@link #getExtraItem()}. + * If returned value is not present in the list it will be added depending on policy set by {@link #addSelectedItemIfMissing()}. * * @return selected combo box item + * @see #addSelectedItemIfMissing() */ @Nullable public T getSelectedItem() { return getExtraItem(); } + /** + * @return whether value returned by {@link #getSelectedItem()} should be forcibly added to the combo box. + */ + protected boolean addSelectedItemIfMissing() { + return false; + } + @SuppressWarnings("unchecked") @Override protected void updateUI() { @@ -123,12 +137,20 @@ public class TaskUiUtil { final T selected = getSelectedItem(); if (selected != null) { if (!selected.equals(extra) && !myResult.contains(selected)) { - myComboBox.addItem(selected); + if (addSelectedItemIfMissing()) { + myComboBox.addItem(selected); + myComboBox.setSelectedItem(selected); + } + else { + selectFirstItem(); + } + } + else { + myComboBox.setSelectedItem(selected); } - myComboBox.setSelectedItem(selected); } - else if (myComboBox.getItemCount() > 0) { - myComboBox.setSelectedIndex(0); + else { + selectFirstItem(); } } else { @@ -136,13 +158,19 @@ public class TaskUiUtil { } } + private void selectFirstItem() { + if (myComboBox.getItemCount() > 0) { + myComboBox.setSelectedIndex(0); + } + } + protected void handleError() { myComboBox.removeAllItems(); } } /** - * Very simple wrapper around {@link com.intellij.ui.ListCellRendererWrapper} useful for + * Very simple wrapper around {@link ListCellRendererWrapper} useful for * combo boxes where each item has plain text representation with special message for * {@code null} value. */ diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/ResponseUtil.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/ResponseUtil.java index e512ff04a15b..c3fc81ec27e7 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/ResponseUtil.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/ResponseUtil.java @@ -134,14 +134,20 @@ public class ResponseUtil { if (LOG.isDebugEnabled()) { String content = getResponseContentAsString(response); TaskUtil.prettyFormatJsonToLog(LOG, content); - return myGson.fromJson(content, myClass); + return myGson.fromJson(content, myClass); + } + else { + return myGson.fromJson(getResponseContentAsReader(response), myClass); } - return myGson.fromJson(getResponseContentAsReader(response), myClass); } catch (JsonSyntaxException e) { LOG.warn("Malformed server response", e); return null; } + catch (NumberFormatException e) { + LOG.error("NFE in response: " + getResponseContentAsString(response), e); + throw new RequestFailedException("Malformed response"); + } } } @@ -175,12 +181,18 @@ public class ResponseUtil { TaskUtil.prettyFormatJsonToLog(LOG, content); return myGson.fromJson(content, myTypeToken.getType()); } - return myGson.fromJson(getResponseContentAsReader(response), myTypeToken.getType()); + else { + return myGson.fromJson(getResponseContentAsReader(response), myTypeToken.getType()); + } } catch (JsonSyntaxException e) { LOG.warn("Malformed server response", e); return Collections.emptyList(); } + catch (NumberFormatException e) { + LOG.error("NFE in response: " + getResponseContentAsString(response), e); + throw new RequestFailedException("Malformed response"); + } } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepository.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepository.java index 89d3e3d7d8f2..e2cc5b078150 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepository.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepository.java @@ -22,6 +22,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.Task; import com.intellij.tasks.TaskBundle; import com.intellij.tasks.TaskRepositoryType; @@ -35,22 +36,27 @@ import com.intellij.tasks.trello.model.TrelloCard; import com.intellij.tasks.trello.model.TrelloList; import com.intellij.tasks.trello.model.TrelloUser; import com.intellij.util.Function; +import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.xmlb.annotations.Tag; import org.apache.http.*; import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestWrapper; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.utils.URIBuilder; import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -202,7 +208,7 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { try { final URIBuilder url = new URIBuilder(getRestApiUrl("members", "me")) .addParameter("fields", TrelloUser.REQUIRED_FIELDS); - return makeRequestAndDeserializeJsonResponse(url.build(), TrelloUser.class); + return ObjectUtils.assertNotNull(makeRequestAndDeserializeJsonResponse(url.build(), TrelloUser.class)); } catch (Exception e) { LOG.warn("Error while fetching initial user info", e); @@ -218,7 +224,7 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { final URIBuilder url = new URIBuilder(getRestApiUrl("boards", id)) .addParameter("fields", TrelloBoard.REQUIRED_FIELDS); try { - return makeRequestAndDeserializeJsonResponse(url.build(), TrelloBoard.class); + return ObjectUtils.assertNotNull(makeRequestAndDeserializeJsonResponse(url.build(), TrelloBoard.class)); } catch (Exception e) { LOG.warn("Error while fetching initial board info", e); @@ -231,7 +237,7 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { final URIBuilder url = new URIBuilder(getRestApiUrl("lists", id)) .addParameter("fields", TrelloList.REQUIRED_FIELDS); try { - return makeRequestAndDeserializeJsonResponse(url.build(), TrelloList.class); + return ObjectUtils.assertNotNull(makeRequestAndDeserializeJsonResponse(url.build(), TrelloList.class)); } catch (Exception e) { LOG.warn("Error while fetching initial list info" + id, e); @@ -244,7 +250,12 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { if (myCurrentBoard == null || myCurrentBoard == UNSPECIFIED_BOARD) { throw new IllegalStateException("Board not set"); } - final URIBuilder url = new URIBuilder(getRestApiUrl("boards", myCurrentBoard.getId(), "lists")) + return fetchBoardLists(myCurrentBoard.getId()); + } + + @NotNull + private List fetchBoardLists(@NotNull String boardId) throws Exception { + final URIBuilder url = new URIBuilder(getRestApiUrl("boards", boardId, "lists")) .addParameter("fields", TrelloList.REQUIRED_FIELDS); return makeRequestAndDeserializeJsonResponse(url.build(), TrelloUtil.LIST_OF_LISTS_TYPE); } @@ -327,7 +338,7 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { return cards; } - @NotNull + @Nullable private T executeMethod(@NotNull HttpUriRequest method, @NotNull ResponseHandler handler) throws Exception { final HttpClient client = getHttpClient(); final HttpResponse response = client.execute(method); @@ -345,10 +356,11 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { @NotNull private List makeRequestAndDeserializeJsonResponse(@NotNull URI url, @NotNull TypeToken> type) throws Exception { - return executeMethod(new HttpGet(url), new GsonMultipleObjectsDeserializer(TrelloUtil.GSON, type)); + final List result = executeMethod(new HttpGet(url), new GsonMultipleObjectsDeserializer(TrelloUtil.GSON, type)); + return ObjectUtils.assertNotNull(result); } - @NotNull + @Nullable private T makeRequestAndDeserializeJsonResponse(@NotNull URI url, @NotNull Class cls) throws Exception { return executeMethod(new HttpGet(url), new GsonSingleObjectDeserializer(TrelloUtil.GSON, cls)); } @@ -424,8 +436,36 @@ public final class TrelloRepository extends NewBaseRepositoryImpl { return "https://api.trello.com"; } + @NotNull + @Override + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + final TrelloCard card = fetchCardById(task.getId()); + if (card != null) { + final List lists = fetchBoardLists(card.getIdBoard()); + final Set result = new HashSet(); + for (TrelloList list : lists) { + if (!list.getId().equals(card.getIdList())) { + result.add(new CustomTaskState(list.getId(), list.getName())); + } + } + return result; + } + return Collections.emptySet(); + } + + @Override + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { + final URI url = new URIBuilder(getRestApiUrl("cards", task.getId(), "idList")).addParameter("value", state.getId()).build(); + final HttpResponse response = getHttpClient().execute(new HttpPut(url)); + if (response.getStatusLine() != null && + response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED && + EntityUtils.toString(response.getEntity()).trim().equals("unauthorized card permission requested")) { + throw new Exception(TaskBundle.message("trello.failure.write.access.required")); + } + } + @Override protected int getFeatures() { - return super.getFeatures() & ~NATIVE_SEARCH; + return super.getFeatures() & ~NATIVE_SEARCH | STATE_UPDATING; } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryEditor.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryEditor.java index 0ea5905cbcb2..f7dcd54d3baa 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryEditor.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryEditor.java @@ -260,6 +260,11 @@ public class TrelloRepositoryEditor extends BaseRepositoryEditor { @@ -284,5 +289,10 @@ public class TrelloRepositoryEditor extends BaseRepositoryEditor { public static final String DEVELOPER_KEY = "d6ec3709f7141007e150de64d4701181"; public static final String CLIENT_AUTHORIZATION_URL = - "https://trello.com/1/authorize?key=" + DEVELOPER_KEY +"&name=JetBrains&expiration=never&response_type=token"; + "https://trello.com/1/authorize?key=" + DEVELOPER_KEY +"&name=JetBrains&expiration=never&response_type=token&scope=read,write"; @NotNull @Override diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.form b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.form deleted file mode 100644 index 8dc52a33211a..000000000000 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.form +++ /dev/null @@ -1,84 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.java deleted file mode 100644 index fc084d293d52..000000000000 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.intellij.tasks.youtrack; - -import com.intellij.ui.components.JBLabel; -import com.intellij.util.ui.UIUtil; - -import javax.swing.*; - -/** - * @author Mikhail Golubev - */ -public class YouTrackOptionsTab { - public static final String MESSAGE = "" + - "This option should be used only when \"Name\" property for \"State\" field " + - "was changed in server settings. It's not related to localized names of states." + - ""; - - private JTextField myInProgressState; - private JTextField myResolvedState; - private JPanel myRootPanel; - private JBLabel myNoteText; - - public YouTrackOptionsTab() { - myNoteText.setComponentStyle(UIUtil.ComponentStyle.SMALL); - myNoteText.setText(MESSAGE); - } - - public JTextField getInProgressState() { - return myInProgressState; - } - - public JTextField getResolvedState() { - return myResolvedState; - } - - public JPanel getRootPanel() { - return myRootPanel; - } - - -} diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepository.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepository.java index 5da18ef96d21..c67fa6834368 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepository.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepository.java @@ -25,11 +25,10 @@ import com.intellij.tasks.impl.BaseRepository; import com.intellij.tasks.impl.BaseRepositoryImpl; import com.intellij.tasks.impl.LocalTaskImpl; import com.intellij.tasks.impl.TaskUtil; +import com.intellij.util.Function; import com.intellij.util.NullableFunction; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.text.VersionComparatorUtil; -import com.intellij.util.xmlb.annotations.MapAnnotation; -import com.intellij.util.xmlb.annotations.Property; import com.intellij.util.xmlb.annotations.Tag; import org.apache.axis.utils.XMLChar; import org.apache.commons.httpclient.HttpClient; @@ -43,14 +42,14 @@ import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.io.InputStream; import java.io.StringReader; import java.util.Date; -import java.util.EnumMap; import java.util.List; -import java.util.Map; +import java.util.Set; /** * @author Dmitry Avdeev @@ -59,13 +58,6 @@ import java.util.Map; public class YouTrackRepository extends BaseRepositoryImpl { private String myDefaultSearch = "Assignee: me sort by: updated #Unresolved"; - private Map myCustomStateNames = new EnumMap(TaskState.class); - - // Default names for supported issues states - { - myCustomStateNames.put(TaskState.IN_PROGRESS, "In Progress"); - myCustomStateNames.put(TaskState.RESOLVED, "Fixed"); - } /** * for serialization @@ -87,7 +79,6 @@ public class YouTrackRepository extends BaseRepositoryImpl { private YouTrackRepository(YouTrackRepository other) { super(other); myDefaultSearch = other.getDefaultSearch(); - myCustomStateNames = new EnumMap(other.getCustomStateNames()); } public Task[] getIssues(@Nullable String request, int max, long since) throws Exception { @@ -98,39 +89,44 @@ public class YouTrackRepository extends BaseRepositoryImpl { } String requestUrl = "/rest/project/issues/?filter=" + encodeUrl(query) + "&max=" + max + "&updatedAfter" + since; HttpMethod method = doREST(requestUrl, false); - InputStream stream = method.getResponseBodyAsStream(); - - // todo workaround for http://youtrack.jetbrains.net/issue/JT-7984 - String s = StreamUtil.readText(stream, CharsetToolkit.UTF8_CHARSET); - for (int i = 0; i < s.length(); i++) { - if (!XMLChar.isValid(s.charAt(i))) { - s = s.replace(s.charAt(i), ' '); - } - } - - Element element; try { - //InputSource source = new InputSource(stream); - //source.setEncoding("UTF-8"); - //element = new SAXBuilder(false).build(source).getRootElement(); - element = new SAXBuilder(false).build(new StringReader(s)).getRootElement(); - } - catch (JDOMException e) { - LOG.error("Can't parse YouTrack response for " + requestUrl, e); - throw e; - } - if ("error".equals(element.getName())) { - throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'"); - } + InputStream stream = method.getResponseBodyAsStream(); - List children = element.getChildren("issue"); - - final List tasks = ContainerUtil.mapNotNull(children, new NullableFunction() { - public Task fun(Element o) { - return createIssue(o); + // todo workaround for http://youtrack.jetbrains.net/issue/JT-7984 + String s = StreamUtil.readText(stream, CharsetToolkit.UTF8_CHARSET); + for (int i = 0; i < s.length(); i++) { + if (!XMLChar.isValid(s.charAt(i))) { + s = s.replace(s.charAt(i), ' '); + } } - }); - return tasks.toArray(new Task[tasks.size()]); + + Element element; + try { + //InputSource source = new InputSource(stream); + //source.setEncoding("UTF-8"); + //element = new SAXBuilder(false).build(source).getRootElement(); + element = new SAXBuilder(false).build(new StringReader(s)).getRootElement(); + } + catch (JDOMException e) { + LOG.error("Can't parse YouTrack response for " + requestUrl, e); + throw e; + } + if ("error".equals(element.getName())) { + throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'"); + } + + List children = element.getChildren("issue"); + + final List tasks = ContainerUtil.mapNotNull(children, new NullableFunction() { + public Task fun(Element o) { + return createIssue(o); + } + }); + return tasks.toArray(new Task[tasks.size()]); + } + finally { + method.releaseConnection(); + } } @Nullable @@ -153,10 +149,16 @@ public class YouTrackRepository extends BaseRepositoryImpl { method.addParameter("password", getPassword()); client.getParams().setContentCharset("UTF-8"); client.executeMethod(method); - if (method.getStatusCode() != 200) { - throw new Exception("Cannot login: HTTP status code " + method.getStatusCode()); + String response; + try { + if (method.getStatusCode() != 200) { + throw new Exception("Cannot login: HTTP status code " + method.getStatusCode()); + } + response = method.getResponseBodyAsString(1000); + } + finally { + method.releaseConnection(); } - String response = method.getResponseBodyAsString(1000); if (response == null) { throw new NullPointerException(); } @@ -173,12 +175,23 @@ public class YouTrackRepository extends BaseRepositoryImpl { @Nullable public Task findTask(@NotNull String id) throws Exception { - HttpMethod method = doREST("/rest/issue/byid/" + id, false); - InputStream stream = method.getResponseBodyAsStream(); - Element element = new SAXBuilder(false).build(stream).getRootElement(); + final Element element = fetchRequestAsElement(id); return element.getName().equals("issue") ? createIssue(element) : null; } + @TestOnly + @NotNull + public Element fetchRequestAsElement(@NotNull String id) throws Exception { + final HttpMethod method = doREST("/rest/issue/byid/" + id, false); + try { + final InputStream stream = method.getResponseBodyAsStream(); + return new SAXBuilder(false).build(stream).getRootElement(); + } + finally { + method.releaseConnection(); + } + } + HttpMethod doREST(String request, boolean post) throws Exception { HttpClient client = login(new PostMethod(getUrl() + "/rest/user/login")); @@ -198,12 +211,28 @@ public class YouTrackRepository extends BaseRepositoryImpl { } @Override - public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception { - String s = myCustomStateNames.get(state); - if (StringUtil.isEmpty(s)) { - s = state.name(); + public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception { + doREST("/rest/issue/execute/" + task.getId() + "?command=" + encodeUrl("state " + state.getId()), true).releaseConnection(); + } + + @NotNull + @Override + public Set getAvailableTaskStates(@NotNull Task task) throws Exception { + final HttpMethod method = doREST("/rest/issue/" + task.getId() + "/execute/intellisense?command=" + encodeUrl("state "), false); + try { + final InputStream stream = method.getResponseBodyAsStream(); + final Element element = new SAXBuilder(false).build(stream).getRootElement(); + return ContainerUtil.map2Set(element.getChild("suggest").getChildren("item"), new Function() { + @Override + public CustomTaskState fun(Element element) { + final String stateName = element.getChildText("option"); + return new CustomTaskState(stateName, stateName); + } + }); + } + finally { + method.releaseConnection(); } - doREST("/rest/issue/execute/" + task.getId() + "?command=" + encodeUrl("state " + s), true); } @Nullable @@ -315,9 +344,7 @@ public class YouTrackRepository extends BaseRepositoryImpl { public boolean equals(Object o) { if (!super.equals(o)) return false; YouTrackRepository repository = (YouTrackRepository)o; - if (!Comparing.equal(repository.getDefaultSearch(), getDefaultSearch())) return false; - if (!Comparing.equal(repository.getCustomStateNames(), getCustomStateNames())) return false; - return true; + return Comparing.equal(repository.getDefaultSearch(), getDefaultSearch()); } private static final Logger LOG = Logger.getInstance("#com.intellij.tasks.youtrack.YouTrackRepository"); @@ -326,20 +353,30 @@ public class YouTrackRepository extends BaseRepositoryImpl { public void updateTimeSpent(@NotNull LocalTask task, @NotNull String timeSpent, @NotNull String comment) throws Exception { checkVersion(); final HttpMethod method = doREST("/rest/issue/execute/" + task.getId() + "?command=work+Today+" + timeSpent.replaceAll(" ", "+") + "+" + comment, true); - if (method.getStatusCode() != 200) { - InputStream stream = method.getResponseBodyAsStream(); - String message = new SAXBuilder(false).build(stream).getRootElement().getText(); - throw new Exception(message); + try { + if (method.getStatusCode() != 200) { + InputStream stream = method.getResponseBodyAsStream(); + String message = new SAXBuilder(false).build(stream).getRootElement().getText(); + throw new Exception(message); + } + } + finally { + method.releaseConnection(); } } private void checkVersion() throws Exception { HttpMethod method = doREST("/rest/workflow/version", false); - InputStream stream = method.getResponseBodyAsStream(); - Element element = new SAXBuilder(false).build(stream).getRootElement(); - final boolean timeTrackingAvailable = element.getName().equals("version") && VersionComparatorUtil.compare(element.getChildText("version"), "4.1") >= 0; - if (!timeTrackingAvailable) { - throw new Exception("This version of Youtrack the time tracking is not supported"); + try { + InputStream stream = method.getResponseBodyAsStream(); + Element element = new SAXBuilder(false).build(stream).getRootElement(); + final boolean timeTrackingAvailable = element.getName().equals("version") && VersionComparatorUtil.compare(element.getChildText("version"), "4.1") >= 0; + if (!timeTrackingAvailable) { + throw new Exception("This version of Youtrack the time tracking is not supported"); + } + } + finally { + method.releaseConnection(); } } @@ -347,26 +384,4 @@ public class YouTrackRepository extends BaseRepositoryImpl { protected int getFeatures() { return super.getFeatures() | TIME_MANAGEMENT | STATE_UPDATING; } - - public void setCustomStateNames(Map customStateNames) { - myCustomStateNames.putAll(customStateNames); - } - - @Tag("customStates") - @Property(surroundWithTag = false) - @MapAnnotation( - surroundWithTag = false, - keyAttributeName = "state", - valueAttributeName = "name", - surroundKeyWithTag = false, - surroundValueWithTag = false - ) - - public Map getCustomStateNames() { - return myCustomStateNames; - } - - public void setCustomStateName(TaskState state, String name) { - myCustomStateNames.put(state, name); - } } diff --git a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepositoryEditor.java b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepositoryEditor.java index db16ea5247bb..34ba0a773b5c 100644 --- a/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepositoryEditor.java +++ b/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepositoryEditor.java @@ -3,10 +3,8 @@ package com.intellij.tasks.youtrack; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; -import com.intellij.tasks.TaskState; import com.intellij.tasks.config.BaseRepositoryEditor; import com.intellij.tasks.youtrack.lang.YouTrackLanguage; import com.intellij.ui.EditorTextField; @@ -17,14 +15,12 @@ import com.intellij.util.ui.FormBuilder; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.Map; /** * @author Dmitry Avdeev */ public class YouTrackRepositoryEditor extends BaseRepositoryEditor { private static final Logger LOG = Logger.getInstance(YouTrackRepository.class); - private final YouTrackOptionsTab myOptions; private EditorTextField myDefaultSearch; private JBLabel mySearchLabel; @@ -36,17 +32,6 @@ public class YouTrackRepositoryEditor extends BaseRepositoryEditor states = myRepository.getCustomStateNames(); - myOptions.getInProgressState().setText(StringUtil.notNullize(states.get(TaskState.IN_PROGRESS))); - myOptions.getResolvedState().setText(StringUtil.notNullize(states.get(TaskState.RESOLVED))); - - installListener(myOptions.getInProgressState()); - installListener(myOptions.getResolvedState()); - - myTabbedPane.add("Options", myOptions.getRootPanel()); } @Override @@ -61,8 +46,6 @@ public class YouTrackRepositoryEditor extends BaseRepositoryEditor states = myRepository.getAvailableTaskStates(task); + final List stateNames = ContainerUtil.map(states, new Function() { + @Override + public String fun(CustomTaskState state) { + return state.getPresentableName(); + } + }); + assertContainsElements(stateNames, "North", "South"); + + // ? -> North + myRepository.setTaskState(task, NORTH_STATE); + Element element = myRepository.fetchRequestAsElement(REQUEST_WITH_CUSTOM_STATES_ID); + assertEquals("North", element.getAttributeValue("state")); + + // North -> Submitted + myRepository.setTaskState(task, SUBMITTED_STATE); + element = myRepository.fetchRequestAsElement(REQUEST_WITH_CUSTOM_STATES_ID); + assertEquals("Submitted", element.getAttributeValue("state")); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myRepository = new YouTrackRepository(new YouTrackRepositoryType()); + myRepository.setUrl(YOUTRACK_4_TEST_SERVER_URL); + myRepository.setUsername("buildtest"); + myRepository.setPassword("buildtest"); + } +} diff --git a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/live/TrelloIntegrationTest.java b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/live/TrelloIntegrationTest.java index 654b51261d88..d9150d342ce2 100644 --- a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/live/TrelloIntegrationTest.java +++ b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/live/TrelloIntegrationTest.java @@ -2,8 +2,10 @@ package com.intellij.tasks.integration.live; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.CustomTaskState; import com.intellij.tasks.trello.TrelloRepository; import com.intellij.tasks.trello.TrelloRepositoryType; +import com.intellij.tasks.trello.TrelloTask; import com.intellij.tasks.trello.model.*; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; @@ -22,8 +24,9 @@ import static com.intellij.tasks.trello.model.TrelloLabel.LabelColor.*; */ public class TrelloIntegrationTest extends LiveIntegrationTestCase { - private static final String BOARD_1_NAME = "Board 1"; - private static final String BOARD_1_ID = "53c416a8a6e5a78753562043"; + // Basic functionality (searching, filtering, etc.) + private static final String BASIC_FUNCTIONALITY_BOARD_NAME = "Basic Functionality"; + private static final String BASIC_FUNCTIONALITY_BOARD_ID = "53c416a8a6e5a78753562043"; private static final String LIST_1_1_NAME = "List 1-1"; private static final String LIST_1_1_ID = "53c416a8a6e5a78753562044"; @@ -31,6 +34,17 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase boards = myRepository.fetchUserBoards(); - assertEquals(2, boards.size()); - assertObjectsNamed("All boards of the user should be included", boards, "Board 1", "Board 2"); + assertEquals(3, boards.size()); + assertObjectsNamed("All boards of the user should be included", boards, + BASIC_FUNCTIONALITY_BOARD_NAME, LABELS_AND_COLORS_BOARD_NAME, STATE_UPDATES_BOARD_NAME); } public void testFetchListsOfBoard() throws Exception { - TrelloBoard selectedBoard = myRepository.fetchBoardById(BOARD_1_ID); + TrelloBoard selectedBoard = myRepository.fetchBoardById(BASIC_FUNCTIONALITY_BOARD_ID); assertNotNull(selectedBoard); myRepository.setCurrentBoard(selectedBoard); List lists = myRepository.fetchBoardLists(); @@ -88,7 +103,7 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase fetchCards(@Nullable String boardId, @Nullable String listId, boolean withClosed) throws Exception { if (boardId != null) { - TrelloBoard selectedBoard = myRepository.fetchBoardById(BOARD_1_ID); + TrelloBoard selectedBoard = myRepository.fetchBoardById(BASIC_FUNCTIONALITY_BOARD_ID); assertNotNull(selectedBoard); myRepository.setCurrentBoard(selectedBoard); } @@ -108,24 +123,24 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase cards = fetchCards(BOARD_1_ID, null, true); + List cards = fetchCards(BASIC_FUNCTIONALITY_BOARD_ID, null, true); assertObjectsNamed("All cards of the board should be included", cards, "Card 1-1-1", "Card 1-1-2", "Card 1-2-1", "Card 1-3-1", "Archived Card"); } public void testCardsFilteringByMembership() throws Exception { myRepository.setIncludeAllCards(true); - List allCards = fetchCards(BOARD_1_ID, LIST_1_1_ID, true); + List allCards = fetchCards(BASIC_FUNCTIONALITY_BOARD_ID, LIST_1_1_ID, true); assertObjectsNamed("All cards of the list should be included", allCards, "Card 1-1-1", "Card 1-1-2", "Archived Card"); myRepository.setIncludeAllCards(false); - List assignedCards = fetchCards(BOARD_1_ID, LIST_1_1_ID, true); + List assignedCards = fetchCards(BASIC_FUNCTIONALITY_BOARD_ID, LIST_1_1_ID, true); assertObjectsNamed("Only cards of the list assigned to user should be included", assignedCards, "Card 1-1-1"); } public void testCardsFilteringByStatus() throws Exception { myRepository.setIncludeAllCards(true); - List allCards = fetchCards(BOARD_1_ID, LIST_1_1_NAME, true); + List allCards = fetchCards(BASIC_FUNCTIONALITY_BOARD_ID, LIST_1_1_NAME, true); assertObjectsNamed("All cards of the list should be included", allCards, "Card 1-1-1", "Card 1-1-2", "Archived Card"); TrelloCard card = ContainerUtil.find(allCards, new Condition() { @@ -139,8 +154,17 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase labels = card.getLabels(); @@ -156,13 +180,41 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase states = myRepository.getAvailableTaskStates(task); + assertEquals(1, states.size()); + final CustomTaskState inProgressState = states.iterator().next(); + assertEquals(IN_PROGRESS_LIST_ID, inProgressState.getId()); + assertEquals("In Progress", inProgressState.getPresentableName()); + + // Backlog -> In Progress + myRepository.setTaskState(task, inProgressState); + card = myRepository.fetchCardById(FEATURE_CARD_ID); + assertNotNull(card); + assertEquals(STATE_UPDATES_BOARD_ID, card.getIdBoard()); + assertEquals(IN_PROGRESS_LIST_ID, card.getIdList()); + + // Discover "Backlog" list + task = new TrelloTask(card, myRepository); + states = myRepository.getAvailableTaskStates(task); + assertEquals(1, states.size()); + final CustomTaskState backlogState = states.iterator().next(); + assertEquals(BACKLOG_LIST_ID, backlogState.getId()); + assertEquals("Backlog", backlogState.getPresentableName()); + + // In Progress -> Backlog + myRepository.setTaskState(task, backlogState); + card = myRepository.fetchCardById(FEATURE_CARD_ID); + assertNotNull(card); + assertEquals(STATE_UPDATES_BOARD_ID, card.getIdBoard()); + assertEquals(BACKLOG_LIST_ID, card.getIdList()); } static void assertObjectsNamed(@NotNull String message, @NotNull Collection objects, @NotNull String... names) {