mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'mikhail.golubev/configurable-issues-states'
Conflicts: plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 + ')';
|
||||
}
|
||||
}
|
||||
@@ -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()}.
|
||||
*
|
||||
* @param task issue to update
|
||||
* @param state new state of the issue
|
||||
* @see com.intellij.tasks.TaskRepositoryType#getPossibleTaskStates()
|
||||
* @see com.intellij.tasks.TaskRepository#getFeatures()
|
||||
* @deprecated Use {@link #setTaskState(Task, CustomTaskState)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
|
||||
throw new UnsupportedOperationException("Setting task to state " + state + " is not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state of the task on server. It's guaranteed that only issues returned by {@link #getAvailableTaskStates(Task)}
|
||||
* will be passed here.
|
||||
* <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 TaskRepositoryType#getPossibleTaskStates()
|
||||
* @see TaskRepository#getFeatures()
|
||||
*/
|
||||
public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception {
|
||||
TaskState predefinedState = null;
|
||||
try {
|
||||
predefinedState = TaskState.valueOf(state.getId());
|
||||
}
|
||||
catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
if (predefinedState != null) {
|
||||
setTaskState(task, predefinedState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// for serialization
|
||||
public TaskRepository() {
|
||||
myType = null;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,16 +15,30 @@
|
||||
*/
|
||||
package com.intellij.tasks;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Predefined common task states were used before {@link CustomTaskState} was introduced.
|
||||
*
|
||||
* @author Dmitry Avdeev
|
||||
*/
|
||||
public enum TaskState {
|
||||
SUBMITTED("Submitted"),
|
||||
OPEN("Open"),
|
||||
IN_PROGRESS("In Progress"),
|
||||
REOPENED("Reopened"),
|
||||
RESOLVED("Resolved"),
|
||||
|
||||
SUBMITTED,
|
||||
OPEN,
|
||||
IN_PROGRESS,
|
||||
REOPENED,
|
||||
RESOLVED,
|
||||
OTHER("Other");
|
||||
|
||||
OTHER
|
||||
private String myPresentableName;
|
||||
|
||||
TaskState(@NotNull String presentableName) {
|
||||
myPresentableName = presentableName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getPresentableName() {
|
||||
return myPresentableName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+27
-14
@@ -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
|
||||
|
||||
+68
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
-13
@@ -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"));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.intellij.tasks.jira.soap;
|
||||
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.tasks.CustomTaskState;
|
||||
import com.intellij.tasks.LocalTask;
|
||||
import com.intellij.tasks.Task;
|
||||
import com.intellij.tasks.TaskBundle;
|
||||
import com.intellij.tasks.TaskState;
|
||||
import com.intellij.tasks.impl.TaskUtil;
|
||||
import com.intellij.tasks.jira.JiraRemoteApi;
|
||||
import com.intellij.tasks.jira.JiraRepository;
|
||||
@@ -19,7 +19,9 @@ import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Legacy integration restored due to IDEA-120595.
|
||||
@@ -102,8 +104,14 @@ public class JiraLegacyApi extends JiraRemoteApi {
|
||||
return ApiType.LEGACY;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public void setTaskState(@NotNull Task task, @NotNull TaskState state) throws Exception {
|
||||
public Set<CustomTaskState> getAvailableTaskStates(@NotNull Task task) throws Exception {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTaskState(@NotNull Task task, @NotNull CustomTaskState state) throws Exception {
|
||||
throw new Exception(TaskBundle.message("jira.failure.no.state.update"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"/>
|
||||
@@ -52,14 +52,6 @@
|
||||
</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 &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"/>
|
||||
@@ -73,6 +65,35 @@
|
||||
<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 &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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
@@ -31,21 +31,12 @@
|
||||
<text value="&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 &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>
|
||||
@@ -124,7 +115,9 @@
|
||||
<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>
|
||||
@@ -137,6 +130,34 @@
|
||||
</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="&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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "In Progress" 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 &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="&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 "In Progress" state used in "Open Task" 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 "Resolved" state used in "Close Task" 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="<html> <head> </head> <body> </body> </html> "/>
|
||||
</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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
// todo workaround for http://youtrack.jetbrains.net/issue/JT-7984
|
||||
String s = StreamUtil.readText(stream, CharsetToolkit.UTF8_CHARSET);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
if (!XMLChar.isValid(s.charAt(i))) {
|
||||
s = s.replace(s.charAt(i), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
Element element;
|
||||
try {
|
||||
//InputSource source = new InputSource(stream);
|
||||
//source.setEncoding("UTF-8");
|
||||
//element = new SAXBuilder(false).build(source).getRootElement();
|
||||
element = new SAXBuilder(false).build(new StringReader(s)).getRootElement();
|
||||
}
|
||||
catch (JDOMException e) {
|
||||
LOG.error("Can't parse YouTrack response for " + requestUrl, e);
|
||||
throw e;
|
||||
}
|
||||
if ("error".equals(element.getName())) {
|
||||
throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'");
|
||||
}
|
||||
InputStream stream = method.getResponseBodyAsStream();
|
||||
|
||||
List<Element> children = element.getChildren("issue");
|
||||
|
||||
final List<Task> tasks = ContainerUtil.mapNotNull(children, new NullableFunction<Element, Task>() {
|
||||
public Task fun(Element o) {
|
||||
return createIssue(o);
|
||||
// todo workaround for http://youtrack.jetbrains.net/issue/JT-7984
|
||||
String s = StreamUtil.readText(stream, CharsetToolkit.UTF8_CHARSET);
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
if (!XMLChar.isValid(s.charAt(i))) {
|
||||
s = s.replace(s.charAt(i), ' ');
|
||||
}
|
||||
}
|
||||
});
|
||||
return tasks.toArray(new Task[tasks.size()]);
|
||||
|
||||
Element element;
|
||||
try {
|
||||
//InputSource source = new InputSource(stream);
|
||||
//source.setEncoding("UTF-8");
|
||||
//element = new SAXBuilder(false).build(source).getRootElement();
|
||||
element = new SAXBuilder(false).build(new StringReader(s)).getRootElement();
|
||||
}
|
||||
catch (JDOMException e) {
|
||||
LOG.error("Can't parse YouTrack response for " + requestUrl, e);
|
||||
throw e;
|
||||
}
|
||||
if ("error".equals(element.getName())) {
|
||||
throw new Exception("Error from YouTrack for " + requestUrl + ": '" + element.getText() + "'");
|
||||
}
|
||||
|
||||
List<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()]);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
-17
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -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());
|
||||
|
||||
+59
@@ -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");
|
||||
}
|
||||
}
|
||||
+71
-19
@@ -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());
|
||||
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());
|
||||
|
||||
myRepository.setPassword("illegal password");
|
||||
final Exception error = myRepository.createCancellableConnection().call();
|
||||
assertNotNull(error);
|
||||
assertTrue(error.getMessage().contains("Unauthorized"));
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user