Merge branch 'mikhail.golubev/configurable-issues-states'
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 24 Feb 2015 11:57:47 +0000 (14:57 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Tue, 24 Feb 2015 12:28:27 +0000 (15:28 +0300)
Conflicts:
plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java

33 files changed:
platform/lang-impl/src/com/intellij/ide/actions/TemplateKindCombo.java
plugins/tasks/tasks-api/src/com/intellij/tasks/CustomTaskState.java [new file with mode: 0644]
plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepository.java
plugins/tasks/tasks-api/src/com/intellij/tasks/TaskRepositoryType.java
plugins/tasks/tasks-api/src/com/intellij/tasks/TaskState.java
plugins/tasks/tasks-api/src/com/intellij/tasks/impl/BaseRepository.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRemoteApi.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/JiraRepository.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/JiraRestApi.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/JiraRestApi2.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api2/model/JiraTransitionsWrapperApi2.java [new file with mode: 0644]
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/rest/api20alpha1/JiraRestApi20Alpha1.java
plugins/tasks/tasks-core/jira/src/com/intellij/tasks/jira/soap/JiraLegacyApi.java
plugins/tasks/tasks-core/src/com/intellij/tasks/TaskBundle.properties
plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskAction.java
plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.form
plugins/tasks/tasks-core/src/com/intellij/tasks/actions/CloseTaskDialog.java
plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.form
plugins/tasks/tasks-core/src/com/intellij/tasks/actions/OpenTaskDialog.java
plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskManagerImpl.java
plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskStateCombo.java [new file with mode: 0644]
plugins/tasks/tasks-core/src/com/intellij/tasks/impl/TaskUiUtil.java
plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/ResponseUtil.java
plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepository.java
plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryEditor.java
plugins/tasks/tasks-core/src/com/intellij/tasks/trello/TrelloRepositoryType.java
plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.form [deleted file]
plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackOptionsTab.java [deleted file]
plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepository.java
plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/YouTrackRepositoryEditor.java
plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java
plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/YouTrackIntegrationTest.java [new file with mode: 0644]
plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/live/TrelloIntegrationTest.java

index 5a76d4bd52c795e29f0f09c5e37f4e46158a81ad..46521faff6c51fd24464969ef0c0dcd4540d069b 100644 (file)
@@ -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 (file)
index 0000000..0fced6c
--- /dev/null
@@ -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 + ')';
+  }
+}
index 416a7158eb260716d360f4e93bef8577fe0695d1..c6e270fdef4e4729dd2ec9752238c092841bbec6 100644 (file)
@@ -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<CustomTaskState> getAvailableTaskStates(@NotNull Task task) throws Exception {
+    //noinspection unchecked
+    return ContainerUtil.map2Set(getRepositoryType().getPossibleTaskStates(), new Function<TaskState, CustomTaskState>() {
+      @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 <tt>PROJECT-123</tt> 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()}.
+   * @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.
+   * <p/>
+   * 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 com.intellij.tasks.TaskRepositoryType#getPossibleTaskStates()
-   * @see com.intellij.tasks.TaskRepository#getFeatures()
+   * @see TaskRepositoryType#getPossibleTaskStates()
+   * @see TaskRepository#getFeatures()
    */
-  public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
-    throw new UnsupportedOperationException("Setting task to state " + state + " is not supported");
+  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;
index f348f4264c2244b21e3c96d3e18b0b0075e52980..d4575cf3eb92e7ed99b7208e1cef71bf2147b2b6 100644 (file)
@@ -67,8 +67,10 @@ public abstract class TaskRepositoryType<T extends TaskRepository> implements Ta
   public abstract Class<T> 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<TaskState> getPossibleTaskStates() {
     return EnumSet.noneOf(TaskState.class);
   }
index c0c0a2628b9df4eadc466d0ebee5b5d12f8078aa..79c504a44a6045f91d2479bc9b9257402bd64342 100644 (file)
  */
 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"),
+
+  OTHER("Other");
+
+  private String myPresentableName;
 
-  SUBMITTED,
-  OPEN,
-  IN_PROGRESS,
-  REOPENED,
-  RESOLVED,
+  TaskState(@NotNull String presentableName) {
+    myPresentableName = presentableName;
+  }
 
-  OTHER
+  @NotNull
+  public String getPresentableName() {
+    return myPresentableName;
+  }
 }
index 8423736c8c10f7d388c67b33e37df0004c3425e3..0f67d4bae9096956fd4935fff5964b1c72d82c98 100644 (file)
@@ -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);
index 8ee9c97512a1941276f52b0fe8fd14ab33f2816e..7b760769a0e3f927a31d1651c99760955351990d 100644 (file)
@@ -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<CustomTaskState> 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;
     }
-
   }
 }
index 820bb22426e00e273e40589ee639b7c554d22146..ec0e612f4e81f36de1b013333b8ecd6f8aedecbc 100644 (file)
@@ -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<CustomTaskState> 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
index 502eed083e7423561c17d7ca1e88536208d9326e..78919e390d5f5909d8a416c8f6891fb7cb510998 100644 (file)
@@ -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 {
index 0c9b647ee7205b0b2d53e1cb5a5ca50148ed2037..b83607481fac6f7c0ae6432f27b24c6825a0d474 100644 (file)
@@ -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<CustomTaskState> 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 (file)
index 0000000..a1534ab
--- /dev/null
@@ -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<JiraTransition> 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<CustomTaskState> getTransitions() {
+    final Set<CustomTaskState> result = new LinkedHashSet<CustomTaskState>();
+    nextTransition:
+    for (JiraTransition transition : transitions) {
+      final String stateName = transition.target.name;
+      final List<String> resolutions = new ArrayList<String>();
+      if (transition.fields != null) {
+        for (Map.Entry<String, JsonElement> 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;
+  }
+}
index 7de17945ac318da643b832be0dcfbcd2abcabe4a..bc70d239ce733b6c8daa5de19678336e89ccbb10 100644 (file)
@@ -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<JiraResponseWrapper.Issues<JiraIssueApi20Alpha1>>() { /* empty */ }.getType();
+  private static final Type ISSUES_WRAPPER_TYPE = new TypeToken<JiraResponseWrapper.Issues<JiraIssueApi20Alpha1>>() {/* empty */}.getType();
 
   public JiraRestApi20Alpha1(JiraRepository repository) {
     super(repository);
   }
 
+  @NotNull
+  @Override
+  public Set<CustomTaskState> 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<CustomTaskState> result = new HashSet<CustomTaskState>();
+    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"));
index e8cbee842cdace3165cbf0f39a903a7a387c7dd8..7f5ab27cc9a189aa188040a9d2b610442668d190 100644 (file)
@@ -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 Set<CustomTaskState> getAvailableTaskStates(@NotNull Task task) throws Exception {
+    return Collections.emptySet();
+  }
+
   @Override
-  public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
+  public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception {
     throw new Exception(TaskBundle.message("jira.failure.no.state.update"));
   }
 
index ce617cbf01ca81064cf83b5c800a4ee0940f3c3e..4d631b7c5d20e6c83eb216d7d16704fa35a6d8c4 100644 (file)
@@ -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.
index 6863eef785105a383ca624206fb44e2c22724e14..7bf2afa94b2fe89982826ab42f2873ac31aecebb 100644 (file)
@@ -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");
index 0c8600a9785b25ae2274c649ed2c891a7cd58c80..7508f5a7dc346309048c657c499b56b0e4083d86 100644 (file)
@@ -3,7 +3,7 @@
   <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
     <margin top="0" left="0" bottom="0" right="0"/>
     <constraints>
-      <xy x="20" y="20" width="477" height="131"/>
+      <xy x="20" y="20" width="477" height="143"/>
     </constraints>
     <properties/>
     <border type="none"/>
           </component>
         </children>
       </grid>
-      <component id="7d098" class="javax.swing.JCheckBox" binding="myCloseIssue">
-        <constraints>
-          <grid row="1" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
-        </constraints>
-        <properties>
-          <text value="Close &amp;issue"/>
-        </properties>
-      </component>
       <component id="5ff61" class="javax.swing.JLabel" binding="myTaskLabel">
         <constraints>
           <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
           <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
         </constraints>
       </hspacer>
+      <grid id="72255" layout-manager="GridLayoutManager" row-count="1" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+        <margin top="0" left="0" bottom="0" right="0"/>
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="3" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+        <border type="none"/>
+        <children>
+          <component id="b0266" class="javax.swing.JLabel" binding="myStateComboBoxLabel">
+            <constraints>
+              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text value="Mark &amp;as:"/>
+            </properties>
+          </component>
+          <hspacer id="76017">
+            <constraints>
+              <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+            </constraints>
+          </hspacer>
+          <component id="cc401" class="com.intellij.tasks.impl.TaskStateCombo" binding="myStateCombo" custom-create="true">
+            <constraints>
+              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties/>
+          </component>
+        </children>
+      </grid>
     </children>
   </grid>
 </form>
index fa9879c05fe0759f63394476da8eacf4079ef15b..73d14d23435ad29cf9e6b7d37fd2704d7f82a8d1 100644 (file)
@@ -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<CustomTaskState> available) {
+        return repository.getPreferredCloseTaskState();
+      }
+    };
+  }
 }
index fb7ab62bdaa8c8c5ce42f3c94635ccdfa0a9170a..64b57bd9d93be799d58dc1f7cf6391589106c4bc 100644 (file)
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.intellij.tasks.actions.OpenTaskDialog">
-  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+  <grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
     <margin top="0" left="0" bottom="0" right="0"/>
     <constraints>
-      <xy x="20" y="20" width="504" height="166"/>
+      <xy x="20" y="20" width="504" height="196"/>
     </constraints>
     <properties/>
     <border type="none"/>
@@ -11,7 +11,7 @@
       <grid id="35df8" layout-manager="GridLayoutManager" row-count="1" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
         <margin top="0" left="0" bottom="0" right="0"/>
         <constraints>
-          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties/>
         <border type="none"/>
               <text value="&amp;Clear current context"/>
             </properties>
           </component>
-          <component id="d4c2" class="javax.swing.JCheckBox" binding="myMarkAsInProgressBox" default-binding="true">
-            <constraints>
-              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
-            </constraints>
-            <properties>
-              <focusable value="false"/>
-              <text value="Mark as 'In &amp;Progress'"/>
-            </properties>
-          </component>
         </children>
       </grid>
       <grid id="29a80" layout-manager="GridLayoutManager" row-count="1" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
         <margin top="0" left="0" bottom="0" right="0"/>
         <constraints>
-          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties/>
         <border type="none"/>
@@ -76,7 +67,7 @@
       <grid id="637f6" binding="myVcsPanel" layout-manager="GridLayoutManager" row-count="2" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
         <margin top="0" left="0" bottom="0" right="0"/>
         <constraints>
-          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
         </constraints>
         <properties/>
         <clientProperties>
             <constraints>
               <grid row="0" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
             </constraints>
-            <properties/>
+            <properties>
+              <model/>
+            </properties>
           </component>
           <component id="2cb41" class="com.intellij.ui.components.JBLabel" binding="myFromLabel">
             <constraints>
           </component>
         </children>
       </grid>
+      <grid id="7efc8" layout-manager="GridLayoutManager" row-count="1" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+        <margin top="0" left="0" bottom="0" right="0"/>
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+        <border type="none"/>
+        <children>
+          <component id="f14c2" class="javax.swing.JLabel" binding="myTaskStateLabel">
+            <constraints>
+              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+            <properties>
+              <text value="&amp;Mark as:"/>
+            </properties>
+          </component>
+          <hspacer id="94ebc">
+            <constraints>
+              <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
+            </constraints>
+          </hspacer>
+          <component id="812b1" class="com.intellij.tasks.impl.TaskStateCombo" binding="myTaskStateCombo" custom-create="true">
+            <constraints>
+              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
+            </constraints>
+          </component>
+        </children>
+      </grid>
     </children>
   </grid>
 </form>
index 66c3f80540c5583b255af3f9fb870e717cef5990..485161687d689034300ddd233fa8914d91a8384d 100644 (file)
@@ -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<CustomTaskState> available) {
+        return repository.getPreferredOpenTaskState();
+      }
+    };
+  }
 }
index 3529dc1229e9260c59d86e63d4832730a65f21b6..3a50bb8da92c3f4fcbd5a10f1826625db371f666 100644 (file)
@@ -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 (file)
index 0000000..2126eec
--- /dev/null
@@ -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<CustomStateTrinityAdapter>() {
+            @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<CustomStateTrinityAdapter>(myProject, "Fetching available task states...", comboBox) {
+        @NotNull
+        @Override
+        protected List<CustomStateTrinityAdapter> 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<CustomTaskState> available);
+
+  private static class CustomStateTrinityAdapter extends Trinity<String, Icon, String> {
+    final CustomTaskState myState;
+
+    public CustomStateTrinityAdapter(@NotNull CustomTaskState state) {
+      super(state.getPresentableName(), null, state.getId());
+      myState = state;
+    }
+
+    @NotNull
+    static List<CustomStateTrinityAdapter> wrapList(@NotNull Collection<CustomTaskState> states) {
+      return ContainerUtil.map(states, new Function<CustomTaskState, CustomStateTrinityAdapter>() {
+        @Override
+        public CustomStateTrinityAdapter fun(CustomTaskState state) {
+          return new CustomStateTrinityAdapter(state);
+        }
+      });
+    }
+
+    @NotNull
+    static List<CustomTaskState> unwrapList(@NotNull Collection<CustomStateTrinityAdapter> wrapped) {
+      return ContainerUtil.map(wrapped, new Function<CustomStateTrinityAdapter, CustomTaskState>() {
+        @Override
+        public CustomTaskState fun(CustomStateTrinityAdapter adapter) {
+          return adapter.myState;
+        }
+      });
+    }
+  }
+}
index d24ed38c98196957a72376d2aaa150ab84f614c1..0f8e571a9e877427550dc1131d2c38533c128002 100644 (file)
@@ -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<T> 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<T> extends RemoteFetchTask<List<T>> {
-    protected final ComboBox myComboBox;
+  public static abstract class ComboBoxUpdater<T> extends RemoteFetchTask<Collection<T>> {
+    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.
    */
index e512ff04a15bb081af43417dbff3d9a4e174a300..c3fc81ec27e7a178e2f877ce220f7466a9ab8606 100644 (file)
@@ -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");
+      }
     }
   }
 
index 89d3e3d7d8f25ceeaeda97c492da2a3d68870ee9..e2cc5b078150200e55995d3a82b9452791febc1a 100644 (file)
@@ -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<TrelloList> 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> T executeMethod(@NotNull HttpUriRequest method, @NotNull ResponseHandler<T> 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 <T> List<T> makeRequestAndDeserializeJsonResponse(@NotNull URI url, @NotNull TypeToken<List<T>> type) throws Exception {
-    return executeMethod(new HttpGet(url), new GsonMultipleObjectsDeserializer<T>(TrelloUtil.GSON, type));
+    final List<T> result = executeMethod(new HttpGet(url), new GsonMultipleObjectsDeserializer<T>(TrelloUtil.GSON, type));
+    return ObjectUtils.assertNotNull(result);
   }
 
-  @NotNull
+  @Nullable
   private <T> T makeRequestAndDeserializeJsonResponse(@NotNull URI url, @NotNull Class<T> cls) throws Exception {
     return executeMethod(new HttpGet(url), new GsonSingleObjectDeserializer<T>(TrelloUtil.GSON, cls));
   }
@@ -424,8 +436,36 @@ public final class TrelloRepository extends NewBaseRepositoryImpl {
     return "https://api.trello.com";
   }
 
+  @NotNull
+  @Override
+  public Set<CustomTaskState> getAvailableTaskStates(@NotNull Task task) throws Exception {
+    final TrelloCard card = fetchCardById(task.getId());
+    if (card != null) {
+      final List<TrelloList> lists = fetchBoardLists(card.getIdBoard());
+      final Set<CustomTaskState> result = new HashSet<CustomTaskState>();
+      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;
   }
 }
index 0ea5905cbcb2eba868fcebb8faccd4fb806f4aae..f7dcd54d3baab5cdc4164b25bea27c2910fb38e8 100644 (file)
@@ -260,6 +260,11 @@ public class TrelloRepositoryEditor extends BaseRepositoryEditor<TrelloRepositor
       super.handleError();
       myListComboBox.removeAllItems();
     }
+
+    @Override
+    protected boolean addSelectedItemIfMissing() {
+      return true;
+    }
   }
 
   private class ListsComboBoxUpdater extends TaskUiUtil.ComboBoxUpdater<TrelloList> {
@@ -284,5 +289,10 @@ public class TrelloRepositoryEditor extends BaseRepositoryEditor<TrelloRepositor
     public TrelloList getSelectedItem() {
       return myRepository.getCurrentList();
     }
+
+    @Override
+    protected boolean addSelectedItemIfMissing() {
+      return true;
+    }
   }
 }
index 86fca7a40be430f4a15901d450b5b718a4ab54d0..253a41f7bbcaf8679b5c5968fe923eed1c1e383b 100644 (file)
@@ -33,7 +33,7 @@ import javax.swing.*;
 public class TrelloRepositoryType extends BaseRepositoryType<TrelloRepository> {
   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 (file)
index 8dc52a3..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.intellij.tasks.youtrack.YouTrackOptionsTab">
-  <grid id="27dc6" binding="myRootPanel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
-    <margin top="0" left="0" bottom="0" right="0"/>
-    <constraints>
-      <xy x="20" y="20" width="500" height="400"/>
-    </constraints>
-    <properties>
-      <requestFocusEnabled value="false"/>
-      <toolTipText value="Name of &quot;In Progress&quot; stated if any exists."/>
-    </properties>
-    <border type="none"/>
-    <children>
-      <grid id="3248b" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
-        <margin top="0" left="0" bottom="0" right="0"/>
-        <constraints>
-          <grid row="0" column="0" row-span="2" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
-        </constraints>
-        <properties/>
-        <border type="etched" title="Custom states">
-          <color color="-16777216"/>
-        </border>
-        <children>
-          <component id="80263" class="com.intellij.ui.components.JBLabel">
-            <constraints>
-              <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
-            </constraints>
-            <properties>
-              <labelFor value="e3c7a"/>
-              <text value="In &amp;Progress:"/>
-            </properties>
-          </component>
-          <component id="123f9" class="com.intellij.ui.components.JBLabel">
-            <constraints>
-              <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
-            </constraints>
-            <properties>
-              <labelFor value="b21ba"/>
-              <text value="&amp;Resolved:"/>
-            </properties>
-          </component>
-          <component id="e3c7a" class="javax.swing.JTextField" binding="myInProgressState">
-            <constraints>
-              <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
-                <preferred-size width="150" height="-1"/>
-              </grid>
-            </constraints>
-            <properties>
-              <text value=""/>
-              <toolTipText value="Actual name of &quot;In Progress&quot; state used in &quot;Open Task&quot; dialog"/>
-            </properties>
-          </component>
-          <component id="b21ba" class="javax.swing.JTextField" binding="myResolvedState">
-            <constraints>
-              <grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
-                <preferred-size width="150" height="-1"/>
-              </grid>
-            </constraints>
-            <properties>
-              <text value=""/>
-              <toolTipText value="Actual name of &quot;Resolved&quot; state used in &quot;Close Task&quot; dialog "/>
-            </properties>
-          </component>
-          <component id="cdcfa" class="com.intellij.ui.components.JBLabel" binding="myNoteText">
-            <constraints>
-              <grid row="2" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false">
-                <preferred-size width="150" height="50"/>
-              </grid>
-            </constraints>
-            <properties>
-              <opaque value="false"/>
-              <text value="&lt;html&gt;&#10;  &lt;head&gt;&#10;    &#10;  &lt;/head&gt;&#10;  &lt;body&gt;&#10;  &lt;/body&gt;&#10;&lt;/html&gt;&#10;"/>
-            </properties>
-          </component>
-        </children>
-      </grid>
-      <vspacer id="3db80">
-        <constraints>
-          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
-        </constraints>
-      </vspacer>
-    </children>
-  </grid>
-</form>
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 (file)
index fc084d2..0000000
+++ /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 = "<html>" +
-                                       "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." +
-                                       "</html>";
-
-  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;
-  }
-
-
-}
index 5da18ef96d210cdfc101335ea60c114e6caf6852..c67fa68343683404883817530c0989d4e80097bf 100644 (file)
@@ -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<TaskState, String> myCustomStateNames = new EnumMap<TaskState, String>(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<TaskState, String>(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();
+    try {
+      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), ' ');
+      // 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() + "'");
-    }
+      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<Element> children = element.getChildren("issue");
+      List<Element> children = element.getChildren("issue");
 
-    final List<Task> tasks = ContainerUtil.mapNotNull(children, new NullableFunction<Element, Task>() {
-      public Task fun(Element o) {
-        return createIssue(o);
-      }
-    });
-    return tasks.toArray(new Task[tasks.size()]);
+      final List<Task> tasks = ContainerUtil.mapNotNull(children, new NullableFunction<Element, Task>() {
+        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<CustomTaskState> 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<Element, CustomTaskState>() {
+        @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<TaskState, String> customStateNames) {
-    myCustomStateNames.putAll(customStateNames);
-  }
-
-  @Tag("customStates")
-  @Property(surroundWithTag = false)
-  @MapAnnotation(
-    surroundWithTag = false,
-    keyAttributeName = "state",
-    valueAttributeName = "name",
-    surroundKeyWithTag = false,
-    surroundValueWithTag = false
-  )
-
-  public Map<TaskState, String> getCustomStateNames() {
-    return myCustomStateNames;
-  }
-
-  public void setCustomStateName(TaskState state, String name) {
-    myCustomStateNames.put(state, name);
-  }
 }
index db16ea5247bb3a659406227ea47e8ca97eabe3ec..34ba0a773b5ced5ff0030268ff1e777970a2823a 100644 (file)
@@ -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<YouTrackRepository> {
   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<YouTrackRepos
     final PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(myDefaultSearch.getDocument());
     assert file != null;
     file.putUserData(YouTrackIntellisense.INTELLISENSE_KEY, new YouTrackIntellisense(myRepository));
-
-    myOptions = new YouTrackOptionsTab();
-
-    Map<TaskState, String> 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<YouTrackRepos
   @Override
   public void apply() {
     myRepository.setDefaultSearch(myDefaultSearch.getText());
-    myRepository.setCustomStateName(TaskState.IN_PROGRESS, myOptions.getInProgressState().getText());
-    myRepository.setCustomStateName(TaskState.RESOLVED, myOptions.getResolvedState().getText());
     super.apply();
   }
 
index a4199be63b4077ee11b2f40d7993b986b0c05c75..67bbb0138f36d0f1396cf0784adc2485fa98f809 100644 (file)
@@ -18,10 +18,7 @@ package com.intellij.tasks.integration;
 import com.google.gson.Gson;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
-import com.intellij.tasks.Task;
-import com.intellij.tasks.TaskBundle;
-import com.intellij.tasks.TaskManagerTestCase;
-import com.intellij.tasks.TaskState;
+import com.intellij.tasks.*;
 import com.intellij.tasks.config.TaskSettings;
 import com.intellij.tasks.impl.LocalTaskImpl;
 import com.intellij.tasks.impl.TaskUtil;
@@ -197,7 +194,7 @@ public class JiraIntegrationTest extends TaskManagerTestCase {
   private void changeTaskStateAndCheck(@NotNull String issueKey) throws Exception {
     final Task original = myRepository.findTask(issueKey);
     assertNotNull(original);
-    myRepository.setTaskState(original, TaskState.IN_PROGRESS);
+    myRepository.setTaskState(original, new CustomTaskState("4", "In Progress"));
     final Task updated = myRepository.findTask(issueKey);
     assertNotNull(updated);
     assertEquals(TaskState.IN_PROGRESS, updated.getState());
diff --git a/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/YouTrackIntegrationTest.java b/plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/YouTrackIntegrationTest.java
new file mode 100644 (file)
index 0000000..f4577b8
--- /dev/null
@@ -0,0 +1,59 @@
+package com.intellij.tasks.integration;
+
+import com.intellij.tasks.CustomTaskState;
+import com.intellij.tasks.Task;
+import com.intellij.tasks.TaskManagerTestCase;
+import com.intellij.tasks.youtrack.YouTrackRepository;
+import com.intellij.tasks.youtrack.YouTrackRepositoryType;
+import com.intellij.util.Function;
+import com.intellij.util.containers.ContainerUtil;
+import org.jdom.Element;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class YouTrackIntegrationTest extends TaskManagerTestCase {
+  private static final String YOUTRACK_4_TEST_SERVER_URL = "http://trackers-tests.labs.intellij.net:8067";
+
+  private static final String REQUEST_WITH_CUSTOM_STATES_ID = "YT4CS-1";
+  private static final CustomTaskState NORTH_STATE = new CustomTaskState("North", "North");
+  private static final CustomTaskState SUBMITTED_STATE = new CustomTaskState("Submitted", "Submitted");
+
+  private YouTrackRepository myRepository;
+
+  public void testCustomTaskStates() throws Exception {
+    final Task task = myRepository.findTask(REQUEST_WITH_CUSTOM_STATES_ID);
+    assertNotNull(task);
+
+    final Set<CustomTaskState> states = myRepository.getAvailableTaskStates(task);
+    final List<String> stateNames = ContainerUtil.map(states, new Function<CustomTaskState, String>() {
+      @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");
+  }
+}
index 654b51261d8883e05eaa05628d8a221f2f2cbd1b..d9150d342ce2e0584fb3ea5e10d5edb6845f6028 100644 (file)
@@ -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<TrelloRepository> {
 
-  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<TrelloReposit
   private static final String CARD_1_1_1_NAME = "Card 1-1-1";
   private static final String CARD_1_1_1_ID = "53c416d8b4bd36fb078446e5";
 
+  // Labels and colors
+  private static final String LABELS_AND_COLORS_BOARD_NAME = "Labels and Colors";
+  private static final String COLORED_CARD_ID = "548591e00f3d598512ced37b";
+
+  // State updates
+  private static final String STATE_UPDATES_BOARD_NAME = "State Updates";
+  private static final String STATE_UPDATES_BOARD_ID = "54b3e0e3b4f415b3c9d03449";
+  private static final String BACKLOG_LIST_ID = "54b3e0e849e831746351e063";
+  private static final String IN_PROGRESS_LIST_ID = "54b3e0ebf5035aaddcbe15b4";
+  private static final String FEATURE_CARD_ID = "54b3e0efed4db033b634cd39";
+
   @Override
   protected TrelloRepository createRepository() throws Exception {
     try {
@@ -53,9 +67,9 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase<TrelloReposit
   // TODO Check various cards visibility corner cases
 
   public void testFetchBoard() throws Exception {
-    TrelloBoard board = myRepository.fetchBoardById(BOARD_1_ID);
+    TrelloBoard board = myRepository.fetchBoardById(BASIC_FUNCTIONALITY_BOARD_ID);
     assertNotNull(board);
-    assertEquals(BOARD_1_NAME, board.getName());
+    assertEquals(BASIC_FUNCTIONALITY_BOARD_NAME, board.getName());
   }
 
   public void testFetchList() throws Exception {
@@ -72,12 +86,13 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase<TrelloReposit
 
   public void testFetchBoardsOfUser() throws Exception {
     List<TrelloBoard> 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<TrelloList> lists = myRepository.fetchBoardLists();
@@ -88,7 +103,7 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase<TrelloReposit
   @NotNull
   private List<TrelloCard> 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<TrelloReposit
 
   public void testFetchingCardsOfBoard() throws Exception {
     myRepository.setIncludeAllCards(true);
-    List<TrelloCard> cards = fetchCards(BOARD_1_ID, null, true);
+    List<TrelloCard> 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<TrelloCard> allCards = fetchCards(BOARD_1_ID, LIST_1_1_ID, true);
+    List<TrelloCard> 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<TrelloCard> assignedCards = fetchCards(BOARD_1_ID, LIST_1_1_ID, true);
+    List<TrelloCard> 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<TrelloCard> allCards = fetchCards(BOARD_1_ID, LIST_1_1_NAME, true);
+    List<TrelloCard> 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<TrelloCard>() {
@@ -139,8 +154,17 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase<TrelloReposit
     assertFalse(card.isVisible());
   }
 
+  public void testTestConnection() throws Exception {
+    assertNull(myRepository.createCancellableConnection().call());
+
+    myRepository.setPassword("illegal password");
+    final Exception error = myRepository.createCancellableConnection().call();
+    assertNotNull(error);
+    assertTrue(error.getMessage().contains("Unauthorized"));
+  }
+
   public void testLabelsAndColors() throws Exception {
-    final TrelloCard card = myRepository.fetchCardById("548591e00f3d598512ced37b");
+    final TrelloCard card = myRepository.fetchCardById(COLORED_CARD_ID);
     assertNotNull(card);
     final List<TrelloLabel> labels = card.getLabels();
 
@@ -156,13 +180,41 @@ public class TrelloIntegrationTest extends LiveIntegrationTestCase<TrelloReposit
     assertEquals(EnumSet.of(SKY, LIME, PINK, BLACK), card.getColors());
   }
 
-  public void testTestConnection() throws Exception {
-    assertNull(myRepository.createCancellableConnection().call());
-
-    myRepository.setPassword("illegal password");
-    final Exception error = myRepository.createCancellableConnection().call();
-    assertNotNull(error);
-    assertTrue(error.getMessage().contains("Unauthorized"));
+  public void testStateUpdates() throws Exception {
+    TrelloCard card = myRepository.fetchCardById(FEATURE_CARD_ID);
+    assertNotNull(card);
+    assertEquals(STATE_UPDATES_BOARD_ID, card.getIdBoard());
+    assertEquals(BACKLOG_LIST_ID, card.getIdList());
+
+    // Discover "In Progress" list
+    TrelloTask task = new TrelloTask(card, myRepository);
+    Set<CustomTaskState> 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<? extends TrelloModel> objects, @NotNull String... names) {