mirror of
https://github.com/theonedev/onedev.git
synced 2025-12-08 18:26:30 +00:00
chore: Add MCP support for issue operations
This commit is contained in:
parent
dc4af70ff1
commit
d9f9fa03e6
175
CLAUDE.md
Normal file
175
CLAUDE.md
Normal file
@ -0,0 +1,175 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
OneDev uses Maven as its build system with a multi-module structure.
|
||||
|
||||
### Essential Commands
|
||||
- **Build the project**: `mvn clean compile`
|
||||
- **Run tests**: `mvn test`
|
||||
- **Package the application**: `mvn clean package`
|
||||
- **Build without tests**: `mvn clean package -DskipTests`
|
||||
- **Build specific module**: `mvn clean package -pl server-core`
|
||||
- **Install to local repository**: `mvn clean install`
|
||||
|
||||
### Profiles
|
||||
- **Community Edition**: `mvn clean package -Pce` (excludes enterprise features)
|
||||
- **Default/Enterprise**: `mvn clean package` (includes all features)
|
||||
|
||||
### Testing
|
||||
- **Run all tests**: `mvn test`
|
||||
- **Run specific test class**: `mvn test -Dtest=ClassName`
|
||||
- **Run tests for specific module**: `mvn test -pl server-core`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
OneDev is a comprehensive DevOps platform built with a sophisticated multi-module Maven architecture:
|
||||
|
||||
### Core Technology Stack
|
||||
- **Web Framework**: Apache Wicket 7.18.0 (component-based UI)
|
||||
- **REST API**: Jersey 2.38 (JAX-RS implementation)
|
||||
- **Database/ORM**: Hibernate 5.4.24.Final with HikariCP connection pooling
|
||||
- **Web Server**: Embedded Jetty 9.4.57
|
||||
- **Dependency Injection**: Google Guice with custom plugin loading
|
||||
- **Security**: Apache Shiro for authentication/authorization
|
||||
- **Search**: Apache Lucene 8.7.0
|
||||
- **Git**: JGit 5.13.3 for Git operations
|
||||
- **Clustering**: Hazelcast 5.3.5 for distributed coordination
|
||||
|
||||
### Module Structure
|
||||
- **server-core**: Core application logic, entities, and services
|
||||
- **server-ee**: Enterprise edition features
|
||||
- **server-plugin**: Plugin framework and all plugin implementations
|
||||
- **server-product**: Final packaging and deployment artifacts
|
||||
|
||||
### Key Subsystems
|
||||
|
||||
#### 1. Application Bootstrap
|
||||
- Main entry point: `server-core/src/main/java/io/onedev/server/OneDev.java`
|
||||
- Module configuration: `server-core/src/main/java/io/onedev/server/CoreModule.java`
|
||||
- Handles server lifecycle, clustering, and graceful shutdown
|
||||
|
||||
#### 2. Entity Management
|
||||
Key domain entities and managers in `server-core/src/main/java/io/onedev/server/model/`:
|
||||
- Project, User, Group, Role management
|
||||
- Issue tracking with customizable workflows
|
||||
- Pull request lifecycle and code review
|
||||
- Build and CI/CD pipeline management
|
||||
- Package registry operations
|
||||
|
||||
#### 3. Git Integration
|
||||
- Full Git repository management via JGit
|
||||
- Git hooks for policy enforcement in `server-core/src/main/java/io/onedev/server/git/`
|
||||
- Code browsing, diff visualization, and blame tracking
|
||||
- SSH server for Git operations
|
||||
|
||||
#### 4. Web Layer (Wicket)
|
||||
- Component-based UI in `server-core/src/main/java/io/onedev/server/web/`
|
||||
- AJAX-heavy interface with WebSocket support
|
||||
- Project browsing, issue boards, pull request review interface
|
||||
|
||||
#### 5. REST API (Jersey)
|
||||
- RESTful services in `server-core/src/main/java/io/onedev/server/rest/`
|
||||
- Project, User, Build, Issue resources
|
||||
- WebHook endpoints and package registry APIs
|
||||
|
||||
#### 6. CI/CD System
|
||||
- YAML-based build specifications
|
||||
- Multi-executor support (Kubernetes, Docker, Shell)
|
||||
- Real-time log streaming and artifact management
|
||||
|
||||
#### 7. Plugin Architecture
|
||||
- Extensible plugin system in `server-plugin/`
|
||||
- Categories: build specs, executors, authenticators, importers, notifications, package registries, report processors
|
||||
- Plugin contributions via Guice modules
|
||||
|
||||
## Development Patterns
|
||||
|
||||
### Code Organization
|
||||
- **Package-by-feature**: Organized around business capabilities
|
||||
- **Dependency Injection**: Guice-based DI throughout the application
|
||||
- **Interface-based design**: For testability and modularity
|
||||
- **Custom annotations**: Extensive use for validation and metadata
|
||||
|
||||
### Design Patterns Used
|
||||
- Repository Pattern for data access
|
||||
- Observer Pattern for event handling
|
||||
- Command Pattern for Git operations
|
||||
- Strategy Pattern for pluggable components
|
||||
- Template Method for build processing
|
||||
|
||||
### Testing Strategy
|
||||
- Unit tests in `src/test/java` directories
|
||||
- Git operation tests with test repositories
|
||||
- Component and integration tests
|
||||
- Utility method tests
|
||||
- Focus on testing business logic and Git operations
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Working with Entities
|
||||
- Entities are in `server-core/src/main/java/io/onedev/server/model/`
|
||||
- Use corresponding managers for database operations
|
||||
- Follow JPA/Hibernate patterns for persistence
|
||||
|
||||
### Adding REST Endpoints
|
||||
- Create resources in `server-core/src/main/java/io/onedev/server/rest/resource/`
|
||||
- Follow Jersey/JAX-RS patterns
|
||||
- Use existing security annotations for authentication
|
||||
|
||||
### Creating Plugins
|
||||
- Extend `AbstractPlugin` class
|
||||
- Implement appropriate interfaces for the plugin category
|
||||
- Add Guice module configuration
|
||||
- Place in appropriate `server-plugin/server-plugin-*` module
|
||||
|
||||
### Working with Git
|
||||
- Use JGit APIs through OneDev's Git service layer
|
||||
- Follow patterns in `server-core/src/main/java/io/onedev/server/git/`
|
||||
- Handle Git operations asynchronously when possible
|
||||
|
||||
### Adding Web Components
|
||||
- Create Wicket components in `server-core/src/main/java/io/onedev/server/web/`
|
||||
- Follow existing component patterns and CSS frameworks
|
||||
- Use AJAX for dynamic behavior
|
||||
|
||||
## Configuration and Deployment
|
||||
|
||||
### Key Configuration Files
|
||||
- `server-product/system/conf/server.properties`: HTTP/SSH ports, clustering
|
||||
- `server-product/system/conf/hibernate.properties`: Database configuration
|
||||
- `server-product/system/conf/logback.xml`: Logging configuration
|
||||
|
||||
### Deployment Options
|
||||
- Standalone JAR with embedded Jetty
|
||||
- Docker containers (see `server-product/docker/`)
|
||||
- Kubernetes via Helm charts (see `server-product/helm/`)
|
||||
|
||||
### Database Support
|
||||
- PostgreSQL (recommended for production)
|
||||
- MySQL/MariaDB
|
||||
- HSQLDB (development/testing)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching
|
||||
- Hibernate second-level cache with Hazelcast
|
||||
- Build artifact caching
|
||||
- Git object caching
|
||||
- Web resource bundling
|
||||
|
||||
### Clustering
|
||||
- Hazelcast-based clustering for high availability
|
||||
- Distributed session management
|
||||
- Leader election for coordinated operations
|
||||
|
||||
## Important Notes
|
||||
|
||||
- OneDev uses a custom plugin loading framework
|
||||
- Git operations are central to the application architecture
|
||||
- The system supports both community (CE) and enterprise (EE) editions
|
||||
- Extensive use of Guice for dependency injection and plugin management
|
||||
- Focus on performance and resource efficiency
|
||||
- Battle-tested in production environments for 5+ years
|
||||
@ -71,6 +71,7 @@ import io.onedev.commons.utils.ExceptionUtils;
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.k8shelper.KubernetesHelper;
|
||||
import io.onedev.k8shelper.OsInfo;
|
||||
import io.onedev.server.ai.McpHelperResource;
|
||||
import io.onedev.server.attachment.AttachmentManager;
|
||||
import io.onedev.server.attachment.DefaultAttachmentManager;
|
||||
import io.onedev.server.buildspec.job.log.instruction.LogInstruction;
|
||||
@ -647,6 +648,7 @@ public class CoreModule extends AbstractPluginModule {
|
||||
contribute(FilterChainConfigurator.class, filterChainManager -> filterChainManager.createChain("/~api/**", "noSessionCreation, authcBasic, authcBearer"));
|
||||
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.packages(ProjectResource.class.getPackage().getName()));
|
||||
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.register(ClusterResource.class));
|
||||
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.register(McpHelperResource.class));
|
||||
}
|
||||
|
||||
private void configureWeb() {
|
||||
|
||||
@ -0,0 +1,986 @@
|
||||
package io.onedev.server.ai;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAcceptableException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.shiro.authz.UnauthenticatedException;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.SubscriptionManager;
|
||||
import io.onedev.server.entitymanager.IssueChangeManager;
|
||||
import io.onedev.server.entitymanager.IssueCommentManager;
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
import io.onedev.server.entitymanager.IssueManager;
|
||||
import io.onedev.server.entitymanager.IssueWorkManager;
|
||||
import io.onedev.server.entitymanager.IterationManager;
|
||||
import io.onedev.server.entitymanager.LinkSpecManager;
|
||||
import io.onedev.server.entitymanager.ProjectManager;
|
||||
import io.onedev.server.entitymanager.SettingManager;
|
||||
import io.onedev.server.entitymanager.UserManager;
|
||||
import io.onedev.server.entityreference.IssueReference;
|
||||
import io.onedev.server.exception.LinkValidationException;
|
||||
import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.IssueComment;
|
||||
import io.onedev.server.model.IssueLink;
|
||||
import io.onedev.server.model.IssueSchedule;
|
||||
import io.onedev.server.model.IssueWork;
|
||||
import io.onedev.server.model.Iteration;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
|
||||
import io.onedev.server.model.support.issue.field.FieldUtils;
|
||||
import io.onedev.server.model.support.issue.field.spec.BooleanField;
|
||||
import io.onedev.server.model.support.issue.field.spec.DateField;
|
||||
import io.onedev.server.model.support.issue.field.spec.DateTimeField;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.model.support.issue.field.spec.FloatField;
|
||||
import io.onedev.server.model.support.issue.field.spec.GroupChoiceField;
|
||||
import io.onedev.server.model.support.issue.field.spec.IntegerField;
|
||||
import io.onedev.server.model.support.issue.field.spec.choicefield.ChoiceField;
|
||||
import io.onedev.server.model.support.issue.field.spec.userchoicefield.UserChoiceField;
|
||||
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
import io.onedev.server.search.entity.EntityQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.ProjectScope;
|
||||
|
||||
@Api(internal = true)
|
||||
@Path("/mcp-helper")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Singleton
|
||||
public class McpHelperResource {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final SettingManager settingManager;
|
||||
|
||||
private final UserManager userManager;
|
||||
|
||||
private final IssueManager issueManager;
|
||||
|
||||
private final ProjectManager projectManager;
|
||||
|
||||
private final LinkSpecManager linkSpecManager;
|
||||
|
||||
private final IssueLinkManager issueLinkManager;
|
||||
|
||||
private final IssueCommentManager issueCommentManager;
|
||||
|
||||
private final IterationManager iterationManager;
|
||||
|
||||
private final IssueChangeManager issueChangeManager;
|
||||
|
||||
private final IssueWorkManager issueWorkManager;
|
||||
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
|
||||
@Inject
|
||||
public McpHelperResource(ObjectMapper objectMapper, SettingManager settingManager,
|
||||
UserManager userManager, IssueManager issueManager, ProjectManager projectManager,
|
||||
LinkSpecManager linkSpecManager, IssueCommentManager issueCommentManager,
|
||||
IterationManager iterationManager, SubscriptionManager subscriptionManager,
|
||||
IssueChangeManager issueChangeManager, IssueLinkManager issueLinkManager,
|
||||
IssueWorkManager issueWorkManager) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.settingManager = settingManager;
|
||||
this.issueManager = issueManager;
|
||||
this.userManager = userManager;
|
||||
this.projectManager = projectManager;
|
||||
this.linkSpecManager = linkSpecManager;
|
||||
this.issueCommentManager = issueCommentManager;
|
||||
this.iterationManager = iterationManager;
|
||||
this.subscriptionManager = subscriptionManager;
|
||||
this.issueChangeManager = issueChangeManager;
|
||||
this.issueLinkManager = issueLinkManager;
|
||||
this.issueWorkManager = issueWorkManager;
|
||||
}
|
||||
|
||||
private String getIssueQueryStringDescription() {
|
||||
var stateNames = new StringBuilder();
|
||||
for (var state: settingManager.getIssueSetting().getStateSpecs()) {
|
||||
stateNames.append(" - ");
|
||||
stateNames.append(state.getName());
|
||||
if (state.getDescription() != null) {
|
||||
stateNames.append(": ").append(state.getDescription().replace("\n", " "));
|
||||
}
|
||||
stateNames.append("\n");
|
||||
}
|
||||
var fieldCriterias = new StringBuilder();
|
||||
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
if (field instanceof ChoiceField) {
|
||||
var choiceField = (ChoiceField) field;
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
|
||||
+ field.getName() + "\" is \"<" + field.getName().toLowerCase()
|
||||
+ " value>\" (quotes are required), where <" + field.getName().toLowerCase()
|
||||
+ " value> is one of below:\n");
|
||||
for (var choice : choiceField.getPossibleValues())
|
||||
fieldCriterias.append(" - " + choice).append("\n");
|
||||
} else if (field instanceof UserChoiceField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
|
||||
+ field.getName() + "\" is \"<login name of a user>\" (quotes are required)\n");
|
||||
fieldCriterias.append(
|
||||
"- " + field.getName().toLowerCase() + " criteria for current user in form of: \""
|
||||
+ field.getName() + "\" is me (quotes are required)\n");
|
||||
} else if (field instanceof GroupChoiceField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
|
||||
+ field.getName() + "\" is \"<group name>\" (quotes are required)\n");
|
||||
} else if (field instanceof BooleanField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " is true criteria in form of: \""
|
||||
+ field.getName() + "\" is \"true\" (quotes are required)\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " is false criteria in form of: \""
|
||||
+ field.getName() + "\" is \"false\" (quotes are required)\n");
|
||||
} else if (field instanceof DateField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is before certain date criteria in form of: \"" + field.getName()
|
||||
+ "\" is before \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is after certain date criteria in form of: \"" + field.getName()
|
||||
+ "\" is after \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
|
||||
} else if (field instanceof DateTimeField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is before certain date time criteria in form of: \"" + field.getName()
|
||||
+ "\" is before \"<date time>\" (quotes are required), where <date time> is of format YYYY-MM-DD HH:mm\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is after certain date time criteria in form of: \"" + field.getName()
|
||||
+ "\" is after \"<date time>\" (quotes are required), where <date time> is of format YYYY-MM-DD HH:mm\n");
|
||||
} else if (field instanceof IntegerField) {
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is equal to certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is greater than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is greater than \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase()
|
||||
+ " is less than certain integer criteria in form of: \"" + field.getName()
|
||||
+ "\" is less than \"<integer>\" (quotes are required), where <integer> is an integer\n");
|
||||
}
|
||||
fieldCriterias.append("- " + field.getName().toLowerCase() + " is not set criteria in form of: \""
|
||||
+ field.getName() + "\" is empty (quotes are required)\n");
|
||||
}
|
||||
var linkCriterias = new StringBuilder();
|
||||
for (var linkSpec: linkSpecManager.query()) {
|
||||
linkCriterias.append("- criteria to list issues with any " + linkSpec.getName().toLowerCase()
|
||||
+ " issues matching certain criteria in form of: any \"" + linkSpec.getName()
|
||||
+ "\" matching(another criteria) (quotes are required)\n");
|
||||
linkCriterias.append("- criteria to list issues with all " + linkSpec.getName().toLowerCase()
|
||||
+ " issues matching certain criteria in form of: all \"" + linkSpec.getName()
|
||||
+ "\" matching(another criteria) (quotes are required)\n");
|
||||
linkCriterias.append("- criteria to list issues with some " + linkSpec.getName().toLowerCase()
|
||||
+ " issues in form of: has any \"" + linkSpec.getName() + "\" (quotes are required)\n");
|
||||
if (linkSpec.getOpposite() != null) {
|
||||
linkCriterias.append("- criteria to list issues with any "
|
||||
+ linkSpec.getOpposite().getName().toLowerCase()
|
||||
+ " issues matching certain criteria in form of: any \"" + linkSpec.getOpposite().getName()
|
||||
+ "\" matching(another criteria) (quotes are required)\n");
|
||||
linkCriterias.append("- criteria to list issues with all "
|
||||
+ linkSpec.getOpposite().getName().toLowerCase()
|
||||
+ " issues matching certain criteria in form of: all \"" + linkSpec.getOpposite().getName()
|
||||
+ "\" matching(another criteria) (quotes are required)\n");
|
||||
linkCriterias.append("- criteria to list issues with some " + linkSpec.getOpposite().getName().toLowerCase()
|
||||
+ " issues in form of: has any \"" + linkSpec.getOpposite().getName() + "\" (quotes are required)\n");
|
||||
}
|
||||
}
|
||||
var orderFields = new StringBuilder();
|
||||
for (var field: Issue.SORT_FIELDS.keySet()) {
|
||||
orderFields.append("- ").append(field).append("\n");
|
||||
}
|
||||
|
||||
return
|
||||
"A query string is one of below criteria:\n" +
|
||||
"- Issue with specified number in form of: \"Number\" is \"#<issue number>\", or in form of: \"Number\" is \"<project key>-<issue number>\" (quotes are required)\n" +
|
||||
"- Text based criteria in form of: ~<containing text>~\n" +
|
||||
"- State criteria in form of: \"State\" is \"<state name>\" (quotes are required), where <state name> is one of below:\n" +
|
||||
stateNames +
|
||||
fieldCriterias +
|
||||
linkCriterias +
|
||||
"- submitter criteria in form of: \"Submitter\" is \"<login name of a user>\" (quotes are required)\n" +
|
||||
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
|
||||
"- submitted before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
|
||||
"- confidential criteria in form of: confidential\n" +
|
||||
"- iteration criteria in form of: \"Iteration\" is \"<iteration name>\" (quotes are required)\n" +
|
||||
"- and criteria in form of <criteria1> and <criteria2>\n" +
|
||||
"- or criteria in form of <criteria1> or <criteria2>. Note that \"and criteria\" takes precedence over \"or criteria\", use braces to group \"or criteria\" like \"(criteria1 or criteria2) and criteria3\" if you want to override precedence\n" +
|
||||
"- not criteria in form of not(<criteria>)\n" +
|
||||
"\n" +
|
||||
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n" +
|
||||
orderFields +
|
||||
"\n" +
|
||||
"Leave empty to search all issues";
|
||||
}
|
||||
|
||||
private String getToolParamName(String fieldName) {
|
||||
return fieldName.replace(" ", "_");
|
||||
}
|
||||
|
||||
private String appendDescription(String description, String additionalDescription) {
|
||||
if (description.length() > 0) {
|
||||
if (description.endsWith("."))
|
||||
return description + " " + additionalDescription;
|
||||
else
|
||||
return description + ". " + additionalDescription;
|
||||
} else {
|
||||
return additionalDescription;
|
||||
}
|
||||
}
|
||||
|
||||
private Project getProject(String projectPath) {
|
||||
var project = projectManager.findByPath(projectPath);
|
||||
if (project == null)
|
||||
throw new NotFoundException("Project not found: " + projectPath);
|
||||
if (!SecurityUtils.canAccessProject(project))
|
||||
throw new UnauthorizedException("Unable to access project: " + projectPath);
|
||||
return project;
|
||||
}
|
||||
|
||||
private Map<String, Object> getFieldProperties(FieldSpec field) {
|
||||
String fieldDescription;
|
||||
if (field.getDescription() != null)
|
||||
fieldDescription = field.getDescription().replace("\n", " ");
|
||||
else
|
||||
fieldDescription = "";
|
||||
if (field instanceof ChoiceField) {
|
||||
var choiceField = (ChoiceField) field;
|
||||
if (field.isAllowMultiple())
|
||||
fieldDescription = appendDescription(fieldDescription,
|
||||
"Expects one or more of: " + String.join(", ", choiceField.getPossibleValues()));
|
||||
else
|
||||
fieldDescription = appendDescription(fieldDescription,
|
||||
"Expects one of: " + String.join(", ", choiceField.getPossibleValues()));
|
||||
} else if (field instanceof UserChoiceField) {
|
||||
if (field.isAllowMultiple())
|
||||
fieldDescription = appendDescription(fieldDescription, "Expects user login names");
|
||||
else
|
||||
fieldDescription = appendDescription(fieldDescription, "Expects user login name");
|
||||
} else if (field instanceof GroupChoiceField) {
|
||||
} else if (field instanceof BooleanField) {
|
||||
fieldDescription = appendDescription(fieldDescription, "Expects boolean value, true or false");
|
||||
} else if (field instanceof IntegerField) {
|
||||
fieldDescription = appendDescription(fieldDescription, "Expects integer value");
|
||||
} else if (field instanceof FloatField) {
|
||||
fieldDescription = appendDescription(fieldDescription, "Expects float value");
|
||||
} else if (field instanceof DateField || field instanceof DateTimeField) {
|
||||
if (field.isAllowMultiple())
|
||||
fieldDescription = appendDescription(fieldDescription,
|
||||
"Expects unix timestamps in milliseconds since epoch");
|
||||
else
|
||||
fieldDescription = appendDescription(fieldDescription,
|
||||
"Expects unix timestamp in milliseconds since epoch");
|
||||
}
|
||||
|
||||
var fieldProperties = new HashMap<String, Object>();
|
||||
if (field.isAllowMultiple()) {
|
||||
fieldProperties.putAll(getArrayProperties(fieldDescription));
|
||||
} else {
|
||||
fieldProperties.put("type", "string");
|
||||
fieldProperties.put("description", fieldDescription);
|
||||
}
|
||||
return fieldProperties;
|
||||
}
|
||||
|
||||
private Map<String, Object> getArrayProperties(String description) {
|
||||
return Map.of(
|
||||
"type", "array",
|
||||
"items", Map.of("type", "string"),
|
||||
"uniqueItems", true,
|
||||
"description", description);
|
||||
}
|
||||
|
||||
@Path("/get-tool-input-schemas")
|
||||
@GET
|
||||
public Map<String, Object> getToolInputSchemas(@QueryParam("currentProject") @NotNull String currentProjectPath) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
Project.push(currentProject);
|
||||
try {
|
||||
var inputSchemas = new HashMap<String, Object>();
|
||||
|
||||
var queryIssuesInputSchema = new HashMap<String, Object>();
|
||||
var queryIssuesProperties = new HashMap<String, Object>();
|
||||
|
||||
queryIssuesProperties.put("query", Map.of(
|
||||
"type", "string",
|
||||
"description", getIssueQueryStringDescription()));
|
||||
queryIssuesProperties.put("offset", Map.of(
|
||||
"type", "integer",
|
||||
"description", "start position for the query (optional, defaults to 0)"));
|
||||
queryIssuesProperties.put("count", Map.of(
|
||||
"type", "integer",
|
||||
"description", "number of issues to return (optional, defaults to 25, max 100)"));
|
||||
|
||||
queryIssuesInputSchema.put("Type", "object");
|
||||
queryIssuesInputSchema.put("Properties", queryIssuesProperties);
|
||||
queryIssuesInputSchema.put("Required", new ArrayList<>());
|
||||
|
||||
inputSchemas.put("queryIssues", queryIssuesInputSchema);
|
||||
|
||||
var createIssueInputSchema = new HashMap<String, Object>();
|
||||
var createIssueProperties = new HashMap<String, Object>();
|
||||
createIssueProperties.put("title", Map.of(
|
||||
"type", "string",
|
||||
"description", "title of the issue"));
|
||||
createIssueProperties.put("description", Map.of(
|
||||
"type", "string",
|
||||
"description", "description of the issue"));
|
||||
createIssueProperties.put("confidential", Map.of(
|
||||
"type", "boolean",
|
||||
"description", "whether the issue is confidential"));
|
||||
|
||||
if (SecurityUtils.canScheduleIssues(currentProject)) {
|
||||
createIssueProperties.put("iterations", getArrayProperties("iteration names"));
|
||||
|
||||
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
|
||||
createIssueProperties.put("ownEstimatedTime", Map.of(
|
||||
"type", "integer",
|
||||
"description", "Estimated time in hours for this issue only (not including linked issues)"));
|
||||
}
|
||||
}
|
||||
|
||||
var createIssueRequiredProperties = new ArrayList<String>();
|
||||
createIssueRequiredProperties.add("title");
|
||||
|
||||
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
if (field.isApplicable(currentProject)
|
||||
&& field.isPromptUponIssueOpen()
|
||||
&& SecurityUtils.canEditIssueField(currentProject, field.getName())) {
|
||||
var paramName = getToolParamName(field.getName());
|
||||
var fieldProperties = getFieldProperties(field);
|
||||
createIssueProperties.put(paramName, fieldProperties);
|
||||
|
||||
if (!field.isAllowEmpty() && field.getShowCondition() == null) {
|
||||
createIssueRequiredProperties.add(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createIssueInputSchema.put("Type", "object");
|
||||
createIssueInputSchema.put("Properties", createIssueProperties);
|
||||
createIssueInputSchema.put("Required", createIssueRequiredProperties);
|
||||
|
||||
inputSchemas.put("createIssue", createIssueInputSchema);
|
||||
|
||||
var updateIssueInputSchema = new HashMap<String, Object>();
|
||||
var updateIssueProperties = new HashMap<String, Object>();
|
||||
updateIssueProperties.put("issueReference", Map.of(
|
||||
"type", "string",
|
||||
"description", "reference of the issue to update"));
|
||||
updateIssueProperties.put("title", Map.of(
|
||||
"type", "string",
|
||||
"description", "title of the issue"));
|
||||
updateIssueProperties.put("description", Map.of(
|
||||
"type", "string",
|
||||
"description", "description of the issue"));
|
||||
updateIssueProperties.put("confidential", Map.of(
|
||||
"type", "boolean",
|
||||
"description", "whether the issue is confidential"));
|
||||
|
||||
if (SecurityUtils.canScheduleIssues(currentProject)) {
|
||||
updateIssueProperties.put("iterations", getArrayProperties("iterations to schedule the issue in"));
|
||||
|
||||
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
|
||||
updateIssueProperties.put("ownEstimatedTime", Map.of(
|
||||
"type", "integer",
|
||||
"description", "Estimated time in hours for this issue only (not including linked issues)"));
|
||||
}
|
||||
}
|
||||
|
||||
for (var field : settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
if (SecurityUtils.canEditIssueField(currentProject, field.getName())) {
|
||||
var paramName = getToolParamName(field.getName());
|
||||
|
||||
var fieldProperties = getFieldProperties(field);
|
||||
updateIssueProperties.put(paramName, fieldProperties);
|
||||
}
|
||||
}
|
||||
|
||||
updateIssueInputSchema.put("Type", "object");
|
||||
updateIssueInputSchema.put("Properties", updateIssueProperties);
|
||||
updateIssueInputSchema.put("Required", List.of("issueReference"));
|
||||
|
||||
inputSchemas.put("updateIssue", updateIssueInputSchema);
|
||||
|
||||
var toStates = new HashSet<String>();
|
||||
for (var transition: settingManager.getIssueSetting().getTransitionSpecs()) {
|
||||
if (transition instanceof ManualSpec) {
|
||||
if (transition.getToStates().isEmpty()) {
|
||||
toStates.addAll(settingManager.getIssueSetting().getStateSpecMap().keySet());
|
||||
break;
|
||||
} else {
|
||||
toStates.addAll(transition.getToStates());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toStates.size() > 0) {
|
||||
var transitionInputSchema = new HashMap<String, Object>();
|
||||
transitionInputSchema.put("Type", "object");
|
||||
|
||||
var transitIssueProperties = new HashMap<String, Object>();
|
||||
transitIssueProperties.put("issueReference", Map.of(
|
||||
"type", "string",
|
||||
"description", "reference of the issue to transit state"));
|
||||
transitIssueProperties.put("state", Map.of(
|
||||
"type", "string",
|
||||
"description", "new state of the issue after transition. Must be one of: " + String.join(", ", toStates)));
|
||||
transitIssueProperties.put("comment", Map.of(
|
||||
"type", "string",
|
||||
"description", "comment of the transition"));
|
||||
|
||||
for (var field : settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
if (SecurityUtils.canEditIssueField(currentProject, field.getName())) {
|
||||
var paramName = getToolParamName(field.getName());
|
||||
|
||||
var fieldProperties = getFieldProperties(field);
|
||||
transitIssueProperties.put(paramName, fieldProperties);
|
||||
}
|
||||
}
|
||||
|
||||
transitionInputSchema.put("Properties", transitIssueProperties);
|
||||
transitionInputSchema.put("Required", List.of("issueReference", "state"));
|
||||
|
||||
inputSchemas.put("transitIssue", transitionInputSchema);
|
||||
}
|
||||
|
||||
var linkSpecs = linkSpecManager.query();
|
||||
if (!linkSpecs.isEmpty()) {
|
||||
var linkInputSchema = new HashMap<String, Object>();
|
||||
linkInputSchema.put("Type", "object");
|
||||
|
||||
var linkProperties = new HashMap<String, Object>();
|
||||
linkProperties.put("sourceIssueReference", Map.of(
|
||||
"type", "string",
|
||||
"description", "Issue reference as source of the link"));
|
||||
linkProperties.put("targetIssueReference", Map.of(
|
||||
"type", "string",
|
||||
"description", "Issue reference as target of the link"
|
||||
));
|
||||
var linkNames = new ArrayList<String>();
|
||||
for (var linkSpec: linkSpecs) {
|
||||
linkNames.add(linkSpec.getName());
|
||||
if (linkSpec.getOpposite() != null)
|
||||
linkNames.add(linkSpec.getOpposite().getName());
|
||||
}
|
||||
linkProperties.put("linkName", Map.of(
|
||||
"type", "string",
|
||||
"description", "Name of the link. Must be one of: " + String.join(", ", linkNames)));
|
||||
linkInputSchema.put("Properties", linkProperties);
|
||||
linkInputSchema.put("Required", List.of("sourceIssueReference", "targetIssueReference", "linkName"));
|
||||
|
||||
inputSchemas.put("linkIssues", linkInputSchema);
|
||||
}
|
||||
|
||||
return inputSchemas;
|
||||
} finally {
|
||||
Project.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/get-prompt-arguments")
|
||||
@GET
|
||||
public Map<String, Object> getPromptArguments(@QueryParam("currentProject") @NotNull String currentProjectPath) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
Project.push(currentProject);
|
||||
try {
|
||||
var arguments = new LinkedHashMap<String, Object>();
|
||||
var createIssueArguments = new ArrayList<Map<String, Object>>();
|
||||
createIssueArguments.add(Map.of(
|
||||
"name", "title",
|
||||
"description", "title of the issue",
|
||||
"required", true));
|
||||
createIssueArguments.add(Map.of(
|
||||
"name", "description",
|
||||
"description", "description of the issue",
|
||||
"required", false));
|
||||
createIssueArguments.add(Map.of(
|
||||
"name", "confidential",
|
||||
"description", "whether the issue is confidential",
|
||||
"required", false));
|
||||
|
||||
if (SecurityUtils.canScheduleIssues(currentProject)) {
|
||||
createIssueArguments.add(Map.of(
|
||||
"name", "iterations",
|
||||
"description", "iterations to schedule the issue in",
|
||||
"required", false));
|
||||
|
||||
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
|
||||
createIssueArguments.add(Map.of(
|
||||
"name", "ownEstimatedTime",
|
||||
"description", "estimated time for this issue only in hours (excluding linked issues)",
|
||||
"required", false));
|
||||
}
|
||||
}
|
||||
|
||||
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
if (field.isApplicable(currentProject)
|
||||
&& field.isPromptUponIssueOpen()
|
||||
&& field.getShowCondition() == null
|
||||
&& SecurityUtils.canEditIssueField(currentProject, field.getName())) {
|
||||
var paramName = getToolParamName(field.getName());
|
||||
String description = "";
|
||||
if (field.getDescription() != null)
|
||||
description = field.getDescription().replace("\n", " ");
|
||||
if (field instanceof ChoiceField) {
|
||||
var choiceField = (ChoiceField) field;
|
||||
if (field.isAllowMultiple())
|
||||
description = appendDescription(description, "Expects one or more of: " + String.join(", ", choiceField.getPossibleValues()));
|
||||
else
|
||||
description = appendDescription(description, "Expects one of: " + String.join(", ", choiceField.getPossibleValues()));
|
||||
}
|
||||
var argumentMap = new HashMap<String, Object>();
|
||||
argumentMap.put("name", paramName);
|
||||
argumentMap.put("required", !field.isAllowEmpty());
|
||||
if (description != null)
|
||||
argumentMap.put("description", description);
|
||||
createIssueArguments.add(argumentMap);
|
||||
}
|
||||
}
|
||||
|
||||
arguments.put("createIssue", createIssueArguments);
|
||||
return arguments;
|
||||
} finally {
|
||||
Project.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/get-login-name")
|
||||
@GET
|
||||
public String getLoginName(@QueryParam("userName") String userName) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
User user;
|
||||
if (userName != null) {
|
||||
user = userManager.findByName(userName);
|
||||
if (user == null)
|
||||
user = userManager.findByFullName(userName);
|
||||
if (user == null) {
|
||||
var matchingUsers = new ArrayList<User>();
|
||||
var lowerCaseUserName = userName.toLowerCase();
|
||||
for (var eachUser: userManager.query()) {
|
||||
if (eachUser.getFullName() != null) {
|
||||
if (Splitter.on(" ").trimResults().omitEmptyStrings().splitToList(eachUser.getFullName().toLowerCase()).contains(lowerCaseUserName)) {
|
||||
matchingUsers.add(eachUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchingUsers.size() == 1) {
|
||||
user = matchingUsers.get(0);
|
||||
} else if (matchingUsers.size() > 1) {
|
||||
throw new InvalidParamsException("Multiple users found: " + userName);
|
||||
}
|
||||
}
|
||||
if (user == null)
|
||||
throw new NotFoundException("User not found: " + userName);
|
||||
} else {
|
||||
user = SecurityUtils.getUser();
|
||||
}
|
||||
return user.getName();
|
||||
}
|
||||
|
||||
@Path("/get-unix-timestamp")
|
||||
@GET
|
||||
public long getUnixTimestamp(@QueryParam("dateTimeDescription") @NotNull String dateTimeDescription) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
return DateUtils.parseRelaxed(dateTimeDescription).getTime();
|
||||
}
|
||||
|
||||
@Path("/query-issues")
|
||||
@GET
|
||||
public List<Map<String, Object>> queryIssues(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("query") String query, @QueryParam("offset") int offset, @QueryParam("count") int count) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
if (count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
EntityQuery<Issue> parsedQuery;
|
||||
if (query != null) {
|
||||
var option = new IssueQueryParseOption();
|
||||
option.withCurrentUserCriteria(true);
|
||||
parsedQuery = IssueQuery.parse(currentProject, query, option, true);
|
||||
} else {
|
||||
parsedQuery = new IssueQuery();
|
||||
}
|
||||
|
||||
var issues = new ArrayList<Map<String, Object>>();
|
||||
for (var issue : issueManager.query(new ProjectScope(currentProject, true, false), parsedQuery, true, offset, count)) {
|
||||
var issueMap = getIssueMap(currentProject, issue);
|
||||
for (var entry: issue.getFieldInputs().entrySet()) {
|
||||
issueMap.put(entry.getKey(), entry.getValue().getValues());
|
||||
}
|
||||
issues.add(issueMap);
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
private Issue getIssue(Project currentProject, String referenceString) {
|
||||
var issueReference = IssueReference.of(referenceString, currentProject);
|
||||
var issue = issueManager.find(issueReference.getProject(), issueReference.getNumber());
|
||||
if (issue != null) {
|
||||
if (!SecurityUtils.canAccessIssue(issue))
|
||||
throw new UnauthorizedException("No permission to access issue: " + referenceString);
|
||||
return issue;
|
||||
} else {
|
||||
throw new NotFoundException("Issue not found: " + referenceString);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> getIssueMap(Project currentProject, Issue issue) {
|
||||
var typeReference = new TypeReference<Map<String, Object>>() {};
|
||||
var issueMap = objectMapper.convertValue(issue, typeReference);
|
||||
issueMap.put("reference", issue.getReference().toString(currentProject));
|
||||
issueMap.remove("submitterId");
|
||||
issueMap.put("Submitter", issue.getSubmitter().getName());
|
||||
issueMap.remove("projectId");
|
||||
issueMap.put("Project", issue.getProject().getPath());
|
||||
return issueMap;
|
||||
}
|
||||
|
||||
@Path("/get-issue-detail")
|
||||
@GET
|
||||
public Map<String, Object> getIssueDetail(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("reference") @NotNull String issueReference) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
|
||||
var issueMap = getIssueMap(currentProject, issue);
|
||||
for (var entry : issue.getFieldInputs().entrySet()) {
|
||||
issueMap.put(entry.getKey(), entry.getValue().getValues());
|
||||
}
|
||||
|
||||
issueMap.put("comments", issue.getComments());
|
||||
|
||||
Map<String, Collection<String>> linkedIssues = new HashMap<>();
|
||||
for (var link: issue.getTargetLinks()) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getTarget().getReference().toString(currentProject));
|
||||
}
|
||||
for (var link : issue.getSourceLinks()) {
|
||||
if (link.getSpec().getOpposite() != null) {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
} else {
|
||||
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
|
||||
.add(link.getSource().getReference().toString(currentProject));
|
||||
}
|
||||
}
|
||||
issueMap.putAll(linkedIssues);
|
||||
|
||||
return issueMap;
|
||||
}
|
||||
|
||||
@Path("/add-issue-comment")
|
||||
@Consumes(MediaType.TEXT_PLAIN)
|
||||
@POST
|
||||
public String addIssueComment(@QueryParam("currentProject") String currentProjectPath,
|
||||
@QueryParam("reference") @NotNull String issueReference, @NotNull String commentContent) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
var comment = new IssueComment();
|
||||
comment.setIssue(issue);
|
||||
comment.setContent(commentContent);
|
||||
comment.setUser(SecurityUtils.getAuthUser());
|
||||
comment.setDate(new Date());
|
||||
issueCommentManager.create(comment);
|
||||
|
||||
return "Commented on issue " + issueReference;
|
||||
}
|
||||
|
||||
@Path("/create-issue")
|
||||
@POST
|
||||
public String createIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@NotNull @Valid Map<String, Serializable> data) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
normalizeData(data);
|
||||
|
||||
var issueSetting = settingManager.getIssueSetting();
|
||||
|
||||
Issue issue = new Issue();
|
||||
var title = (String) data.remove("title");
|
||||
if (title == null)
|
||||
throw new InvalidParamsException("title is required");
|
||||
issue.setTitle(title);
|
||||
var description = (String) data.remove("description");
|
||||
issue.setDescription(description);
|
||||
var confidential = (Boolean) data.remove("confidential");
|
||||
if (confidential != null)
|
||||
issue.setConfidential(confidential);
|
||||
|
||||
Integer ownEstimatedTime = (Integer) data.remove("ownEstimatedTime");
|
||||
if (ownEstimatedTime != null) {
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new NotAcceptableException("An active subscription is required for this feature");
|
||||
if (!currentProject.isTimeTracking())
|
||||
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
|
||||
if (!SecurityUtils.canScheduleIssues(currentProject))
|
||||
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
|
||||
issue.setOwnEstimatedTime(ownEstimatedTime*60);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> iterationNames = (List<String>) data.remove("iterations");
|
||||
if (iterationNames != null) {
|
||||
if (!SecurityUtils.canScheduleIssues(currentProject))
|
||||
throw new UnauthorizedException("Issue schedule permission required to set iterations");
|
||||
for (var iterationName : iterationNames) {
|
||||
var iteration = iterationManager.findInHierarchy(currentProject, iterationName);
|
||||
if (iteration == null)
|
||||
throw new NotFoundException("Iteration '" + iterationName + "' not found");
|
||||
IssueSchedule schedule = new IssueSchedule();
|
||||
schedule.setIssue(issue);
|
||||
schedule.setIteration(iteration);
|
||||
issue.getSchedules().add(schedule);
|
||||
}
|
||||
}
|
||||
|
||||
issue.setProject(currentProject);
|
||||
issue.setSubmitDate(new Date());
|
||||
issue.setSubmitter(SecurityUtils.getAuthUser());
|
||||
issue.setState(issueSetting.getInitialStateSpec().getName());
|
||||
|
||||
issue.setFieldValues(FieldUtils.getFieldValues(issue.getProject(), data));
|
||||
|
||||
try {
|
||||
issueManager.open(issue);
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
|
||||
}
|
||||
|
||||
return "Created issue " + issue.getReference().toString(currentProject);
|
||||
}
|
||||
|
||||
@Path("/update-issue")
|
||||
@POST
|
||||
public String updateIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("reference") @NotNull String issueReference, @NotNull Map<String, Serializable> data) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
if (!SecurityUtils.canModifyIssue(issue))
|
||||
throw new UnauthorizedException();
|
||||
|
||||
normalizeData(data);
|
||||
|
||||
var title = (String) data.remove("title");
|
||||
if (title != null)
|
||||
issueChangeManager.changeTitle(issue, title);
|
||||
|
||||
if (data.containsKey("description"))
|
||||
issueChangeManager.changeDescription(issue, (String) data.remove("description"));
|
||||
|
||||
var confidential = (Boolean) data.remove("confidential");
|
||||
if (confidential != null)
|
||||
issueChangeManager.changeConfidential(issue, confidential);
|
||||
|
||||
Integer ownEstimatedTime = (Integer) data.remove("ownEstimatedTime");
|
||||
if (ownEstimatedTime != null) {
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new NotAcceptableException("An active subscription is required for this feature");
|
||||
if (!issue.getProject().isTimeTracking())
|
||||
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
|
||||
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
|
||||
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
|
||||
issueChangeManager.changeOwnEstimatedTime(issue, ownEstimatedTime*60);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> iterationNames = (List<String>) data.remove("iterations");
|
||||
if (iterationNames != null) {
|
||||
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
|
||||
throw new UnauthorizedException("Issue schedule permission required to set iterations");
|
||||
var iterations = new ArrayList<Iteration>();
|
||||
for (var iterationName : iterationNames) {
|
||||
var iteration = iterationManager.findInHierarchy(issue.getProject(), iterationName);
|
||||
if (iteration == null)
|
||||
throw new NotFoundException("Iteration '" + iterationName + "' not found");
|
||||
iterations.add(iteration);
|
||||
}
|
||||
issueChangeManager.changeIterations(issue, iterations);
|
||||
}
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
var issueSetting = settingManager.getIssueSetting();
|
||||
String initialState = issueSetting.getInitialStateSpec().getName();
|
||||
|
||||
if (!SecurityUtils.canManageIssues(issue.getProject())
|
||||
&& !(issue.getSubmitter().equals(SecurityUtils.getAuthUser())
|
||||
&& issue.getState().equals(initialState))) {
|
||||
throw new UnauthorizedException("No permission to update issue fields");
|
||||
}
|
||||
|
||||
try {
|
||||
issueChangeManager.changeFields(issue, FieldUtils.getFieldValues(issue.getProject(), data));
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
|
||||
}
|
||||
}
|
||||
|
||||
return "Updated issue " + issueReference;
|
||||
}
|
||||
|
||||
@Path("/transit-issue")
|
||||
@POST
|
||||
public String transitIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("reference") @NotNull String issueReference, @NotNull Map<String, Serializable> data) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
normalizeData(data);
|
||||
var state = (String) data.remove("state");
|
||||
if (state == null)
|
||||
throw new InvalidParamsException("state is required");
|
||||
var comment = (String) data.remove("comment");
|
||||
ManualSpec transition = settingManager.getIssueSetting().getManualSpec(issue, state);
|
||||
|
||||
var fieldValues = FieldUtils.getFieldValues(issue.getProject(), data);
|
||||
try {
|
||||
issueChangeManager.changeState(issue, state, fieldValues, transition.getPromptFields(),
|
||||
transition.getRemoveFields(), comment);
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
|
||||
}
|
||||
return "Issue " + issueReference + " transited to state \"" + state + "\"";
|
||||
}
|
||||
|
||||
@Path("/link-issues")
|
||||
@GET
|
||||
public String linkIssues(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("sourceReference") @NotNull String sourceReference,
|
||||
@QueryParam("linkName") @Nullable String linkName,
|
||||
@QueryParam("targetReference") @NotNull String targetReference) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var sourceIssue = getIssue(currentProject, sourceReference);
|
||||
var targetIssue = getIssue(currentProject, targetReference);
|
||||
var linkSpec = linkSpecManager.find(linkName);
|
||||
if (linkSpec == null)
|
||||
throw new NotFoundException("Link spec not found: " + linkName);
|
||||
if (!SecurityUtils.canEditIssueLink(sourceIssue.getProject(), linkSpec)
|
||||
|| !SecurityUtils.canEditIssueLink(targetIssue.getProject(), linkSpec)) {
|
||||
throw new UnauthorizedException("No permission to add specified link for specified issues");
|
||||
}
|
||||
|
||||
var link = new IssueLink();
|
||||
link.setSpec(linkSpec);
|
||||
if (linkName.equals(linkSpec.getName())) {
|
||||
link.setSource(sourceIssue);
|
||||
link.setTarget(targetIssue);
|
||||
} else {
|
||||
link.setSource(targetIssue);
|
||||
link.setTarget(sourceIssue);
|
||||
}
|
||||
try {
|
||||
link.validate();
|
||||
} catch (LinkValidationException e) {
|
||||
throw new NotAcceptableException(e.getMessage());
|
||||
}
|
||||
issueLinkManager.create(link);
|
||||
|
||||
return "Issue " + targetReference + " added as \"" + linkName + "\" of " + sourceReference;
|
||||
}
|
||||
|
||||
@Path("/log-work")
|
||||
@Consumes(MediaType.TEXT_PLAIN)
|
||||
@POST
|
||||
public String logWork(@QueryParam("currentProject") @NotNull String currentProjectPath,
|
||||
@QueryParam("reference") @NotNull String issueReference,
|
||||
@QueryParam("spentHours") int spentHours, @Nullable String comment) {
|
||||
if (SecurityUtils.getAuthUser() == null)
|
||||
throw new UnauthenticatedException();
|
||||
var currentProject = getProject(currentProjectPath);
|
||||
var issue = getIssue(currentProject, issueReference);
|
||||
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new NotAcceptableException("An active subscription is required for this feature");
|
||||
if (!issue.getProject().isTimeTracking())
|
||||
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
|
||||
if (!SecurityUtils.canAccessIssue(issue))
|
||||
throw new UnauthorizedException("No permission to access issue: " + issueReference);
|
||||
|
||||
var work = new IssueWork();
|
||||
work.setIssue(issue);
|
||||
work.setUser(SecurityUtils.getAuthUser());
|
||||
work.setMinutes(spentHours * 60);
|
||||
work.setNote(StringUtils.trimToNull(comment));
|
||||
issueWorkManager.createOrUpdate(work);
|
||||
return "Work logged for issue " + issueReference;
|
||||
}
|
||||
|
||||
private void normalizeData(Map<String, Serializable> data) {
|
||||
for (var entry: data.entrySet()) {
|
||||
if (entry.getValue() instanceof String)
|
||||
entry.setValue(StringUtils.trimToNull((String) entry.getValue()));
|
||||
}
|
||||
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
|
||||
var paramName = getToolParamName(field.getName());
|
||||
if (!paramName.equals(field.getName()) && data.containsKey(paramName)) {
|
||||
data.put(field.getName(), data.get(paramName));
|
||||
data.remove(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -14,7 +14,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
|
||||
public class ParamCombination implements Serializable {
|
||||
|
||||
|
||||
@ -5,8 +5,9 @@ import io.onedev.server.annotation.ChoiceProvider;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.buildspec.param.ParamCombination;
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.model.Build;
|
||||
import io.onedev.server.util.Input;
|
||||
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
|
||||
|
||||
@ -11,8 +11,8 @@ import javax.validation.constraints.NotEmpty;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.buildspec.param.ParamCombination;
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.model.Build;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.annotation.ChoiceProvider;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package io.onedev.server.util;
|
||||
package io.onedev.server.buildspecmodel.inputspec;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
@ -11,8 +11,6 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
|
||||
public class Input implements Serializable {
|
||||
@ -82,7 +80,7 @@ public class Input implements Serializable {
|
||||
else
|
||||
displayValue = "" + getValues();
|
||||
|
||||
logger.error("Error converting field value (field: {}, value: {}, error: {})",
|
||||
logger.error("Error converting input (name: {}, value: {}, error: {})",
|
||||
getName(), displayValue, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
@ -51,7 +51,7 @@ public interface IssueChangeManager extends EntityManager<IssueChange> {
|
||||
void removeSchedule(Issue issue, Iteration iteration);
|
||||
|
||||
void changeState(Issue issue, String state, Map<String, Object> fieldValues,
|
||||
Collection<String> removeFields, @Nullable String comment);
|
||||
Collection<String> promptFields, Collection<String> removeFields, @Nullable String comment);
|
||||
|
||||
void batchUpdate(Iterator<? extends Issue> issues, @Nullable String state, @Nullable Boolean confidential,
|
||||
@Nullable Collection<Iteration> iterations, Map<String, Object> fieldValues,
|
||||
|
||||
@ -41,6 +41,7 @@ import io.onedev.commons.utils.match.Matcher;
|
||||
import io.onedev.commons.utils.match.PathMatcher;
|
||||
import io.onedev.commons.utils.match.StringMatcher;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.cluster.ClusterManager;
|
||||
import io.onedev.server.entitymanager.IssueChangeManager;
|
||||
import io.onedev.server.entitymanager.IssueDescriptionRevisionManager;
|
||||
@ -86,6 +87,7 @@ import io.onedev.server.model.support.issue.changedata.IssueStateChangeData;
|
||||
import io.onedev.server.model.support.issue.changedata.IssueTitleChangeData;
|
||||
import io.onedev.server.model.support.issue.changedata.IssueTotalEstimatedTimeChangeData;
|
||||
import io.onedev.server.model.support.issue.changedata.IssueTotalSpentTimeChangeData;
|
||||
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
|
||||
import io.onedev.server.model.support.issue.transitionspec.BranchUpdatedSpec;
|
||||
import io.onedev.server.model.support.issue.transitionspec.BuildSuccessfulSpec;
|
||||
import io.onedev.server.model.support.issue.transitionspec.IssueStateTransitedSpec;
|
||||
@ -112,7 +114,6 @@ import io.onedev.server.search.entity.issue.StateCriteria;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.taskschedule.SchedulableTask;
|
||||
import io.onedev.server.taskschedule.TaskScheduler;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.ProjectScope;
|
||||
import io.onedev.server.util.ProjectScopedCommit;
|
||||
import io.onedev.server.util.concurrent.BatchWorkManager;
|
||||
@ -377,13 +378,14 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
create(change, null, sendNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void changeFields(Issue issue, Map<String, Object> fieldValues) {
|
||||
Map<String, Input> prevFields = issue.getFieldInputs();
|
||||
issue.setFieldValues(fieldValues);
|
||||
if (!prevFields.equals(issue.getFieldInputs())) {
|
||||
issue.checkEmptyFields();
|
||||
if (!prevFields.equals(issue.getFieldInputs())) {
|
||||
issueFieldManager.saveFields(issue);
|
||||
|
||||
IssueChange change = new IssueChange();
|
||||
@ -397,14 +399,16 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
@Transactional
|
||||
@Override
|
||||
public void changeState(Issue issue, String state, Map<String, Object> fieldValues,
|
||||
Collection<String> removeFields, @Nullable String comment) {
|
||||
Collection<String> promptFields, Collection<String> removeFields, @Nullable String comment) {
|
||||
String prevState = issue.getState();
|
||||
Map<String, Input> prevFields = issue.getFieldInputs();
|
||||
|
||||
issue.setState(state);
|
||||
issue.removeFields(removeFields);
|
||||
issue.setFieldValues(fieldValues);
|
||||
|
||||
issue.addMissingFields(promptFields);
|
||||
issue.checkEmptyFields();
|
||||
|
||||
issueFieldManager.saveFields(issue);
|
||||
|
||||
IssueChange change = new IssueChange();
|
||||
@ -436,6 +440,12 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
issueScheduleManager.syncIterations(issue, iterations);
|
||||
|
||||
issue.setFieldValues(fieldValues);
|
||||
try {
|
||||
issue.checkEmptyFields();
|
||||
} catch (EmptyFieldsException e) {
|
||||
Collection<String> emptyFields = e.getEmptyFields();
|
||||
throw new EmptyFieldsException("The following fields must be set for issue " + issue.getReference().toString(issue.getProject()) + ": " + String.join(", ", emptyFields), emptyFields);
|
||||
}
|
||||
issueFieldManager.saveFields(issue);
|
||||
|
||||
if (!prevState.equals(issue.getState())
|
||||
@ -496,7 +506,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
String message = "State changed as issue " + issue.getReference().toString(each.getProject())
|
||||
+ " transited to '" + issue.getState() + "'";
|
||||
changeState(each, issueStateTransitedSpec.getToState(), new HashMap<>(),
|
||||
transition.getRemoveFields(), message);
|
||||
new ArrayList<>(), transition.getRemoveFields(), message);
|
||||
}
|
||||
} finally {
|
||||
Issue.pop();
|
||||
@ -545,7 +555,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
for (Issue issue: issueManager.query(projectScope, query, true, 0, MAX_VALUE)) {
|
||||
String message = "State changed as build " + build.getReference().toString(issue.getProject()) + " is successful";
|
||||
changeState(issue, buildSuccessfulSpec.getToState(), new HashMap<>(),
|
||||
transition.getRemoveFields(), message);
|
||||
new ArrayList<>(), transition.getRemoveFields(), message);
|
||||
}
|
||||
} finally {
|
||||
Build.pop();
|
||||
@ -585,8 +595,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
try {
|
||||
for (Issue issue: issueManager.query(projectScope, query, true, 0, MAX_VALUE)) {
|
||||
String statusName = request.getStatus().toString().toLowerCase();
|
||||
changeState(issue, pullRequestSpec.getToState(), new HashMap<>(),
|
||||
transition.getRemoveFields(),
|
||||
changeState(issue, pullRequestSpec.getToState(), new HashMap<>(), new ArrayList<>(), transition.getRemoveFields(),
|
||||
"State changed as pull request " + request.getReference().toString(issue.getProject()) + " is " + statusName);
|
||||
}
|
||||
} finally {
|
||||
@ -697,8 +706,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
String commitFQN = commit.name();
|
||||
if (!project.equals(issue.getProject()))
|
||||
commitFQN = project.getPath() + ":" + commitFQN;
|
||||
changeState(issue, branchUpdatedSpec.getToState(), new HashMap<>(),
|
||||
transition.getRemoveFields(),
|
||||
changeState(issue, branchUpdatedSpec.getToState(), new HashMap<>(), new ArrayList<>(), transition.getRemoveFields(),
|
||||
"State changed as code fixing the issue is committed (" + commitFQN + ")");
|
||||
}
|
||||
}
|
||||
@ -745,7 +753,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
|
||||
query = new IssueQuery(Criteria.andCriterias(criterias), new ArrayList<>());
|
||||
|
||||
for (Issue issue: issueManager.query(null, query, true, 0, MAX_VALUE)) {
|
||||
changeState(issue, noActivitySpec.getToState(), new HashMap<>(),
|
||||
changeState(issue, noActivitySpec.getToState(), new HashMap<>(), new ArrayList<>(),
|
||||
transition.getRemoveFields(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +241,10 @@ public class DefaultIssueManager extends BaseEntityManager<Issue> implements Iss
|
||||
lastActivity.setDescription("opened");
|
||||
lastActivity.setDate(issue.getSubmitDate());
|
||||
issue.setLastActivity(lastActivity);
|
||||
|
||||
|
||||
issue.addMissingFields(settingManager.getIssueSetting().getPromptFieldsUponIssueOpen(issue.getProject()));
|
||||
issue.checkEmptyFields();
|
||||
|
||||
dao.persist(issue);
|
||||
|
||||
fieldManager.saveFields(issue);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package io.onedev.server.event.project.issue;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.entitymanager.GroupManager;
|
||||
import io.onedev.server.entitymanager.UserManager;
|
||||
import io.onedev.server.model.Group;
|
||||
@ -8,7 +9,6 @@ import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.util.CommitAware;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.ProjectScopedCommit;
|
||||
import io.onedev.server.util.commenttext.CommentText;
|
||||
import io.onedev.server.util.commenttext.MarkdownText;
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package io.onedev.server.exception;
|
||||
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
|
||||
public class LinkValidationException extends ExplicitException {
|
||||
|
||||
public LinkValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@ -76,6 +76,7 @@ import io.onedev.server.buildspec.job.Job;
|
||||
import io.onedev.server.buildspec.param.ParamCombination;
|
||||
import io.onedev.server.buildspec.param.ParamUtils;
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
|
||||
import io.onedev.server.cluster.ClusterManager;
|
||||
import io.onedev.server.entitymanager.AccessTokenManager;
|
||||
@ -94,7 +95,6 @@ import io.onedev.server.search.entity.SortField;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.FilenameUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.artifact.ArtifactInfo;
|
||||
import io.onedev.server.util.artifact.DirectoryInfo;
|
||||
import io.onedev.server.util.criteria.Criteria;
|
||||
|
||||
@ -23,6 +23,7 @@ import static java.util.Comparator.comparing;
|
||||
import static java.util.Comparator.comparingInt;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@ -70,6 +71,7 @@ import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.attachment.AttachmentStorageSupport;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.entitymanager.GroupManager;
|
||||
import io.onedev.server.entitymanager.PullRequestManager;
|
||||
@ -81,13 +83,14 @@ import io.onedev.server.model.support.EntityWatch;
|
||||
import io.onedev.server.model.support.LastActivity;
|
||||
import io.onedev.server.model.support.ProjectBelonging;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.model.support.issue.field.FieldUtils;
|
||||
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.search.entity.EntitySort;
|
||||
import io.onedev.server.search.entity.IssueSortField;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.ProjectScopedCommit;
|
||||
import io.onedev.server.util.facade.IssueFacade;
|
||||
import io.onedev.server.web.UrlManager;
|
||||
@ -1023,6 +1026,37 @@ public class Issue extends ProjectBelonging implements AttachmentStorageSupport
|
||||
public boolean isFieldVisible(String fieldName) {
|
||||
return isFieldVisible(fieldName, Sets.newHashSet());
|
||||
}
|
||||
|
||||
public void addMissingFields(Collection<String> fieldNames) {
|
||||
try {
|
||||
var fieldBean = FieldUtils.getFieldBeanClass().getConstructor().newInstance();
|
||||
var existingFieldNames = getFieldNames();
|
||||
var fieldValues = FieldUtils.getFieldValues(null, fieldBean, fieldNames);
|
||||
for (var entry: fieldValues.entrySet()) {
|
||||
if (!existingFieldNames.contains(entry.getKey()))
|
||||
setFieldValue(entry.getKey(), entry.getValue());
|
||||
}
|
||||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkEmptyFields() {
|
||||
var emptyFields = new ArrayList<String>();
|
||||
for (var fieldName: getFieldNames()) {
|
||||
var field = getIssueSetting().getFieldSpec(fieldName);
|
||||
if (field != null
|
||||
&& !field.isAllowEmpty()
|
||||
&& isFieldVisible(fieldName)
|
||||
&& SecurityUtils.canEditIssueField(getProject(), fieldName)
|
||||
&& getFieldValue(fieldName) == null) {
|
||||
emptyFields.add(field.getName());
|
||||
}
|
||||
}
|
||||
if (emptyFields.size() > 0)
|
||||
throw new EmptyFieldsException(emptyFields);
|
||||
}
|
||||
|
||||
public List<User> getParticipants() {
|
||||
if (participants == null) {
|
||||
|
||||
@ -8,6 +8,8 @@ import javax.persistence.ManyToOne;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.UniqueConstraint;
|
||||
|
||||
import io.onedev.server.exception.LinkValidationException;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
indexes={@Index(columnList="o_source_id"), @Index(columnList="o_target_id"), @Index(columnList="o_spec_id")},
|
||||
@ -62,4 +64,37 @@ public class IssueLink extends AbstractEntity {
|
||||
return issue.equals(source)?target:source;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (getSource().equals(getTarget()))
|
||||
throw new LinkValidationException("Can not link to self");
|
||||
if (getSpec().getOpposite() != null) {
|
||||
if (getSource().getTargetLinks().stream()
|
||||
.anyMatch(it -> it.getSpec().equals(getSpec()) && it.getTarget().equals(getTarget())))
|
||||
throw new LinkValidationException("Source issue already linked to target issue via specified link spec");
|
||||
if (!getSpec().isMultiple()
|
||||
&& getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
|
||||
throw new LinkValidationException(
|
||||
"Link spec is not multiple and the source issue is already linked to another issue via this link spec");
|
||||
if (!getSpec().getParsedIssueQuery(getSource().getProject()).matches(getTarget()))
|
||||
throw new LinkValidationException("Link spec not allowed to link to the target issue");
|
||||
if (!getSpec().getOpposite().isMultiple()
|
||||
&& getTarget().getSourceLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
|
||||
throw new LinkValidationException(
|
||||
"Opposite side of link spec is not multiple and the target issue is already linked to another issue via this link spec");
|
||||
if (!getSpec().getOpposite().getParsedIssueQuery(getSource().getProject())
|
||||
.matches(getSource()))
|
||||
throw new LinkValidationException("Opposite side of link spec not allowed to link to the source issue");
|
||||
} else {
|
||||
if (getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())
|
||||
&& it.getLinked(getSource()).equals(getTarget())))
|
||||
throw new LinkValidationException("Specified issues already linked via specified link spec");
|
||||
if (!getSpec().isMultiple()
|
||||
&& getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
|
||||
throw new LinkValidationException(
|
||||
"Link spec is not multiple and source issue is already linked to another issue via this link spec");
|
||||
var parsedIssueQuery = getSpec().getParsedIssueQuery(getSource().getProject());
|
||||
if (!parsedIssueQuery.matches(getSource()) || !parsedIssueQuery.matches(getTarget()))
|
||||
throw new LinkValidationException("Link spec not allowed to link specified issues");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package io.onedev.server.model.support.administration;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@ -17,8 +18,6 @@ import com.google.common.collect.Lists;
|
||||
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.commons.utils.match.Matcher;
|
||||
import io.onedev.commons.utils.match.PathMatcher;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.Choice;
|
||||
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.SpecifiedChoices;
|
||||
@ -41,9 +40,9 @@ import io.onedev.server.model.support.issue.transitionspec.BranchUpdatedSpec;
|
||||
import io.onedev.server.model.support.issue.transitionspec.IssueStateTransitedSpec;
|
||||
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
|
||||
import io.onedev.server.model.support.issue.transitionspec.TransitionSpec;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.search.entity.issue.IssueQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
|
||||
import io.onedev.server.util.patternset.PatternSet;
|
||||
import io.onedev.server.util.usage.Usage;
|
||||
import io.onedev.server.web.component.issue.workflowreconcile.ReconcileUtils;
|
||||
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldResolution;
|
||||
@ -750,17 +749,31 @@ public class GlobalIssueSetting implements Serializable {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Collection<String> getPromptFieldsUponIssueOpen(Project project) {
|
||||
Matcher matcher = new PathMatcher();
|
||||
return getFieldSpecs().stream()
|
||||
.filter(it->it.isPromptUponIssueOpen() && (it.getApplicableProjects() == null || PatternSet.parse(it.getApplicableProjects()).matches(matcher, project.getPath())))
|
||||
.map(it->it.getName())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public int getStateOrdinal(String state) {
|
||||
return getStateSpecs().indexOf(getStateSpec(state));
|
||||
}
|
||||
|
||||
public ManualSpec getManualSpec(Issue issue, String state) {
|
||||
for (var transition: getTransitionSpecs()) {
|
||||
if (transition instanceof ManualSpec) {
|
||||
var manualSpec = (ManualSpec) transition;
|
||||
if (manualSpec.canTransit(issue, state) && manualSpec.isAuthorized(issue)) {
|
||||
return manualSpec;
|
||||
}
|
||||
}
|
||||
}
|
||||
var message = MessageFormat.format(
|
||||
"No applicable manual transition spec found for current user (issue: {0}, from state: {1}, to state: {2})",
|
||||
issue.getReference().toString(), issue.getState(), state);
|
||||
throw new InvalidParamsException(message);
|
||||
}
|
||||
|
||||
public Collection<String> getPromptFieldsUponIssueOpen(Project project) {
|
||||
return getFieldSpecs().stream()
|
||||
.filter(it -> it.isPromptUponIssueOpen() && it.isApplicable(project))
|
||||
.map(it -> it.getName())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.model.Iteration;
|
||||
import io.onedev.server.util.Input;
|
||||
|
||||
public class IssueBatchUpdateData extends IssueFieldChangeData {
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.entitymanager.GroupManager;
|
||||
import io.onedev.server.entitymanager.UserManager;
|
||||
@ -20,7 +21,6 @@ import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.notification.ActivityDetail;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
|
||||
public class IssueFieldChangeData extends IssueChangeData {
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ package io.onedev.server.model.support.issue.changedata;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.notification.ActivityDetail;
|
||||
import io.onedev.server.util.Input;
|
||||
|
||||
public class IssueStateChangeData extends IssueFieldChangeData {
|
||||
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package io.onedev.server.model.support.issue.field;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
|
||||
public class EmptyFieldsException extends ExplicitException {
|
||||
|
||||
private final Collection<String> emptyFields;
|
||||
|
||||
public EmptyFieldsException(Collection<String> emptyFields) {
|
||||
this("The following fields must be set: " + String.join(", ", emptyFields), emptyFields);
|
||||
}
|
||||
|
||||
public EmptyFieldsException(String message, Collection<String> emptyFields) {
|
||||
super(message);
|
||||
this.emptyFields = emptyFields;
|
||||
}
|
||||
|
||||
public Collection<String> getEmptyFields() {
|
||||
return emptyFields;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package io.onedev.server.model.support.issue.field;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -9,7 +10,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.ValidationException;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.apache.wicket.Component;
|
||||
import org.apache.wicket.MarkupContainer;
|
||||
import org.apache.wicket.MetaDataKey;
|
||||
@ -20,16 +23,16 @@ import org.slf4j.LoggerFactory;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.entitymanager.SettingManager;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputContext;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.model.support.issue.field.spec.SecretField;
|
||||
import io.onedev.server.entitymanager.SettingManager;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.model.support.issue.field.instance.FieldInstance;
|
||||
import io.onedev.server.model.support.issue.field.instance.SpecifiedValue;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.model.support.issue.field.spec.SecretField;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.EditContext;
|
||||
@ -103,8 +106,9 @@ public class FieldUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map<String, Object> getFieldValues(ComponentContext context, Serializable fieldBean, Collection<String> fieldNames) {
|
||||
ComponentContext.push(context);
|
||||
public static Map<String, Object> getFieldValues(@Nullable ComponentContext context, Serializable fieldBean, Collection<String> fieldNames) {
|
||||
if (context != null)
|
||||
ComponentContext.push(context);
|
||||
try {
|
||||
Map<String, Object> fieldValues = new HashMap<>();
|
||||
BeanDescriptor beanDescriptor = new BeanDescriptor(fieldBean.getClass());
|
||||
@ -117,7 +121,8 @@ public class FieldUtils {
|
||||
|
||||
return fieldValues;
|
||||
} finally {
|
||||
ComponentContext.pop();
|
||||
if (context != null)
|
||||
ComponentContext.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +177,30 @@ public class FieldUtils {
|
||||
validateFieldMap(fieldSpecs, fieldMap);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> getFieldValues(Project project, Map<String, Serializable> fieldEdits) {
|
||||
var settingManager = OneDev.getInstance(SettingManager.class);
|
||||
var issueSetting = settingManager.getIssueSetting();
|
||||
Map<String, Object> fieldValues = new HashMap<>();
|
||||
for (Map.Entry<String, Serializable> entry : fieldEdits.entrySet()) {
|
||||
var fieldName = entry.getKey();
|
||||
var fieldSpec = issueSetting.getFieldSpec(fieldName);
|
||||
if (fieldSpec == null)
|
||||
throw new BadRequestException("Undefined field: " + fieldName);
|
||||
if (!SecurityUtils.canEditIssueField(project, fieldName))
|
||||
throw new UnauthorizedException("No permission to edit field: " + fieldName);
|
||||
|
||||
List<String> values = new ArrayList<>();
|
||||
if (entry.getValue() instanceof String) {
|
||||
values.add((String) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Collection) {
|
||||
values.addAll((Collection<String>) entry.getValue());
|
||||
}
|
||||
fieldValues.put(entry.getKey(), fieldSpec.convertToObject(values));
|
||||
}
|
||||
return fieldValues;
|
||||
}
|
||||
|
||||
public static boolean isFieldVisible(BeanDescriptor beanDescriptor, Serializable fieldBean, String fieldName) {
|
||||
String propertyName = getPropertyName(beanDescriptor, fieldName);
|
||||
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(fieldBean.getClass(), propertyName);
|
||||
|
||||
@ -1,32 +1,40 @@
|
||||
package io.onedev.server.model.support.issue.field.spec;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
import org.apache.wicket.MarkupContainer;
|
||||
|
||||
import io.onedev.commons.codeassist.InputSuggestion;
|
||||
import io.onedev.commons.utils.match.PathMatcher;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.annotation.FieldName;
|
||||
import io.onedev.server.annotation.Multiline;
|
||||
import io.onedev.server.entitymanager.SettingManager;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.annotation.Patterns;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputContext;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.SpecifiedChoices;
|
||||
import io.onedev.server.buildspecmodel.inputspec.showcondition.ShowCondition;
|
||||
import io.onedev.server.buildspecmodel.inputspec.showcondition.ValueIsNotAnyOf;
|
||||
import io.onedev.server.buildspecmodel.inputspec.showcondition.ValueIsOneOf;
|
||||
import io.onedev.server.entitymanager.SettingManager;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.EditContext;
|
||||
import io.onedev.server.util.patternset.PatternSet;
|
||||
import io.onedev.server.util.usage.Usage;
|
||||
import io.onedev.server.annotation.FieldName;
|
||||
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldResolution;
|
||||
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValue;
|
||||
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValuesResolution;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.annotation.Patterns;
|
||||
import io.onedev.server.web.util.SuggestionUtils;
|
||||
import org.apache.wicket.MarkupContainer;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.*;
|
||||
|
||||
@Editable
|
||||
public abstract class FieldSpec extends InputSpec {
|
||||
@ -362,6 +370,11 @@ public abstract class FieldSpec extends InputSpec {
|
||||
|
||||
return usage.prefix("custom field '" + getName() + "'");
|
||||
}
|
||||
|
||||
public boolean isApplicable(Project project) {
|
||||
var matcher = new PathMatcher();
|
||||
return getApplicableProjects() == null || PatternSet.parse(getApplicableProjects()).matches(matcher, project.getPath());
|
||||
}
|
||||
|
||||
protected void onDeleteProject(Usage usage, String projectPath) {
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
package io.onedev.server.rest;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
||||
public class InvalidParamException extends BadRequestException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public InvalidParamException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidParamException(String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package io.onedev.server.rest;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
||||
public class InvalidParamsException extends BadRequestException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public InvalidParamsException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidParamsException(String message, Exception cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@ -45,12 +45,12 @@ public class ParamCheckFilter implements ContainerRequestFilter {
|
||||
Set<String> suppliedQueryParams = new HashSet<>(uriInfo.getQueryParameters().keySet());
|
||||
suppliedQueryParams.removeAll(definedQueryParams);
|
||||
if (!suppliedQueryParams.isEmpty())
|
||||
throw new InvalidParamException("Unexpected query params: " + suppliedQueryParams);
|
||||
throw new InvalidParamsException("Unexpected query params: " + suppliedQueryParams);
|
||||
}
|
||||
|
||||
requiredQueryParams.removeAll(uriInfo.getQueryParameters().keySet());
|
||||
if (!requiredQueryParams.isEmpty())
|
||||
throw new InvalidParamException("Missing query params: " + requiredQueryParams);
|
||||
throw new InvalidParamsException("Missing query params: " + requiredQueryParams);
|
||||
}
|
||||
|
||||
public static boolean isRequired(Parameter param) {
|
||||
|
||||
@ -23,7 +23,7 @@ import io.onedev.server.entitymanager.AgentAttributeManager;
|
||||
import io.onedev.server.entitymanager.AgentManager;
|
||||
import io.onedev.server.entitymanager.AuditManager;
|
||||
import io.onedev.server.model.Agent;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.search.entity.agent.AgentQuery;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
@ -78,7 +78,7 @@ public class AgentResource {
|
||||
try {
|
||||
parsedQuery = AgentQuery.parse(query, false);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
return agentManager.query(parsedQuery, offset, count);
|
||||
|
||||
@ -24,7 +24,7 @@ import io.onedev.server.entitymanager.AuditManager;
|
||||
import io.onedev.server.model.Agent;
|
||||
import io.onedev.server.model.AgentToken;
|
||||
import io.onedev.server.persistence.dao.EntityCriteria;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
@ -76,7 +76,7 @@ public class AgentTokenResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
EntityCriteria<AgentToken> criteria = EntityCriteria.of(AgentToken.class);
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ import io.onedev.server.model.Build;
|
||||
import io.onedev.server.model.BuildDependence;
|
||||
import io.onedev.server.model.BuildLabel;
|
||||
import io.onedev.server.model.BuildParam;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
import io.onedev.server.search.entity.build.BuildQuery;
|
||||
@ -131,13 +131,13 @@ public class BuildResource {
|
||||
@QueryParam("count") @Api(example="100") int count) {
|
||||
|
||||
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
BuildQuery parsedQuery;
|
||||
try {
|
||||
parsedQuery = BuildQuery.parse(null, query, true, true);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
return buildManager.query(null, parsedQuery, false, offset, count);
|
||||
|
||||
@ -1,19 +1,28 @@
|
||||
package io.onedev.server.rest.resource;
|
||||
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
import io.onedev.server.model.IssueLink;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import static io.onedev.server.security.SecurityUtils.canAccessIssue;
|
||||
import static io.onedev.server.security.SecurityUtils.canEditIssueLink;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAcceptableException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static io.onedev.server.security.SecurityUtils.canAccessIssue;
|
||||
import static io.onedev.server.security.SecurityUtils.canEditIssueLink;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
import io.onedev.server.exception.LinkValidationException;
|
||||
import io.onedev.server.model.IssueLink;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
|
||||
@Path("/issue-links")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@ -41,31 +50,17 @@ public class IssueLinkResource {
|
||||
@Api(order=200, description="Create new issue link")
|
||||
@POST
|
||||
public Long createLink(@NotNull IssueLink link) {
|
||||
if (!canAccessIssue(link.getSource()) || !canAccessIssue(link.getTarget()))
|
||||
throw new UnauthorizedException("No permission to access specified issues");
|
||||
if (!canEditIssueLink(link.getSource().getProject(), link.getSpec())
|
||||
&& !canEditIssueLink(link.getTarget().getProject(), link.getSpec())) {
|
||||
throw new UnauthorizedException();
|
||||
|| !canEditIssueLink(link.getTarget().getProject(), link.getSpec())) {
|
||||
throw new UnauthorizedException("No permission to add specified link for specified issues");
|
||||
|
||||
}
|
||||
if (link.getSource().equals(link.getTarget()))
|
||||
throw new BadRequestException("Can not link to self");
|
||||
if (link.getSpec().getOpposite() != null) {
|
||||
if (link.getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec()) && it.getTarget().equals(link.getTarget())))
|
||||
throw new BadRequestException("Source issue already linked to target issue via specified link spec");
|
||||
if (!link.getSpec().isMultiple() && link.getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
|
||||
throw new BadRequestException("Link spec is not multiple and the source issue is already linked to another issue via this link spec");
|
||||
if (!link.getSpec().getParsedIssueQuery(link.getSource().getProject()).matches(link.getTarget()))
|
||||
throw new BadRequestException("Link spec not allowed to link to the target issue");
|
||||
if (!link.getSpec().getOpposite().isMultiple() && link.getTarget().getSourceLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
|
||||
throw new BadRequestException("Opposite side of link spec is not multiple and the target issue is already linked to another issue via this link spec");
|
||||
if (!link.getSpec().getOpposite().getParsedIssueQuery(link.getSource().getProject()).matches(link.getSource()))
|
||||
throw new BadRequestException("Opposite side of link spec not allowed to link to the source issue");
|
||||
} else {
|
||||
if (link.getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec()) && it.getLinked(link.getSource()).equals(link.getTarget())))
|
||||
throw new BadRequestException("Specified issues already linked via specified link spec");
|
||||
if (!link.getSpec().isMultiple() && link.getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
|
||||
throw new BadRequestException("Link spec is not multiple and source issue is already linked to another issue via this link spec");
|
||||
var parsedIssueQuery = link.getSpec().getParsedIssueQuery(link.getSource().getProject());
|
||||
if (!parsedIssueQuery.matches(link.getSource()) || !parsedIssueQuery.matches(link.getTarget()))
|
||||
throw new BadRequestException("Link spec not allowed to link specified issues");
|
||||
try {
|
||||
link.validate();
|
||||
} catch (LinkValidationException e) {
|
||||
throw new NotAcceptableException(e.getMessage());
|
||||
}
|
||||
|
||||
linkManager.create(link);
|
||||
|
||||
@ -20,6 +20,7 @@ import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAcceptableException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@ -34,6 +35,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.SubscriptionManager;
|
||||
import io.onedev.server.attachment.AttachmentManager;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.entitymanager.AuditManager;
|
||||
@ -54,8 +56,10 @@ import io.onedev.server.model.Iteration;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.PullRequest;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
|
||||
import io.onedev.server.model.support.issue.field.FieldUtils;
|
||||
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.annotation.EntityCreate;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
@ -94,12 +98,14 @@ public class IssueResource {
|
||||
|
||||
private final UrlManager urlManager;
|
||||
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
|
||||
@Inject
|
||||
public IssueResource(SettingManager settingManager, IssueManager issueManager,
|
||||
IssueChangeManager issueChangeManager, IterationManager iterationManager,
|
||||
ProjectManager projectManager, ObjectMapper objectMapper,
|
||||
AuditManager auditManager, AttachmentManager attachmentManager,
|
||||
UrlManager urlManager) {
|
||||
UrlManager urlManager, SubscriptionManager subscriptionManager) {
|
||||
this.settingManager = settingManager;
|
||||
this.issueManager = issueManager;
|
||||
this.issueChangeManager = issueChangeManager;
|
||||
@ -109,6 +115,7 @@ public class IssueResource {
|
||||
this.auditManager = auditManager;
|
||||
this.attachmentManager = attachmentManager;
|
||||
this.urlManager = urlManager;
|
||||
this.subscriptionManager = subscriptionManager;
|
||||
}
|
||||
|
||||
@Api(order=100)
|
||||
@ -252,14 +259,14 @@ public class IssueResource {
|
||||
@QueryParam("count") @Api(example="100") int count) {
|
||||
|
||||
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
IssueQuery parsedQuery;
|
||||
try {
|
||||
IssueQueryParseOption option = new IssueQueryParseOption().withCurrentUserCriteria(true);
|
||||
parsedQuery = IssueQuery.parse(null, query, option, true);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
var typeReference = new TypeReference<Map<String, Object>>() {};
|
||||
@ -291,8 +298,17 @@ public class IssueResource {
|
||||
if (!SecurityUtils.canAccessProject(project))
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (!data.getIterationIds().isEmpty() && !SecurityUtils.canScheduleIssues(project))
|
||||
throw new UnauthorizedException("No permission to schedule issue");
|
||||
if (data.getIterationIds() != null && !data.getIterationIds().isEmpty() && !SecurityUtils.canScheduleIssues(project))
|
||||
throw new UnauthorizedException("No permission to schedule issue. Remove iterationIds if you want to create issue without scheduling it.");
|
||||
|
||||
if (data.getOwnEstimatedTime() != null) {
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new NotAcceptableException("An active subscription is required for this feature");
|
||||
if (!project.isTimeTracking())
|
||||
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
|
||||
if (!SecurityUtils.canScheduleIssues(project))
|
||||
throw new UnauthorizedException("Issue schedule permission required to set own estimated time. Remove ownEstimatedTime if you want to create issue without setting own estimated time.");
|
||||
}
|
||||
|
||||
var issueSetting = settingManager.getIssueSetting();
|
||||
|
||||
@ -304,20 +320,29 @@ public class IssueResource {
|
||||
issue.setSubmitDate(new Date());
|
||||
issue.setSubmitter(user);
|
||||
issue.setState(issueSetting.getInitialStateSpec().getName());
|
||||
issue.setOwnEstimatedTime(data.getOwnEstimatedTime());
|
||||
if (data.getOwnEstimatedTime() != null)
|
||||
issue.setOwnEstimatedTime(data.getOwnEstimatedTime());
|
||||
|
||||
for (Long iterationId : data.getIterationIds()) {
|
||||
Iteration iteration = iterationManager.load(iterationId);
|
||||
if (!iteration.getProject().isSelfOrAncestorOf(project))
|
||||
throw new BadRequestException("Iteration is not defined in project hierarchy of the issue");
|
||||
IssueSchedule schedule = new IssueSchedule();
|
||||
schedule.setIssue(issue);
|
||||
schedule.setIteration(iteration);
|
||||
issue.getSchedules().add(schedule);
|
||||
if (data.getIterationIds() != null) {
|
||||
for (Long iterationId : data.getIterationIds()) {
|
||||
Iteration iteration = iterationManager.load(iterationId);
|
||||
if (!iteration.getProject().isSelfOrAncestorOf(project))
|
||||
throw new BadRequestException("Iteration is not defined in project hierarchy of the issue");
|
||||
IssueSchedule schedule = new IssueSchedule();
|
||||
schedule.setIssue(issue);
|
||||
schedule.setIteration(iteration);
|
||||
issue.getSchedules().add(schedule);
|
||||
}
|
||||
}
|
||||
|
||||
issue.setFieldValues(FieldUtils.getFieldValues(project, data.fields));
|
||||
|
||||
try {
|
||||
issueManager.open(issue);
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException(e.getMessage());
|
||||
}
|
||||
|
||||
issue.setFieldValues(getFieldObjs(issue, data.fields));
|
||||
issueManager.open(issue);
|
||||
return issue.getId();
|
||||
}
|
||||
|
||||
@ -357,11 +382,15 @@ public class IssueResource {
|
||||
@Api(order=1275)
|
||||
@Path("/{issueId}/own-estimated-time")
|
||||
@POST
|
||||
public Response setOwnEstimatedTime(@PathParam("issueId") Long issueId, int hours) {
|
||||
public Response setOwnEstimatedTime(@PathParam("issueId") Long issueId, int minutes) {
|
||||
Issue issue = issueManager.load(issueId);
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new NotAcceptableException("An active subscription is required for this feature");
|
||||
if (!issue.getProject().isTimeTracking())
|
||||
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
|
||||
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
|
||||
throw new UnauthorizedException();
|
||||
issueChangeManager.changeOwnEstimatedTime(issue, hours);
|
||||
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
|
||||
issueChangeManager.changeOwnEstimatedTime(issue, minutes);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@ -371,13 +400,13 @@ public class IssueResource {
|
||||
public Response setIterations(@PathParam("issueId") Long issueId, List<Long> iterationIds) {
|
||||
Issue issue = issueManager.load(issueId);
|
||||
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
|
||||
throw new UnauthorizedException("No permission to schedule issue");
|
||||
throw new UnauthorizedException("Issue schedule permission required to set iterations");
|
||||
|
||||
Collection<Iteration> iterations = new HashSet<>();
|
||||
for (Long iterationId: iterationIds) {
|
||||
Iteration iteration = iterationManager.load(iterationId);
|
||||
if (!iteration.getProject().isSelfOrAncestorOf(issue.getProject()))
|
||||
throw new InvalidParamException("Iteration is not defined in project hierarchy of the issue");
|
||||
throw new InvalidParamsException("Iteration is not defined in project hierarchy of the issue");
|
||||
iterations.add(iteration);
|
||||
}
|
||||
|
||||
@ -399,7 +428,11 @@ public class IssueResource {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
issueChangeManager.changeFields(issue, getFieldObjs(issue, fields));
|
||||
try {
|
||||
issueChangeManager.changeFields(issue, FieldUtils.getFieldValues(issue.getProject(), fields));
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException(e.getMessage());
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@ -409,24 +442,20 @@ public class IssueResource {
|
||||
example.put("field2", new String[]{"value1", "value2"});
|
||||
return example;
|
||||
}
|
||||
|
||||
|
||||
@Api(order=1500)
|
||||
@Path("/{issueId}/state-transitions")
|
||||
@POST
|
||||
public Response transitState(@PathParam("issueId") Long issueId, @NotNull @Valid StateTransitionData data) {
|
||||
Issue issue = issueManager.load(issueId);
|
||||
var applicableTransitions = new ArrayList<ManualSpec>();
|
||||
for (var transition: settingManager.getIssueSetting().getTransitionSpecs()) {
|
||||
if (transition instanceof ManualSpec && ((ManualSpec)transition).canTransit(issue, data.getState()))
|
||||
applicableTransitions.add((ManualSpec) transition);
|
||||
}
|
||||
if (applicableTransitions.isEmpty())
|
||||
throw new BadRequestException("No applicable transition spec for: " + issue.getState() + "->" + data.getState());
|
||||
if (applicableTransitions.stream().noneMatch(it->it.isAuthorized(issue)))
|
||||
throw new UnauthorizedException();
|
||||
ManualSpec transition = settingManager.getIssueSetting().getManualSpec(issue, data.getState());
|
||||
|
||||
issueChangeManager.changeState(issue, data.getState(), getFieldObjs(issue, data.getFields()),
|
||||
data.getRemoveFields(), data.getComment());
|
||||
var fieldValues = FieldUtils.getFieldValues(issue.getProject(), data.getFields());
|
||||
try {
|
||||
issueChangeManager.changeState(issue, data.getState(), fieldValues, transition.getPromptFields(), transition.getRemoveFields(), data.getComment());
|
||||
} catch (EmptyFieldsException e) {
|
||||
throw new InvalidParamsException(e.getMessage());
|
||||
}
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@ -457,29 +486,6 @@ public class IssueResource {
|
||||
auditManager.audit(issue.getProject(), "deleted issue \"" + issue.getReference().toString(issue.getProject()) + "\" via RESTful API", oldAuditContent, null);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> getFieldObjs(Issue issue, Map<String, Serializable> fields) {
|
||||
var issueSetting = settingManager.getIssueSetting();
|
||||
Map<String, Object> fieldObjs = new HashMap<>();
|
||||
for (Map.Entry<String, Serializable> entry: fields.entrySet()) {
|
||||
var fieldName = entry.getKey();
|
||||
var fieldSpec = issueSetting.getFieldSpec(fieldName);
|
||||
if (fieldSpec == null)
|
||||
throw new BadRequestException("Undefined field: " + fieldName);
|
||||
if (!SecurityUtils.canEditIssueField(issue.getProject(), fieldName))
|
||||
throw new UnauthorizedException("No permission to edit field: " + fieldName);
|
||||
|
||||
List<String> values = new ArrayList<>();
|
||||
if (entry.getValue() instanceof String) {
|
||||
values.add((String) entry.getValue());
|
||||
} else if (entry.getValue() instanceof Collection) {
|
||||
values.addAll((Collection<String>) entry.getValue());
|
||||
}
|
||||
fieldObjs.put(entry.getKey(), fieldSpec.convertToObject(values));
|
||||
}
|
||||
return fieldObjs;
|
||||
}
|
||||
|
||||
@EntityCreate(Issue.class)
|
||||
public static class IssueOpenData implements Serializable {
|
||||
@ -498,8 +504,8 @@ public class IssueResource {
|
||||
@Api(order=400)
|
||||
private boolean confidential;
|
||||
|
||||
@Api(order=450, description = "Own estimated time in hours")
|
||||
private int ownEstimatedTime;
|
||||
@Api(order=450, description = "Own estimated time in minutes. Only be used when subscription is active and time tracking is enabled")
|
||||
private Integer ownEstimatedTime;
|
||||
|
||||
@Api(order=500)
|
||||
private List<Long> iterationIds = new ArrayList<>();
|
||||
@ -541,11 +547,11 @@ public class IssueResource {
|
||||
this.confidential = confidential;
|
||||
}
|
||||
|
||||
public int getOwnEstimatedTime() {
|
||||
public Integer getOwnEstimatedTime() {
|
||||
return ownEstimatedTime;
|
||||
}
|
||||
|
||||
public void setOwnEstimatedTime(int ownEstimatedTime) {
|
||||
public void setOwnEstimatedTime(Integer ownEstimatedTime) {
|
||||
this.ownEstimatedTime = ownEstimatedTime;
|
||||
}
|
||||
|
||||
@ -581,10 +587,7 @@ public class IssueResource {
|
||||
|
||||
@Api(order=200, exampleProvider = "getFieldsExample")
|
||||
private Map<String, Serializable> fields = new HashMap<>();
|
||||
|
||||
@Api(order=300)
|
||||
private Collection<String> removeFields = new HashSet<>();
|
||||
|
||||
|
||||
@Api(order=400)
|
||||
private String comment;
|
||||
|
||||
@ -606,15 +609,6 @@ public class IssueResource {
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<String> getRemoveFields() {
|
||||
return removeFields;
|
||||
}
|
||||
|
||||
public void setRemoveFields(Collection<String> removeFields) {
|
||||
this.removeFields = removeFields;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAcceptableException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@ -55,9 +56,9 @@ public class IssueWorkResource {
|
||||
@POST
|
||||
public Long createWork(@NotNull IssueWork work) {
|
||||
if (!subscriptionManager.isSubscriptionActive())
|
||||
throw new UnsupportedOperationException("This feature requires an active subscription");
|
||||
throw new NotAcceptableException("This feature requires an active subscription");
|
||||
if (!work.getIssue().getProject().isTimeTracking())
|
||||
throw new UnsupportedOperationException("Time tracking not enabled for project");
|
||||
throw new NotAcceptableException("Time tracking not enabled for project");
|
||||
|
||||
if (!SecurityUtils.canAccessIssue(work.getIssue())
|
||||
|| !SecurityUtils.isAdministrator() && !work.getUser().equals(SecurityUtils.getAuthUser())) {
|
||||
|
||||
@ -26,7 +26,7 @@ import io.onedev.server.model.Pack;
|
||||
import io.onedev.server.model.PackBlob;
|
||||
import io.onedev.server.model.PackBlobReference;
|
||||
import io.onedev.server.model.PackLabel;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
import io.onedev.server.search.entity.pack.PackQuery;
|
||||
@ -87,13 +87,13 @@ public class PackResource {
|
||||
@QueryParam("count") @Api(example="100") int count) {
|
||||
|
||||
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
PackQuery parsedQuery;
|
||||
try {
|
||||
parsedQuery = PackQuery.parse(null, query, true);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
return packManager.query(null, parsedQuery, false, offset, count);
|
||||
|
||||
@ -62,7 +62,7 @@ import io.onedev.server.model.support.issue.ProjectIssueSetting;
|
||||
import io.onedev.server.model.support.pack.ProjectPackSetting;
|
||||
import io.onedev.server.model.support.pullrequest.ProjectPullRequestSetting;
|
||||
import io.onedev.server.persistence.dao.EntityCriteria;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.annotation.EntityCreate;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
@ -192,13 +192,13 @@ public class ProjectResource {
|
||||
@QueryParam("count") @Api(example="100") int count) {
|
||||
|
||||
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
ProjectQuery parsedQuery;
|
||||
try {
|
||||
parsedQuery = ProjectQuery.parse(query);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
return projectManager.query(parsedQuery, false, offset, count).stream()
|
||||
@ -221,7 +221,7 @@ public class ProjectResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
EntityCriteria<Iteration> criteria = EntityCriteria.of(Iteration.class);
|
||||
criteria.add(Restrictions.in(Iteration.PROP_PROJECT, project.getSelfAndAncestors()));
|
||||
@ -254,7 +254,7 @@ public class ProjectResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
int sinceDay = (int) LocalDate.parse(since).toEpochDay();
|
||||
int untilDay = (int) LocalDate.parse(until).toEpochDay();
|
||||
|
||||
@ -51,7 +51,7 @@ import io.onedev.server.model.User;
|
||||
import io.onedev.server.model.support.pullrequest.AutoMerge;
|
||||
import io.onedev.server.model.support.pullrequest.MergePreview;
|
||||
import io.onedev.server.model.support.pullrequest.MergeStrategy;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.annotation.EntityCreate;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
@ -217,13 +217,13 @@ public class PullRequestResource {
|
||||
@QueryParam("count") @Api(example="100") int count) {
|
||||
|
||||
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
PullRequestQuery parsedQuery;
|
||||
try {
|
||||
parsedQuery = PullRequestQuery.parse(null, query, true);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
return pullRequestManager.query(null, parsedQuery, false, offset, count);
|
||||
@ -241,18 +241,18 @@ public class PullRequestResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (target.equals(source))
|
||||
throw new InvalidParamException("Source and target are the same");
|
||||
throw new InvalidParamsException("Source and target are the same");
|
||||
|
||||
PullRequest request = pullRequestManager.findOpen(target, source);
|
||||
if (request != null)
|
||||
throw new InvalidParamException("Another pull request already opened for this change");
|
||||
throw new InvalidParamsException("Another pull request already opened for this change");
|
||||
|
||||
request = pullRequestManager.findEffective(target, source);
|
||||
if (request != null) {
|
||||
if (request.isOpen())
|
||||
throw new InvalidParamException("Another pull request already opened for this change");
|
||||
throw new InvalidParamsException("Another pull request already opened for this change");
|
||||
else
|
||||
throw new InvalidParamException("Change already merged");
|
||||
throw new InvalidParamsException("Change already merged");
|
||||
}
|
||||
|
||||
request = new PullRequest();
|
||||
@ -261,7 +261,7 @@ public class PullRequestResource {
|
||||
source.getProject(), source.getObjectId());
|
||||
|
||||
if (baseCommitId == null)
|
||||
throw new InvalidParamException("No common base for target and source");
|
||||
throw new InvalidParamsException("No common base for target and source");
|
||||
|
||||
request.setTitle(data.getTitle());
|
||||
request.setTarget(target);
|
||||
@ -276,7 +276,7 @@ public class PullRequestResource {
|
||||
request.setMergeStrategy(request.getProject().findDefaultPullRequestMergeStrategy());
|
||||
|
||||
if (request.getBaseCommitHash().equals(source.getObjectName()))
|
||||
throw new InvalidParamException("Change already merged");
|
||||
throw new InvalidParamsException("Change already merged");
|
||||
|
||||
PullRequestUpdate update = new PullRequestUpdate();
|
||||
update.setDate(new DateTime(request.getSubmitDate()).plusSeconds(1).toDate());
|
||||
|
||||
@ -15,7 +15,7 @@ import io.onedev.server.git.service.GitService;
|
||||
import io.onedev.server.git.service.RefFacade;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.User;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.FileCreateOrUpdateRequest;
|
||||
import io.onedev.server.rest.resource.support.FileEditRequest;
|
||||
@ -138,7 +138,7 @@ public class RepositoryResource {
|
||||
if (!SecurityUtils.canWriteCode(project))
|
||||
throw new UnauthorizedException();
|
||||
else if (project.getBranchRef(request.getBranchName()) != null)
|
||||
throw new InvalidParamException("Branch '" + request.getBranchName() + "' already exists");
|
||||
throw new InvalidParamsException("Branch '" + request.getBranchName() + "' already exists");
|
||||
else if (project.getBranchProtection(request.getBranchName(), user).isPreventCreation())
|
||||
throw new ExplicitException("Branch creation prohibited by branch protection rule");
|
||||
|
||||
@ -217,7 +217,7 @@ public class RepositoryResource {
|
||||
}
|
||||
|
||||
if (project.getTagRef(request.getTagName()) != null) {
|
||||
throw new InvalidParamException("Tag '" + request.getTagName() + "' already exists");
|
||||
throw new InvalidParamsException("Tag '" + request.getTagName() + "' already exists");
|
||||
} else {
|
||||
User user = SecurityUtils.getUser();
|
||||
gitService.createTag(project, request.getTagName(), request.getRevision(), user.asPerson(),
|
||||
@ -257,13 +257,13 @@ public class RepositoryResource {
|
||||
}
|
||||
|
||||
if (count > MAX_COMMITS)
|
||||
throw new InvalidParamException("Count should not be greater than " + MAX_COMMITS);
|
||||
throw new InvalidParamsException("Count should not be greater than " + MAX_COMMITS);
|
||||
|
||||
CommitQuery parsedQuery;
|
||||
try {
|
||||
parsedQuery = CommitQuery.parse(project, query, true);
|
||||
} catch (Exception e) {
|
||||
throw new InvalidParamException("Error parsing query", e);
|
||||
throw new InvalidParamsException("Error parsing query", e);
|
||||
}
|
||||
|
||||
RevListOptions options = new RevListOptions();
|
||||
@ -311,7 +311,7 @@ public class RepositoryResource {
|
||||
BlobIdent blobIdent = new BlobIdent(project, revisionAndPathSegments);
|
||||
|
||||
if (!blobIdent.isTree()) {
|
||||
throw new InvalidParamException("Specified path is not a directory: " + blobIdent.path);
|
||||
throw new InvalidParamsException("Specified path is not a directory: " + blobIdent.path);
|
||||
}
|
||||
|
||||
ObjectId revId = project.getObjectId(blobIdent.revision, true);
|
||||
@ -343,7 +343,7 @@ public class RepositoryResource {
|
||||
BlobIdent blobIdent = new BlobIdent(project, revisionAndPathSegments);
|
||||
|
||||
if (!blobIdent.isFile()) {
|
||||
throw new InvalidParamException("Specified path is not a file: " + blobIdent.path);
|
||||
throw new InvalidParamsException("Specified path is not a file: " + blobIdent.path);
|
||||
}
|
||||
|
||||
Blob blob = project.getBlob(blobIdent, true);
|
||||
@ -375,14 +375,14 @@ public class RepositoryResource {
|
||||
revisionAndPath = RevisionAndPath.parse(project, revisionAndPathSegments);
|
||||
RefFacade ref = project.getBranchRef(revisionAndPath.getRevision());
|
||||
if (ref == null)
|
||||
throw new InvalidParamException("Not a branch: " + revisionAndPath.getRevision());
|
||||
throw new InvalidParamsException("Not a branch: " + revisionAndPath.getRevision());
|
||||
refName = ref.getName();
|
||||
oldCommitId = ref.getObjectId();
|
||||
if (revisionAndPath.getPath() == null)
|
||||
throw new InvalidParamException("Branch and file should be specified");
|
||||
throw new InvalidParamsException("Branch and file should be specified");
|
||||
} else {
|
||||
if (revisionAndPathSegments.size() < 2)
|
||||
throw new InvalidParamException("Branch and file should be specified");
|
||||
throw new InvalidParamsException("Branch and file should be specified");
|
||||
revisionAndPath = new RevisionAndPath(
|
||||
revisionAndPathSegments.get(0),
|
||||
StringUtils.join(revisionAndPathSegments.subList(1, revisionAndPathSegments.size())));
|
||||
|
||||
@ -27,7 +27,7 @@ import io.onedev.server.entitymanager.AuditManager;
|
||||
import io.onedev.server.entitymanager.RoleManager;
|
||||
import io.onedev.server.model.Role;
|
||||
import io.onedev.server.persistence.dao.EntityCriteria;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.rest.resource.support.RestConstants;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
@ -66,7 +66,7 @@ public class RoleResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (count > RestConstants.MAX_PAGE_SIZE)
|
||||
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
|
||||
|
||||
EntityCriteria<Role> criteria = EntityCriteria.of(Role.class);
|
||||
if (name != null)
|
||||
|
||||
@ -38,7 +38,7 @@ import io.onedev.server.model.support.administration.emailtemplates.EmailTemplat
|
||||
import io.onedev.server.model.support.administration.jobexecutor.JobExecutor;
|
||||
import io.onedev.server.model.support.administration.mailservice.MailService;
|
||||
import io.onedev.server.model.support.administration.sso.SsoConnector;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.web.page.layout.ContributedAdministrationSetting;
|
||||
@ -211,7 +211,7 @@ public class SettingResource {
|
||||
throw new UnauthorizedException();
|
||||
String ingressUrl = OneDev.getInstance().getIngressUrl();
|
||||
if (ingressUrl != null && !ingressUrl.equals(systemSetting.getServerUrl()))
|
||||
throw new InvalidParamException("Server URL can only be \"" + ingressUrl + "\"");
|
||||
throw new InvalidParamsException("Server URL can only be \"" + ingressUrl + "\"");
|
||||
var oldAuditContent = VersionedXmlDoc.fromBean(settingManager.getSystemSetting()).toXML();
|
||||
settingManager.saveSystemSetting(systemSetting);
|
||||
auditManager.audit(null, "changed system setting via RESTful API",
|
||||
|
||||
@ -6,7 +6,7 @@ import io.onedev.server.entitymanager.ProjectManager;
|
||||
import io.onedev.server.git.GitUtils;
|
||||
import io.onedev.server.job.JobManager;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.rest.InvalidParamException;
|
||||
import io.onedev.server.rest.InvalidParamsException;
|
||||
import io.onedev.server.rest.annotation.Api;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
@ -92,11 +92,11 @@ public class TriggerJobResource {
|
||||
String accessTokenValue, UriInfo uriInfo) {
|
||||
Project project = projectManager.findByPath(projectPath);
|
||||
if (project == null)
|
||||
throw new InvalidParamException("Project not found: " + projectPath);
|
||||
throw new InvalidParamsException("Project not found: " + projectPath);
|
||||
|
||||
var accessToken = accessTokenManager.findByValue(accessTokenValue);
|
||||
if (accessToken == null)
|
||||
throw new InvalidParamException("Invalid access token");
|
||||
throw new InvalidParamsException("Invalid access token");
|
||||
|
||||
ThreadContext.bind(accessToken.asSubject());
|
||||
try {
|
||||
@ -104,7 +104,7 @@ public class TriggerJobResource {
|
||||
throw new UnauthorizedException();
|
||||
|
||||
if (StringUtils.isNotBlank(branch) && StringUtils.isNotBlank(tag))
|
||||
throw new InvalidParamException("Either branch or tag should be specified, but not both");
|
||||
throw new InvalidParamsException("Either branch or tag should be specified, but not both");
|
||||
|
||||
String refName;
|
||||
if (branch != null)
|
||||
@ -116,7 +116,7 @@ public class TriggerJobResource {
|
||||
|
||||
RevCommit commit = project.getRevCommit(refName, false);
|
||||
if (commit == null)
|
||||
throw new InvalidParamException("Ref not found: " + refName);
|
||||
throw new InvalidParamsException("Ref not found: " + refName);
|
||||
|
||||
Map<String, List<String>> jobParams = new HashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry: uriInfo.getQueryParameters().entrySet()) {
|
||||
|
||||
@ -39,7 +39,7 @@ public class ComponentContext implements Serializable {
|
||||
|
||||
@Nullable
|
||||
public static ComponentContext get() {
|
||||
if (!stack.get().isEmpty()) {
|
||||
if (!stack.get().isEmpty()) {
|
||||
return stack.get().peek();
|
||||
} else {
|
||||
Page page = WicketUtils.getPage();
|
||||
|
||||
@ -36,7 +36,7 @@ public class UserCache extends MapProxy<Long, UserFacade> {
|
||||
@Nullable
|
||||
public UserFacade findByFullName(String fullName) {
|
||||
for (UserFacade facade: values()) {
|
||||
if (fullName.equals(facade.getFullName()))
|
||||
if (fullName.equalsIgnoreCase(facade.getFullName()))
|
||||
return facade;
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -5,10 +5,10 @@ import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.buildspec.job.JobVariable;
|
||||
import io.onedev.server.buildspec.param.ParamCombination;
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.model.Build;
|
||||
import io.onedev.server.model.support.build.JobProperty;
|
||||
import io.onedev.server.util.GroovyUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.interpolative.Interpolative.Segment;
|
||||
import io.onedev.server.util.interpolative.Interpolative.Segment.Type;
|
||||
import io.onedev.server.web.editable.EditableStringTransformer;
|
||||
|
||||
@ -7,9 +7,9 @@ import org.apache.wicket.model.Model;
|
||||
|
||||
import io.onedev.commons.utils.StringUtils;
|
||||
import io.onedev.server.buildspec.param.spec.ParamSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
|
||||
public class ParamValuesLabel extends Label {
|
||||
|
||||
|
||||
@ -54,6 +54,7 @@ import com.google.common.collect.Sets;
|
||||
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.entitymanager.AuditManager;
|
||||
import io.onedev.server.entitymanager.BuildManager;
|
||||
@ -77,7 +78,6 @@ import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.security.permission.JobPermission;
|
||||
import io.onedev.server.security.permission.RunJob;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.web.WebConstants;
|
||||
import io.onedev.server.web.WebSession;
|
||||
import io.onedev.server.web.behavior.BuildQueryBehavior;
|
||||
|
||||
@ -33,6 +33,7 @@ import com.google.common.collect.Sets;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspec.BuildSpec;
|
||||
import io.onedev.server.buildspec.job.Job;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.entitymanager.BuildLabelManager;
|
||||
import io.onedev.server.entityreference.EntityReference;
|
||||
import io.onedev.server.git.BlobIdent;
|
||||
@ -43,7 +44,6 @@ import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.PullRequest;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.criteria.Criteria;
|
||||
import io.onedev.server.web.asset.emoji.Emojis;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
|
||||
@ -108,7 +108,7 @@ public abstract class NewIssueEditor extends FormComponentPanel<Issue> implement
|
||||
Class<?> fieldBeanClass = FieldUtils.getFieldBeanClass();
|
||||
Serializable fieldBean = issue.getFieldBean(fieldBeanClass, true);
|
||||
|
||||
Collection<String> fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
|
||||
var fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
|
||||
issue.setFieldValues(FieldUtils.getFieldValues(new ComponentContext(this), fieldBean,
|
||||
FieldUtils.getEditableFields(getProject(), fieldNames)));
|
||||
|
||||
@ -409,7 +409,8 @@ public abstract class NewIssueEditor extends FormComponentPanel<Issue> implement
|
||||
issue.setConfidential(confidentialInput.getConvertedInput());
|
||||
|
||||
fieldEditor.convertInput();
|
||||
Collection<String> fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
|
||||
|
||||
var fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
|
||||
issue.setFieldValues(FieldUtils.getFieldValues(fieldEditor.newComponentContext(),
|
||||
fieldEditor.getConvertedInput(), fieldNames));
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.unbescape.html.HtmlEscape;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputContext;
|
||||
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
|
||||
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
|
||||
@ -57,7 +58,6 @@ import io.onedev.server.util.ColorUtils;
|
||||
import io.onedev.server.util.ComponentContext;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.EditContext;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
|
||||
import io.onedev.server.web.ajaxlistener.DisableGlobalAjaxIndicatorListener;
|
||||
import io.onedev.server.web.component.MultilineLabel;
|
||||
|
||||
@ -50,6 +50,7 @@ import io.onedev.server.model.Issue;
|
||||
import io.onedev.server.model.Iteration;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.administration.GlobalIssueSetting;
|
||||
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
|
||||
import io.onedev.server.model.support.issue.field.FieldUtils;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.search.entity.issue.IssueQuery;
|
||||
@ -311,8 +312,14 @@ abstract class BatchEditPanel extends Panel implements InputContext {
|
||||
|
||||
Map<String, Object> fieldValues = FieldUtils.getFieldValues(customFieldsEditor.newComponentContext(),
|
||||
customFieldsBean, selectedFields);
|
||||
OneDev.getInstance(IssueChangeManager.class).batchUpdate(
|
||||
getIssueIterator(), state, confidential, iterations, fieldValues, comment, sendNotifications);
|
||||
try {
|
||||
OneDev.getInstance(IssueChangeManager.class).batchUpdate(
|
||||
getIssueIterator(), state, confidential, iterations, fieldValues, comment, sendNotifications);
|
||||
} catch (EmptyFieldsException e) {
|
||||
form.error(e.getMessage());
|
||||
target.add(form);
|
||||
return;
|
||||
}
|
||||
onUpdated(target);
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +76,7 @@ import com.google.common.collect.Sets;
|
||||
import edu.emory.mathcs.backport.java.util.Collections;
|
||||
import io.onedev.commons.utils.ExplicitException;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.entitymanager.AuditManager;
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
@ -106,7 +107,6 @@ import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.security.permission.AccessProject;
|
||||
import io.onedev.server.timetracking.TimeTrackingManager;
|
||||
import io.onedev.server.util.DateUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.LinkDescriptor;
|
||||
import io.onedev.server.util.ProjectScope;
|
||||
import io.onedev.server.util.facade.ProjectCache;
|
||||
|
||||
@ -111,10 +111,9 @@ public abstract class TransitionMenuLink extends MenuLink {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransit(AjaxRequestTarget target, Map<String, Object> fieldValues,
|
||||
String comment) {
|
||||
protected void onTransit(AjaxRequestTarget target, Map<String, Object> fieldValues, String comment) {
|
||||
IssueChangeManager manager = OneDev.getInstance(IssueChangeManager.class);
|
||||
manager.changeState(getIssue(), toState, fieldValues, transition.getRemoveFields(), comment);
|
||||
manager.changeState(getIssue(), toState, fieldValues, transition.getPromptFields(), transition.getRemoveFields(), comment);
|
||||
((BasePage)getPage()).notifyObservablesChange(target, getIssue().getChangeObservables(true));
|
||||
modal.close();
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.entitymanager.IssueChangeManager;
|
||||
import io.onedev.server.entitymanager.IssueVoteManager;
|
||||
import io.onedev.server.entitymanager.IssueWatchManager;
|
||||
@ -63,7 +64,6 @@ import io.onedev.server.search.entity.issue.IssueQuery;
|
||||
import io.onedev.server.search.entity.issue.IssueQueryLexer;
|
||||
import io.onedev.server.search.entity.issue.StateCriteria;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.Similarities;
|
||||
import io.onedev.server.web.WebConstants;
|
||||
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
|
||||
|
||||
@ -62,8 +62,8 @@ public class MultiChoiceEditor extends PropertyEditor<List<String>> {
|
||||
@Override
|
||||
protected Map<String, String> load() {
|
||||
ComponentContext componentContext = new ComponentContext(MultiChoiceEditor.this);
|
||||
ComponentContext.push(componentContext);
|
||||
if (getChoiceProvider().displayNames().length() != 0) {
|
||||
ComponentContext.push(componentContext);
|
||||
try {
|
||||
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().displayNames());
|
||||
} finally {
|
||||
@ -81,8 +81,8 @@ public class MultiChoiceEditor extends PropertyEditor<List<String>> {
|
||||
@Override
|
||||
protected Map<String, String> load() {
|
||||
ComponentContext componentContext = new ComponentContext(MultiChoiceEditor.this);
|
||||
ComponentContext.push(componentContext);
|
||||
if (getChoiceProvider().descriptions().length() != 0) {
|
||||
ComponentContext.push(componentContext);
|
||||
try {
|
||||
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().descriptions());
|
||||
} finally {
|
||||
|
||||
@ -60,8 +60,8 @@ public class SingleChoiceEditor extends PropertyEditor<String> {
|
||||
@Override
|
||||
protected Map<String, String> load() {
|
||||
ComponentContext componentContext = new ComponentContext(SingleChoiceEditor.this);
|
||||
ComponentContext.push(componentContext);
|
||||
if (getChoiceProvider().displayNames().length() != 0) {
|
||||
ComponentContext.push(componentContext);
|
||||
try {
|
||||
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().displayNames());
|
||||
} finally {
|
||||
@ -79,8 +79,8 @@ public class SingleChoiceEditor extends PropertyEditor<String> {
|
||||
@Override
|
||||
protected Map<String, String> load() {
|
||||
ComponentContext componentContext = new ComponentContext(SingleChoiceEditor.this);
|
||||
ComponentContext.push(componentContext);
|
||||
if (getChoiceProvider().descriptions().length() != 0) {
|
||||
ComponentContext.push(componentContext);
|
||||
try {
|
||||
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().descriptions());
|
||||
} finally {
|
||||
|
||||
@ -32,6 +32,7 @@ import org.apache.wicket.request.cycle.RequestCycle;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
import io.onedev.server.entitymanager.IssueManager;
|
||||
import io.onedev.server.model.Issue;
|
||||
@ -40,7 +41,6 @@ import io.onedev.server.model.Iteration;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.model.support.issue.BoardSpec;
|
||||
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.LinkDescriptor;
|
||||
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
|
||||
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener.AttachMode;
|
||||
|
||||
@ -427,7 +427,7 @@ abstract class BoardColumnPanel extends AbstractColumnPanel {
|
||||
};
|
||||
} else {
|
||||
getIssueChangeManager().changeState(issue, getColumn(),
|
||||
new HashMap<>(), transitionRef.get().getRemoveFields(), null);
|
||||
new HashMap<>(), transitionRef.get().getPromptFields(), transitionRef.get().getRemoveFields(), null);
|
||||
cardListPanel.onCardDropped(target, issueId, cardIndex, true);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -61,7 +61,7 @@ abstract class StateTransitionPanel extends Panel implements InputContext {
|
||||
Map<String, Object> fieldValues = FieldUtils.getFieldValues(
|
||||
editor.newComponentContext(), fieldBean, editableFields);
|
||||
OneDev.getInstance(IssueChangeManager.class).changeState(getIssue(),
|
||||
getToState(), fieldValues, transition.getRemoveFields(), null);
|
||||
getToState(), fieldValues, transition.getPromptFields(), transition.getRemoveFields(), null);
|
||||
onSaved(target);
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.buildspecmodel.inputspec.Input;
|
||||
import io.onedev.server.data.migration.VersionedXmlDoc;
|
||||
import io.onedev.server.entitymanager.IssueLinkManager;
|
||||
import io.onedev.server.entitymanager.IssueManager;
|
||||
@ -50,7 +51,6 @@ import io.onedev.server.model.support.QueryPersonalization;
|
||||
import io.onedev.server.model.support.issue.NamedIssueQuery;
|
||||
import io.onedev.server.model.support.issue.ProjectIssueSetting;
|
||||
import io.onedev.server.security.SecurityUtils;
|
||||
import io.onedev.server.util.Input;
|
||||
import io.onedev.server.util.LinkDescriptor;
|
||||
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
|
||||
import io.onedev.server.web.behavior.ChangeObserver;
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 39fd6c34f64496cf7d7725aacfdfbcf4f37e47ce
|
||||
Subproject commit b4df43a570ed7b071a49e45fa8dbb7cf208b170b
|
||||
Loading…
x
Reference in New Issue
Block a user