Merge branch 'mikhail.golubev/configurable-issues-states'

Conflicts:
	plugins/tasks/tasks-tests/test/com/intellij/tasks/integration/JiraIntegrationTest.java
This commit is contained in:
Mikhail Golubev
2015-02-24 15:28:27 +03:00
33 changed files with 1040 additions and 393 deletions
@@ -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 {
@@ -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
@@ -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;
}
}
@@ -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 &amp;issue"/>
</properties>
</component>
<component id="5ff61" class="javax.swing.JLabel" binding="myTaskLabel">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
@@ -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 &amp;as:"/>
</properties>
</component>
<hspacer id="76017">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="cc401" class="com.intellij.tasks.impl.TaskStateCombo" binding="myStateCombo" custom-create="true">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
</children>
</grid>
</children>
</grid>
</form>
@@ -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="&amp;Clear current context"/>
</properties>
</component>
<component id="d4c2" class="javax.swing.JCheckBox" binding="myMarkAsInProgressBox" default-binding="true">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<focusable value="false"/>
<text value="Mark as 'In &amp;Progress'"/>
</properties>
</component>
</children>
</grid>
<grid id="29a80" layout-manager="GridLayoutManager" row-count="1" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
@@ -76,7 +67,7 @@
<grid id="637f6" binding="myVcsPanel" layout-manager="GridLayoutManager" row-count="2" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<clientProperties>
@@ -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="&amp;Mark as:"/>
</properties>
</component>
<hspacer id="94ebc">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="812b1" class="com.intellij.tasks.impl.TaskStateCombo" binding="myTaskStateCombo" custom-create="true">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
</component>
</children>
</grid>
</children>
</grid>
</form>
@@ -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 &quot;In Progress&quot; stated if any exists."/>
</properties>
<border type="none"/>
<children>
<grid id="3248b" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="2" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="etched" title="Custom states">
<color color="-16777216"/>
</border>
<children>
<component id="80263" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="e3c7a"/>
<text value="In &amp;Progress:"/>
</properties>
</component>
<component id="123f9" class="com.intellij.ui.components.JBLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="b21ba"/>
<text value="&amp;Resolved:"/>
</properties>
</component>
<component id="e3c7a" class="javax.swing.JTextField" binding="myInProgressState">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<text value=""/>
<toolTipText value="Actual name of &quot;In Progress&quot; state used in &quot;Open Task&quot; dialog"/>
</properties>
</component>
<component id="b21ba" class="javax.swing.JTextField" binding="myResolvedState">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<text value=""/>
<toolTipText value="Actual name of &quot;Resolved&quot; state used in &quot;Close Task&quot; dialog "/>
</properties>
</component>
<component id="cdcfa" class="com.intellij.ui.components.JBLabel" binding="myNoteText">
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false">
<preferred-size width="150" height="50"/>
</grid>
</constraints>
<properties>
<opaque value="false"/>
<text value="&lt;html&gt;&#10; &lt;head&gt;&#10; &#10; &lt;/head&gt;&#10; &lt;body&gt;&#10; &lt;/body&gt;&#10;&lt;/html&gt;&#10;"/>
</properties>
</component>
</children>
</grid>
<vspacer id="3db80">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -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);
}
}
@@ -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();
}
@@ -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());
@@ -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");
}
}
@@ -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) {