completes feature: project components

Sat, 17 Oct 2020 19:56:50 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 17 Oct 2020 19:56:50 +0200
changeset 134
f47e82cd6077
parent 133
ef075cd7ce55
child 135
bafc315294fd

completes feature: project components

src/main/java/de/uapcore/lightpit/dao/IssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Component.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Issue.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Project.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Version.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/component-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/components.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-navmenu.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/versions.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/project-header.jspf file | annotate | diff | comparison | revisions
     1.1 --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Sat Oct 17 15:21:56 2020 +0200
     1.2 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Sat Oct 17 19:56:50 2020 +0200
     1.3 @@ -28,10 +28,7 @@
     1.4   */
     1.5  package de.uapcore.lightpit.dao;
     1.6  
     1.7 -import de.uapcore.lightpit.entities.Issue;
     1.8 -import de.uapcore.lightpit.entities.IssueComment;
     1.9 -import de.uapcore.lightpit.entities.Project;
    1.10 -import de.uapcore.lightpit.entities.Version;
    1.11 +import de.uapcore.lightpit.entities.*;
    1.12  
    1.13  import java.sql.SQLException;
    1.14  import java.util.List;
    1.15 @@ -39,14 +36,39 @@
    1.16  public interface IssueDao extends ChildEntityDao<Issue, Project> {
    1.17  
    1.18      /**
    1.19 -     * Lists all issues that are somehow related to the specified version.
    1.20 -     * If version is null, search for issues that are not related to any version.
    1.21 +     * Lists all issues that are related to the specified component and version.
    1.22 +     * If component or version is null, search for issues that are not assigned to any
    1.23 +     * component or version, respectively.
    1.24       *
    1.25 +     * @param project the project
    1.26 +     * @param component the component or null
    1.27       * @param version the version or null
    1.28       * @return a list of issues
    1.29       * @throws SQLException on any kind of SQL error
    1.30       */
    1.31 -    List<Issue> list(Version version) throws SQLException;
    1.32 +    List<Issue> list(Project project, Component component, Version version) throws SQLException;
    1.33 +
    1.34 +    /**
    1.35 +     * Lists all issues that are related to the specified version.
    1.36 +     * If the version is null, lists issues that are not assigned to any version.
    1.37 +     *
    1.38 +     * @param project the project (mandatory)
    1.39 +     * @param version the version or null
    1.40 +     * @return a list of issues
    1.41 +     * @throws SQLException on any kind of SQL error
    1.42 +     */
    1.43 +    List<Issue> list(Project project, Version version) throws SQLException;
    1.44 +
    1.45 +    /**
    1.46 +     * Lists all issues that are related to the specified component.
    1.47 +     * If the component is null, lists issues that are not assigned to a component.
    1.48 +     *
    1.49 +     * @param project the project (mandatory)
    1.50 +     * @param component the component or null
    1.51 +     * @return a list of issues
    1.52 +     * @throws SQLException on any kind of SQL error
    1.53 +     */
    1.54 +    List<Issue> list(Project project, Component component) throws SQLException;
    1.55  
    1.56      /**
    1.57       * Lists all comments for a specific issue in chronological order.
     2.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Sat Oct 17 15:21:56 2020 +0200
     2.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Sat Oct 17 19:56:50 2020 +0200
     2.3 @@ -31,13 +31,11 @@
     2.4  import de.uapcore.lightpit.dao.IssueDao;
     2.5  import de.uapcore.lightpit.entities.*;
     2.6  
     2.7 -import java.sql.Connection;
     2.8 -import java.sql.PreparedStatement;
     2.9 -import java.sql.ResultSet;
    2.10 -import java.sql.SQLException;
    2.11 +import java.sql.*;
    2.12  import java.util.ArrayList;
    2.13  import java.util.List;
    2.14  import java.util.Objects;
    2.15 +import java.util.Optional;
    2.16  
    2.17  import static de.uapcore.lightpit.dao.Functions.*;
    2.18  
    2.19 @@ -51,43 +49,50 @@
    2.20  
    2.21      public PGIssueDao(Connection connection) throws SQLException {
    2.22          list = connection.prepareStatement(
    2.23 -                "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
    2.24 +                "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
    2.25 +                        "status, category, subject, i.description, " +
    2.26                          "userid, username, givenname, lastname, mail, " +
    2.27                          "created, updated, eta " +
    2.28                          "from lpit_issue i " +
    2.29 -                        "join lpit_project p on project = projectid " +
    2.30 +                        "join lpit_project p on i.project = projectid " +
    2.31 +                        "left join lpit_component c on component = c.id " +
    2.32                          "left join lpit_user on userid = assignee " +
    2.33 -                        "where project = ? ");
    2.34 +                        "where i.project = ? and coalesce(component, -1) = coalesce(?, component, -1)");
    2.35  
    2.36          listForVersion = connection.prepareStatement(
    2.37                  "with issue_version as ( "+
    2.38                          "select issueid, versionid from lpit_issue_affected_version union "+
    2.39                          "select issueid, versionid from lpit_issue_resolved_version) "+
    2.40 -                        "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
    2.41 +                        "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
    2.42 +                        "status, category, subject, i.description, " +
    2.43                          "userid, username, givenname, lastname, mail, " +
    2.44                          "created, updated, eta " +
    2.45                          "from lpit_issue i " +
    2.46 -                        "join lpit_project p on project = projectid " +
    2.47 +                        "join lpit_project p on i.project = projectid " +
    2.48 +                        "left join lpit_component c on component = c.id " +
    2.49                          "left join issue_version using (issueid) "+
    2.50                          "left join lpit_user on userid = assignee " +
    2.51 -                        "where coalesce(versionid,-1) = ? "
    2.52 +                        "where coalesce(versionid,-1) = ? and coalesce(component, -1) = coalesce(?, component, -1)"
    2.53          );
    2.54  
    2.55          find = connection.prepareStatement(
    2.56 -                "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
    2.57 +                "select issueid, i.project, p.name as projectname, component, c.name as componentname, " +
    2.58 +                        "status, category, subject, i.description, " +
    2.59                          "userid, username, givenname, lastname, mail, " +
    2.60                          "created, updated, eta " +
    2.61                          "from lpit_issue i " +
    2.62 -                        "left join lpit_project p on project = projectid " +
    2.63 +                        "join lpit_project p on i.project = projectid " +
    2.64 +                        "left join lpit_component c on component = c.id " +
    2.65                          "left join lpit_user on userid = assignee " +
    2.66                          "where issueid = ? ");
    2.67  
    2.68          insert = connection.prepareStatement(
    2.69 -                "insert into lpit_issue (project, status, category, subject, description, assignee, eta) " +
    2.70 +                "insert into lpit_issue (project, component, status, category, subject, description, assignee, eta) " +
    2.71                          "values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?) returning issueid"
    2.72          );
    2.73          update = connection.prepareStatement(
    2.74 -                "update lpit_issue set updated = now(), status = ?::issue_status, category = ?::issue_category, " +
    2.75 +                "update lpit_issue set " +
    2.76 +                        "updated = now(), component = ?, status = ?::issue_status, category = ?::issue_category, " +
    2.77                          "subject = ?, description = ?, assignee = ?, eta = ? where issueid = ?"
    2.78          );
    2.79  
    2.80 @@ -123,8 +128,15 @@
    2.81      private Issue mapColumns(ResultSet result) throws SQLException {
    2.82          final var project = new Project(result.getInt("project"));
    2.83          project.setName(result.getString("projectname"));
    2.84 +        var component = new Component(result.getInt("component"));
    2.85 +        if (result.wasNull()) {
    2.86 +            component = null;
    2.87 +        } else {
    2.88 +            component.setName(result.getString("componentname"));
    2.89 +        }
    2.90          final var issue = new Issue(result.getInt("issueid"));
    2.91          issue.setProject(project);
    2.92 +        issue.setComponent(component);
    2.93          issue.setStatus(IssueStatus.valueOf(result.getString("status")));
    2.94          issue.setCategory(IssueCategory.valueOf(result.getString("category")));
    2.95          issue.setSubject(result.getString("subject"));
    2.96 @@ -161,17 +173,24 @@
    2.97          }
    2.98      }
    2.99  
   2.100 +    private int setData(PreparedStatement stmt, int column, Issue instance) throws SQLException {
   2.101 +        setForeignKeyOrNull(stmt, ++column, instance.getComponent(), Component::getId);
   2.102 +        stmt.setString(++column, instance.getStatus().name());
   2.103 +        stmt.setString(++column, instance.getCategory().name());
   2.104 +        stmt.setString(++column, instance.getSubject());
   2.105 +        setStringOrNull(stmt, ++column, instance.getDescription());
   2.106 +        setForeignKeyOrNull(stmt, ++column, instance.getAssignee(), User::getId);
   2.107 +        setDateOrNull(stmt, ++column, instance.getEta());
   2.108 +        return column;
   2.109 +    }
   2.110 +
   2.111      @Override
   2.112      public void save(Issue instance, Project project) throws SQLException {
   2.113          Objects.requireNonNull(instance.getSubject());
   2.114          instance.setProject(project);
   2.115 -        insert.setInt(1, instance.getProject().getId());
   2.116 -        insert.setString(2, instance.getStatus().name());
   2.117 -        insert.setString(3, instance.getCategory().name());
   2.118 -        insert.setString(4, instance.getSubject());
   2.119 -        setStringOrNull(insert, 5, instance.getDescription());
   2.120 -        setForeignKeyOrNull(insert, 6, instance.getAssignee(), User::getId);
   2.121 -        setDateOrNull(insert, 7, instance.getEta());
   2.122 +        int column = 0;
   2.123 +        insert.setInt(++column, instance.getProject().getId());
   2.124 +        setData(insert, column, instance);
   2.125          // insert and retrieve the ID
   2.126          final var rs = insert.executeQuery();
   2.127          rs.next();
   2.128 @@ -183,13 +202,8 @@
   2.129      public boolean update(Issue instance) throws SQLException {
   2.130          if (instance.getId() < 0) return false;
   2.131          Objects.requireNonNull(instance.getSubject());
   2.132 -        update.setString(1, instance.getStatus().name());
   2.133 -        update.setString(2, instance.getCategory().name());
   2.134 -        update.setString(3, instance.getSubject());
   2.135 -        setStringOrNull(update, 4, instance.getDescription());
   2.136 -        setForeignKeyOrNull(update, 5, instance.getAssignee(), User::getId);
   2.137 -        setDateOrNull(update, 6, instance.getEta());
   2.138 -        update.setInt(7, instance.getId());
   2.139 +        int column = setData(update, 0, instance);
   2.140 +        update.setInt(++column, instance.getId());
   2.141          boolean success = update.executeUpdate() > 0;
   2.142          if (success) {
   2.143              updateVersionLists(instance);
   2.144 @@ -199,8 +213,7 @@
   2.145          }
   2.146      }
   2.147  
   2.148 -    private List<Issue> list(PreparedStatement query, int arg) throws SQLException {
   2.149 -        query.setInt(1, arg);
   2.150 +    private List<Issue> executeQuery(PreparedStatement query) throws SQLException {
   2.151          List<Issue> issues = new ArrayList<>();
   2.152          try (var result = query.executeQuery()) {
   2.153              while (result.next()) {
   2.154 @@ -212,12 +225,30 @@
   2.155  
   2.156      @Override
   2.157      public List<Issue> list(Project project) throws SQLException {
   2.158 -        return list(list, project.getId());
   2.159 +        list.setInt(1, project.getId());
   2.160 +        list.setNull(2, Types.INTEGER);
   2.161 +        return executeQuery(list);
   2.162      }
   2.163  
   2.164      @Override
   2.165 -    public List<Issue> list(Version version) throws SQLException {
   2.166 -        return list(listForVersion, version == null ? -1 : version.getId());
   2.167 +    public List<Issue> list(Project project, Component component, Version version) throws SQLException {
   2.168 +        listForVersion.setInt(1, Optional.ofNullable(version).map(Version::getId).orElse(-1));
   2.169 +        listForVersion.setInt(2, Optional.ofNullable(component).map(Component::getId).orElse(-1));
   2.170 +        return executeQuery(listForVersion);
   2.171 +    }
   2.172 +
   2.173 +    @Override
   2.174 +    public List<Issue> list(Project project, Version version) throws SQLException {
   2.175 +        listForVersion.setInt(1, Optional.ofNullable(version).map(Version::getId).orElse(-1));
   2.176 +        listForVersion.setNull(2, Types.INTEGER);
   2.177 +        return executeQuery(listForVersion);
   2.178 +    }
   2.179 +
   2.180 +    @Override
   2.181 +    public List<Issue> list(Project project, Component component) throws SQLException {
   2.182 +        list.setInt(1, project.getId());
   2.183 +        list.setInt(2, Optional.ofNullable(component).map(Component::getId).orElse(-1));
   2.184 +        return executeQuery(list);
   2.185      }
   2.186  
   2.187      @Override
     3.1 --- a/src/main/java/de/uapcore/lightpit/entities/Component.java	Sat Oct 17 15:21:56 2020 +0200
     3.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Component.java	Sat Oct 17 19:56:50 2020 +0200
     3.3 @@ -38,7 +38,9 @@
     3.4  
     3.5      private String name;
     3.6  
     3.7 -    private WebColor color;
     3.8 +    private String node;
     3.9 +
    3.10 +    private WebColor color = new WebColor("000000");
    3.11  
    3.12      private int ordinal = 0;
    3.13  
    3.14 @@ -66,6 +68,14 @@
    3.15          this.name = name;
    3.16      }
    3.17  
    3.18 +    public String getNode() {
    3.19 +        return node == null ? String.valueOf(id) : node;
    3.20 +    }
    3.21 +
    3.22 +    public void setNode(String node) {
    3.23 +        this.node = node;
    3.24 +    }
    3.25 +
    3.26      public WebColor getColor() {
    3.27          return color;
    3.28      }
     4.1 --- a/src/main/java/de/uapcore/lightpit/entities/Issue.java	Sat Oct 17 15:21:56 2020 +0200
     4.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java	Sat Oct 17 19:56:50 2020 +0200
     4.3 @@ -39,6 +39,7 @@
     4.4  
     4.5      private int id;
     4.6      private Project project;
     4.7 +    private Component component;
     4.8  
     4.9      private IssueStatus status;
    4.10      private IssueCategory category;
    4.11 @@ -78,6 +79,14 @@
    4.12          return project;
    4.13      }
    4.14  
    4.15 +    public Component getComponent() {
    4.16 +        return component;
    4.17 +    }
    4.18 +
    4.19 +    public void setComponent(Component component) {
    4.20 +        this.component = component;
    4.21 +    }
    4.22 +
    4.23      public IssueStatus getStatus() {
    4.24          return status;
    4.25      }
     5.1 --- a/src/main/java/de/uapcore/lightpit/entities/Project.java	Sat Oct 17 15:21:56 2020 +0200
     5.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Project.java	Sat Oct 17 19:56:50 2020 +0200
     5.3 @@ -34,6 +34,7 @@
     5.4  
     5.5      private final int id;
     5.6      private String name;
     5.7 +    private String node;
     5.8      private String description;
     5.9      private String repoUrl;
    5.10      private User owner;
    5.11 @@ -54,6 +55,14 @@
    5.12          this.name = name;
    5.13      }
    5.14  
    5.15 +    public String getNode() {
    5.16 +        return node == null ? String.valueOf(id) : node;
    5.17 +    }
    5.18 +
    5.19 +    public void setNode(String node) {
    5.20 +        this.node = node;
    5.21 +    }
    5.22 +
    5.23      public String getDescription() {
    5.24          return description;
    5.25      }
     6.1 --- a/src/main/java/de/uapcore/lightpit/entities/Version.java	Sat Oct 17 15:21:56 2020 +0200
     6.2 +++ b/src/main/java/de/uapcore/lightpit/entities/Version.java	Sat Oct 17 19:56:50 2020 +0200
     6.3 @@ -34,6 +34,7 @@
     6.4  
     6.5      private final int id;
     6.6      private String name;
     6.7 +    private String node;
     6.8      /**
     6.9       * If we do not want versions to be ordered lexicographically we may specify an order.
    6.10       */
    6.11 @@ -56,6 +57,14 @@
    6.12          this.name = name;
    6.13      }
    6.14  
    6.15 +    public String getNode() {
    6.16 +        return node == null ? String.valueOf(id) : node;
    6.17 +    }
    6.18 +
    6.19 +    public void setNode(String node) {
    6.20 +        this.node = node;
    6.21 +    }
    6.22 +
    6.23      public int getOrdinal() {
    6.24          return ordinal;
    6.25      }
     7.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat Oct 17 15:21:56 2020 +0200
     7.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat Oct 17 19:56:50 2020 +0200
     7.3 @@ -32,6 +32,7 @@
     7.4  import de.uapcore.lightpit.*;
     7.5  import de.uapcore.lightpit.dao.DataAccessObjects;
     7.6  import de.uapcore.lightpit.entities.*;
     7.7 +import de.uapcore.lightpit.types.WebColor;
     7.8  import de.uapcore.lightpit.viewmodel.*;
     7.9  import de.uapcore.lightpit.viewmodel.util.IssueSorter;
    7.10  import org.slf4j.Logger;
    7.11 @@ -43,6 +44,7 @@
    7.12  import java.io.IOException;
    7.13  import java.sql.Date;
    7.14  import java.sql.SQLException;
    7.15 +import java.util.List;
    7.16  import java.util.NoSuchElementException;
    7.17  import java.util.stream.Collectors;
    7.18  import java.util.stream.Stream;
    7.19 @@ -84,19 +86,30 @@
    7.20          }
    7.21  
    7.22          // Select Version
    7.23 -        final int vid = Functions.parseIntOrZero(pathParameters.get("version"));
    7.24 -        if (vid > 0) {
    7.25 -            viewModel.setVersionFilter(versionDao.find(vid));
    7.26 +        final var pathParamVersion = pathParameters.get("version");
    7.27 +        if ("no-version".equals(pathParamVersion)) {
    7.28 +            viewModel.setVersionFilter(ProjectView.NO_VERSION);
    7.29 +        } else if ("all-versions".equals(pathParamVersion)) {
    7.30 +            viewModel.setVersionFilter(ProjectView.ALL_VERSIONS);
    7.31 +        } else {
    7.32 +            final int vid = Functions.parseIntOrZero(pathParamVersion);
    7.33 +            if (vid > 0) {
    7.34 +                viewModel.setVersionFilter(versionDao.find(vid));
    7.35 +            }
    7.36          }
    7.37 -        // TODO: don't treat unknown == unassigned - send 404 for unknown and introduce special word for unassigned
    7.38  
    7.39          // Select Component
    7.40 -        final int cid = Functions.parseIntOrZero(pathParameters.get("component"));
    7.41 -        if (cid > 0) {
    7.42 -            viewModel.setComponentFilter(componentDao.find(cid));
    7.43 +        final var pathParamComponent = pathParameters.get("component");
    7.44 +        if ("no-component".equals(pathParamComponent)) {
    7.45 +            viewModel.setComponentFilter(ProjectView.NO_COMPONENT);
    7.46 +        } else if ("all-components".equals(pathParamComponent)) {
    7.47 +            viewModel.setComponentFilter(ProjectView.ALL_COMPONENTS);
    7.48 +        } else {
    7.49 +            final int cid = Functions.parseIntOrZero(pathParamComponent);
    7.50 +            if (cid > 0) {
    7.51 +                viewModel.setComponentFilter(componentDao.find(cid));
    7.52 +            }
    7.53          }
    7.54 -
    7.55 -        // TODO: distinguish all/unassigned for components
    7.56      }
    7.57  
    7.58      private ResponseType forwardView(HttpServletRequest req, ProjectView viewModel, String name) {
    7.59 @@ -133,7 +146,7 @@
    7.60          final var viewModel = new ProjectEditView();
    7.61          populate(viewModel, pathParams, dao);
    7.62  
    7.63 -        if (viewModel.getProjectInfo() == null) {
    7.64 +        if (!viewModel.isProjectInfoPresent()) {
    7.65              resp.sendError(HttpServletResponse.SC_NOT_FOUND);
    7.66              return ResponseType.NONE;
    7.67          }
    7.68 @@ -176,28 +189,60 @@
    7.69          }
    7.70      }
    7.71  
    7.72 -    @RequestMapping(requestPath = "$project/versions/$version", method = HttpMethod.GET)
    7.73 -    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws SQLException, IOException {
    7.74 +    @RequestMapping(requestPath = "$project/$component/$version/issues/", method = HttpMethod.GET)
    7.75 +    public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParams, DataAccessObjects dao) throws SQLException, IOException {
    7.76          final var viewModel = new ProjectDetailsView();
    7.77          populate(viewModel, pathParams, dao);
    7.78 -        final var version = viewModel.getVersionFilter();
    7.79  
    7.80 -        if (viewModel.getProjectInfo() == null || version == null) {
    7.81 +        if (!viewModel.isEveryFilterValid()) {
    7.82              resp.sendError(HttpServletResponse.SC_NOT_FOUND);
    7.83              return ResponseType.NONE;
    7.84          }
    7.85  
    7.86 +        final var project = viewModel.getProjectInfo().getProject();
    7.87 +        final var version = viewModel.getVersionFilter();
    7.88 +        final var component = viewModel.getComponentFilter();
    7.89 +
    7.90          final var issueDao = dao.getIssueDao();
    7.91  
    7.92 -        final var detailView = viewModel.getProjectDetails();
    7.93 -        final var issues = issueDao.list(version);
    7.94 +        final List<Issue> issues;
    7.95 +        if (version.equals(ProjectView.NO_VERSION)) {
    7.96 +            if (component.equals(ProjectView.ALL_COMPONENTS)) {
    7.97 +                issues = issueDao.list(project, (Version) null);
    7.98 +            } else if (component.equals(ProjectView.NO_COMPONENT)) {
    7.99 +                issues = issueDao.list(project, null, null);
   7.100 +            } else {
   7.101 +                issues = issueDao.list(project, component, null);
   7.102 +            }
   7.103 +        } else if (version.equals(ProjectView.ALL_VERSIONS)) {
   7.104 +            if (component.equals(ProjectView.ALL_COMPONENTS)) {
   7.105 +                issues = issueDao.list(project);
   7.106 +            } else if (component.equals(ProjectView.NO_COMPONENT)) {
   7.107 +                issues = issueDao.list(project, (Component)null);
   7.108 +            } else {
   7.109 +                issues = issueDao.list(project, component);
   7.110 +            }
   7.111 +        } else {
   7.112 +            if (component.equals(ProjectView.ALL_COMPONENTS)) {
   7.113 +                issues = issueDao.list(project, version);
   7.114 +            } else if (component.equals(ProjectView.NO_COMPONENT)) {
   7.115 +                issues = issueDao.list(project, null, version);
   7.116 +            } else {
   7.117 +                issues = issueDao.list(project, component, version);
   7.118 +            }
   7.119 +        }
   7.120 +
   7.121          for (var issue : issues) issueDao.joinVersionInformation(issue);
   7.122          issues.sort(new IssueSorter(
   7.123                  new IssueSorter.Criteria(IssueSorter.Field.PHASE, true),
   7.124                  new IssueSorter.Criteria(IssueSorter.Field.ETA, true),
   7.125                  new IssueSorter.Criteria(IssueSorter.Field.UPDATED, false)
   7.126          ));
   7.127 -        detailView.updateDetails(issues, version);
   7.128 +
   7.129 +
   7.130 +        viewModel.getProjectDetails().updateDetails(issues);
   7.131 +        if (version.getId() > 0)
   7.132 +            viewModel.getProjectDetails().updateVersionInfo(version);
   7.133  
   7.134          return forwardView(req, viewModel, "project-details");
   7.135      }
   7.136 @@ -262,7 +307,6 @@
   7.137              version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
   7.138              dao.getVersionDao().saveOrUpdate(version, project);
   7.139  
   7.140 -            // TODO: improve building the redirect location
   7.141              setRedirectLocation(req, "./projects/" + project.getId() + "/versions/");
   7.142              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   7.143          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
   7.144 @@ -274,11 +318,90 @@
   7.145          return ResponseType.HTML;
   7.146      }
   7.147  
   7.148 +    @RequestMapping(requestPath = "$project/components/", method = HttpMethod.GET)
   7.149 +    public ResponseType components(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
   7.150 +        final var viewModel = new ComponentsView();
   7.151 +        populate(viewModel, pathParameters, dao);
   7.152 +
   7.153 +        final var projectInfo = viewModel.getProjectInfo();
   7.154 +        if (projectInfo == null) {
   7.155 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   7.156 +            return ResponseType.NONE;
   7.157 +        }
   7.158 +
   7.159 +        final var issueDao = dao.getIssueDao();
   7.160 +        final var issues = issueDao.list(projectInfo.getProject());
   7.161 +        viewModel.update(projectInfo.getComponents(), issues);
   7.162 +
   7.163 +        return forwardView(req, viewModel, "components");
   7.164 +    }
   7.165 +
   7.166 +    @RequestMapping(requestPath = "$project/components/$component/edit", method = HttpMethod.GET)
   7.167 +    public ResponseType editComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
   7.168 +        final var viewModel = new ComponentEditView();
   7.169 +        populate(viewModel, pathParameters, dao);
   7.170 +
   7.171 +        if (viewModel.getProjectInfo() == null || viewModel.getComponentFilter() == null) {
   7.172 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   7.173 +            return ResponseType.NONE;
   7.174 +        }
   7.175 +
   7.176 +        viewModel.setComponent(viewModel.getComponentFilter());
   7.177 +        viewModel.setUsers(dao.getUserDao().list());
   7.178 +
   7.179 +        return forwardView(req, viewModel, "component-form");
   7.180 +    }
   7.181 +
   7.182 +    @RequestMapping(requestPath = "$project/create-component", method = HttpMethod.GET)
   7.183 +    public ResponseType createComponent(HttpServletRequest req, HttpServletResponse resp, PathParameters pathParameters, DataAccessObjects dao) throws IOException, SQLException {
   7.184 +        final var viewModel = new ComponentEditView();
   7.185 +        populate(viewModel, pathParameters, dao);
   7.186 +
   7.187 +        if (viewModel.getProjectInfo() == null) {
   7.188 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
   7.189 +            return ResponseType.NONE;
   7.190 +        }
   7.191 +
   7.192 +        viewModel.setComponent(new Component(-1));
   7.193 +        viewModel.setUsers(dao.getUserDao().list());
   7.194 +
   7.195 +        return forwardView(req, viewModel, "component-form");
   7.196 +    }
   7.197 +
   7.198 +    @RequestMapping(requestPath = "commit-component", method = HttpMethod.POST)
   7.199 +    public ResponseType commitComponent(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
   7.200 +
   7.201 +        try {
   7.202 +            final var project = new Project(getParameter(req, Integer.class, "pid").orElseThrow());
   7.203 +            final var component = new Component(getParameter(req, Integer.class, "id").orElseThrow());
   7.204 +            component.setName(getParameter(req, String.class, "name").orElseThrow());
   7.205 +            component.setColor(getParameter(req, WebColor.class, "color").orElseThrow());
   7.206 +            getParameter(req, Integer.class, "ordinal").ifPresent(component::setOrdinal);
   7.207 +            getParameter(req, Integer.class, "lead").map(
   7.208 +                    userid -> userid >= 0 ? new User(userid) : null
   7.209 +            ).ifPresent(component::setLead);
   7.210 +            getParameter(req, String.class, "description").ifPresent(component::setDescription);
   7.211 +
   7.212 +            dao.getComponentDao().saveOrUpdate(component, project);
   7.213 +
   7.214 +            setRedirectLocation(req, "./projects/" + project.getId() + "/components/");
   7.215 +            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   7.216 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
   7.217 +            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
   7.218 +            // TODO: implement - fix issue #21
   7.219 +            return ResponseType.NONE;
   7.220 +        }
   7.221 +
   7.222 +        return ResponseType.HTML;
   7.223 +    }
   7.224 +
   7.225      private void configureIssueEditor(IssueEditView viewModel, Issue issue, DataAccessObjects dao) throws SQLException {
   7.226 -        issue.setProject(viewModel.getProjectInfo().getProject());
   7.227 +        final var project = viewModel.getProjectInfo().getProject();
   7.228 +        issue.setProject(project);
   7.229          viewModel.setIssue(issue);
   7.230          viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
   7.231          viewModel.setUsers(dao.getUserDao().list());
   7.232 +        viewModel.setComponents(dao.getComponentDao().list(project));
   7.233          if (issue.getId() >= 0) {
   7.234              viewModel.setComments(dao.getIssueDao().listComments(issue));
   7.235          }
   7.236 @@ -337,6 +460,9 @@
   7.237              getParameter(req, Integer.class, "assignee").map(
   7.238                      userid -> userid >= 0 ? new User(userid) : null
   7.239              ).ifPresent(issue::setAssignee);
   7.240 +            getParameter(req, Integer.class, "component").map(
   7.241 +                    cid -> cid >= 0 ? new Component(cid) : null
   7.242 +            ).ifPresent(issue::setComponent);
   7.243              getParameter(req, String.class, "description").ifPresent(issue::setDescription);
   7.244              getParameter(req, Date.class, "eta").ifPresent(issue::setEta);
   7.245  
   7.246 @@ -354,7 +480,7 @@
   7.247              dao.getIssueDao().saveOrUpdate(issue, issue.getProject());
   7.248  
   7.249              // TODO: fix issue #14
   7.250 -            setRedirectLocation(req, "./projects/" + issue.getProject().getId() + "/versions/");
   7.251 +            setRedirectLocation(req, "./projects/" + issue.getProject().getId() + "/all-components/all-versions/issues/");
   7.252              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
   7.253  
   7.254              return ResponseType.HTML;
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ComponentEditView.java	Sat Oct 17 19:56:50 2020 +0200
     8.3 @@ -0,0 +1,40 @@
     8.4 +package de.uapcore.lightpit.viewmodel;
     8.5 +
     8.6 +import de.uapcore.lightpit.entities.Component;
     8.7 +import de.uapcore.lightpit.entities.User;
     8.8 +
     8.9 +import java.util.List;
    8.10 +
    8.11 +public class ComponentEditView extends ProjectView {
    8.12 +    private Component component;
    8.13 +    private List<User> users;
    8.14 +    private String errorText;
    8.15 +
    8.16 +    public ComponentEditView() {
    8.17 +        setSelectedPage(SELECTED_PAGE_COMPONENTS);
    8.18 +    }
    8.19 +
    8.20 +    public void setComponent(Component component) {
    8.21 +        this.component = component;
    8.22 +    }
    8.23 +
    8.24 +    public Component getComponent() {
    8.25 +        return component;
    8.26 +    }
    8.27 +
    8.28 +    public List<User> getUsers() {
    8.29 +        return users;
    8.30 +    }
    8.31 +
    8.32 +    public void setUsers(List<User> users) {
    8.33 +        this.users = users;
    8.34 +    }
    8.35 +
    8.36 +    public String getErrorText() {
    8.37 +        return errorText;
    8.38 +    }
    8.39 +
    8.40 +    public void setErrorText(String errorText) {
    8.41 +        this.errorText = errorText;
    8.42 +    }
    8.43 +}
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ComponentInfo.java	Sat Oct 17 19:56:50 2020 +0200
     9.3 @@ -0,0 +1,42 @@
     9.4 +package de.uapcore.lightpit.viewmodel;
     9.5 +
     9.6 +import de.uapcore.lightpit.entities.Component;
     9.7 +import de.uapcore.lightpit.entities.Issue;
     9.8 +import de.uapcore.lightpit.entities.IssueSummary;
     9.9 +
    9.10 +import java.util.ArrayList;
    9.11 +import java.util.List;
    9.12 +
    9.13 +public class ComponentInfo {
    9.14 +
    9.15 +    private final Component component;
    9.16 +
    9.17 +    private final IssueSummary issueSummary = new IssueSummary();
    9.18 +
    9.19 +    private final List<Issue> issues = new ArrayList<>();
    9.20 +
    9.21 +    public ComponentInfo(Component component) {
    9.22 +        this.component = component;
    9.23 +    }
    9.24 +
    9.25 +    public Component getComponent() {
    9.26 +        return component;
    9.27 +    }
    9.28 +
    9.29 +    public IssueSummary getIssueSummary() {
    9.30 +        return issueSummary;
    9.31 +    }
    9.32 +
    9.33 +    public List<Issue> getIssues() {
    9.34 +        return issues;
    9.35 +    }
    9.36 +
    9.37 +    public void collectIssues(List<Issue> issues) {
    9.38 +        for (Issue issue : issues) {
    9.39 +            if (component.equals(issue.getComponent())) {
    9.40 +                this.issues.add(issue);
    9.41 +                this.issueSummary.add(issue);
    9.42 +            }
    9.43 +        }
    9.44 +    }
    9.45 +}
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ComponentsView.java	Sat Oct 17 19:56:50 2020 +0200
    10.3 @@ -0,0 +1,29 @@
    10.4 +package de.uapcore.lightpit.viewmodel;
    10.5 +
    10.6 +import de.uapcore.lightpit.entities.Component;
    10.7 +import de.uapcore.lightpit.entities.Issue;
    10.8 +
    10.9 +import java.util.ArrayList;
   10.10 +import java.util.List;
   10.11 +
   10.12 +public class ComponentsView extends ProjectView {
   10.13 +
   10.14 +    private List<ComponentInfo> componentInfos = new ArrayList<>();
   10.15 +
   10.16 +    public ComponentsView() {
   10.17 +        setSelectedPage(SELECTED_PAGE_COMPONENTS);
   10.18 +    }
   10.19 +
   10.20 +    public void update(List<Component> components, List<Issue> issues) {
   10.21 +        componentInfos.clear();
   10.22 +        for (var component : components) {
   10.23 +            final var info = new ComponentInfo(component);
   10.24 +            info.collectIssues(issues);
   10.25 +            componentInfos.add(info);
   10.26 +        }
   10.27 +    }
   10.28 +
   10.29 +    public List<ComponentInfo> getComponentInfos() {
   10.30 +        return componentInfos;
   10.31 +    }
   10.32 +}
    11.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sat Oct 17 15:21:56 2020 +0200
    11.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Sat Oct 17 19:56:50 2020 +0200
    11.3 @@ -11,6 +11,7 @@
    11.4      private Set<Version> versionsUpcoming = new HashSet<>();
    11.5      private Set<Version> versionsRecent = new HashSet<>();
    11.6      private List<User> users;
    11.7 +    private List<Component> components;
    11.8      private List<IssueComment> comments;
    11.9  
   11.10      public void setIssue(Issue issue) {
   11.11 @@ -45,7 +46,8 @@
   11.12          versionsUpcoming.addAll(issue.getResolvedVersions());
   11.13          for (var v : versions) {
   11.14              if (v.getStatus().isReleased()) {
   11.15 -                versionsRecent.add(v);
   11.16 +                if (!v.getStatus().equals(VersionStatus.Deprecated))
   11.17 +                    versionsRecent.add(v);
   11.18              } else {
   11.19                  versionsUpcoming.add(v);
   11.20              }
   11.21 @@ -60,6 +62,14 @@
   11.22          this.users = users;
   11.23      }
   11.24  
   11.25 +    public List<Component> getComponents() {
   11.26 +        return components;
   11.27 +    }
   11.28 +
   11.29 +    public void setComponents(List<Component> components) {
   11.30 +        this.components = components;
   11.31 +    }
   11.32 +
   11.33      public IssueStatus[] getIssueStatus() {
   11.34          return IssueStatus.values();
   11.35      }
    12.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java	Sat Oct 17 15:21:56 2020 +0200
    12.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectDetails.java	Sat Oct 17 19:56:50 2020 +0200
    12.3 @@ -13,14 +13,15 @@
    12.4      private List<Issue> issues;
    12.5      private IssueSummary issueSummary;
    12.6  
    12.7 -    public void updateDetails(List<Issue> issues, Version version) {
    12.8 +    public void updateDetails(List<Issue> issues) {
    12.9          this.issues = issues;
   12.10          issueSummary = new IssueSummary();
   12.11          issues.forEach(issueSummary::add);
   12.12 -        if (version != null) {
   12.13 -            versionInfo = new VersionInfo(version);
   12.14 -            versionInfo.collectIssues(issues);
   12.15 -        }
   12.16 +    }
   12.17 +
   12.18 +    public void updateVersionInfo(Version version) {
   12.19 +        versionInfo = new VersionInfo(version);
   12.20 +        versionInfo.collectIssues(issues);
   12.21      }
   12.22  
   12.23      public List<Issue> getIssues() {
    13.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sat Oct 17 15:21:56 2020 +0200
    13.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Sat Oct 17 19:56:50 2020 +0200
    13.3 @@ -8,11 +8,29 @@
    13.4  
    13.5  public class ProjectView {
    13.6  
    13.7 +    public static final int SELECTED_PAGE_ISSUES = 0;
    13.8 +    public static final int SELECTED_PAGE_VERSIONS = 1;
    13.9 +    public static final int SELECTED_PAGE_COMPONENTS = 2;
   13.10 +
   13.11 +    public static final Version ALL_VERSIONS = new Version(0);
   13.12 +    public static final Version NO_VERSION = new Version(-1);
   13.13 +    public static final Component ALL_COMPONENTS = new Component(0);
   13.14 +    public static final Component NO_COMPONENT = new Component(-1);
   13.15 +
   13.16 +    static {
   13.17 +        ALL_VERSIONS.setNode("all-versions");
   13.18 +        NO_VERSION.setNode("no-version");
   13.19 +        ALL_COMPONENTS.setNode("all-components");
   13.20 +        NO_COMPONENT.setNode("no-component");
   13.21 +    }
   13.22 +
   13.23      private final List<ProjectInfo> projectList = new ArrayList<>();
   13.24      private ProjectInfo projectInfo;
   13.25      private Version versionFilter;
   13.26      private Component componentFilter;
   13.27  
   13.28 +    private int selectedPage = SELECTED_PAGE_ISSUES;
   13.29 +
   13.30      public List<ProjectInfo> getProjectList() {
   13.31          return projectList;
   13.32      }
   13.33 @@ -25,6 +43,14 @@
   13.34          this.projectInfo = projectInfo;
   13.35      }
   13.36  
   13.37 +    public int getSelectedPage() {
   13.38 +        return selectedPage;
   13.39 +    }
   13.40 +
   13.41 +    public void setSelectedPage(int selectedPage) {
   13.42 +        this.selectedPage = selectedPage;
   13.43 +    }
   13.44 +
   13.45      public Version getVersionFilter() {
   13.46          return versionFilter;
   13.47      }
   13.48 @@ -40,4 +66,20 @@
   13.49      public void setComponentFilter(Component componentFilter) {
   13.50          this.componentFilter = componentFilter;
   13.51      }
   13.52 +
   13.53 +    public boolean isProjectInfoPresent() {
   13.54 +        return projectInfo != null;
   13.55 +    }
   13.56 +
   13.57 +    public boolean isVersionFilterValid() {
   13.58 +        return projectInfo != null && versionFilter != null;
   13.59 +    }
   13.60 +
   13.61 +    public boolean isComponentFilterValid() {
   13.62 +        return projectInfo != null && componentFilter != null;
   13.63 +    }
   13.64 +
   13.65 +    public boolean isEveryFilterValid() {
   13.66 +        return projectInfo != null && versionFilter != null && componentFilter != null;
   13.67 +    }
   13.68  }
    14.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sat Oct 17 15:21:56 2020 +0200
    14.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Sat Oct 17 19:56:50 2020 +0200
    14.3 @@ -7,6 +7,10 @@
    14.4      private Version version;
    14.5      private String errorText;
    14.6  
    14.7 +    public VersionEditView() {
    14.8 +        setSelectedPage(SELECTED_PAGE_VERSIONS);
    14.9 +    }
   14.10 +
   14.11      public void setVersion(Version version) {
   14.12          this.version = version;
   14.13      }
    15.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java	Sat Oct 17 15:21:56 2020 +0200
    15.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionsView.java	Sat Oct 17 19:56:50 2020 +0200
    15.3 @@ -8,18 +8,22 @@
    15.4  
    15.5  public class VersionsView extends ProjectView {
    15.6  
    15.7 -    private List<VersionInfo> versionInfo = new ArrayList<>();
    15.8 +    private List<VersionInfo> versionInfos = new ArrayList<>();
    15.9 +
   15.10 +    public VersionsView() {
   15.11 +        setSelectedPage(SELECTED_PAGE_VERSIONS);
   15.12 +    }
   15.13  
   15.14      public void update(List<Version> versions, List<Issue> issues) {
   15.15 -        versionInfo.clear();
   15.16 +        versionInfos.clear();
   15.17          for (var version : versions) {
   15.18              final var info = new VersionInfo(version);
   15.19              info.collectIssues(issues);
   15.20 -            versionInfo.add(info);
   15.21 +            versionInfos.add(info);
   15.22          }
   15.23      }
   15.24  
   15.25 -    public List<VersionInfo> getVersionInfo() {
   15.26 -        return versionInfo;
   15.27 +    public List<VersionInfo> getVersionInfos() {
   15.28 +        return versionInfos;
   15.29      }
   15.30  }
    16.1 --- a/src/main/resources/localization/projects.properties	Sat Oct 17 15:21:56 2020 +0200
    16.2 +++ b/src/main/resources/localization/projects.properties	Sat Oct 17 19:56:50 2020 +0200
    16.3 @@ -24,6 +24,8 @@
    16.4  pageTitle=Project Tracking
    16.5  
    16.6  button.create=New Project
    16.7 +button.component.create=New Component
    16.8 +button.component.edit=Edit Component
    16.9  button.version.create=New Version
   16.10  button.version.edit=Edit Version
   16.11  button.issue.create=New Issue
   16.12 @@ -43,6 +45,8 @@
   16.13  owner=Project Lead
   16.14  version.latest=Latest Version
   16.15  version.next=Next Version
   16.16 +issues=Issues
   16.17 +component=Component
   16.18  
   16.19  progress=Overall Progress
   16.20  
   16.21 @@ -56,10 +60,20 @@
   16.22  version.name=Version
   16.23  version.status=Status
   16.24  version.ordinal=Ordering
   16.25 +
   16.26 +component.project=Project
   16.27 +component.name=Component
   16.28 +component.color=Color
   16.29 +component.lead=Lead
   16.30 +component.ordinal=Ordering
   16.31 +component.description=Description
   16.32 +
   16.33  tooltip.ordinal=Use to override lexicographic ordering.
   16.34  
   16.35  placeholder.null-owner=Unassigned
   16.36 +placeholder.null-lead=Unassigned
   16.37  placeholder.null-assignee=Unassigned
   16.38 +placeholder.null-component=Unassigned
   16.39  
   16.40  version.status.Future=Future
   16.41  version.status.Unreleased=Unreleased
   16.42 @@ -67,10 +81,10 @@
   16.43  version.status.LTS=LTS
   16.44  version.status.Deprecated=Deprecated
   16.45  
   16.46 -
   16.47  issue.without-version=No Assigned Version
   16.48  issue.id=Issue ID
   16.49  issue.project=Project
   16.50 +issue.component=Component
   16.51  issue.subject=Subject
   16.52  issue.description=Description
   16.53  issue.assignee=Assignee
    17.1 --- a/src/main/resources/localization/projects_de.properties	Sat Oct 17 15:21:56 2020 +0200
    17.2 +++ b/src/main/resources/localization/projects_de.properties	Sat Oct 17 19:56:50 2020 +0200
    17.3 @@ -24,6 +24,8 @@
    17.4  pageTitle=Projektverwaltung
    17.5  
    17.6  button.create=Neues Projekt
    17.7 +button.component.create=Neue Komponente
    17.8 +button.component.edit=Komponente Bearbeiten
    17.9  button.version.create=Neue Version
   17.10  button.version.edit=Version Bearbeiten
   17.11  button.issue.create=Neuer Vorgang
   17.12 @@ -43,6 +45,8 @@
   17.13  owner=Projektleitung
   17.14  version.latest=Neuste Version
   17.15  version.next=N\u00e4chste Version
   17.16 +component=Komponente
   17.17 +issues=Vorg\u00e4nge
   17.18  
   17.19  progress=Gesamtfortschritt
   17.20  
   17.21 @@ -56,10 +60,20 @@
   17.22  version.name=Version
   17.23  version.status=Status
   17.24  version.ordinal=Sequenznummer
   17.25 +
   17.26 +component.project=Projekt
   17.27 +component.name=Komponente
   17.28 +component.color=Farbe
   17.29 +component.lead=Leitung
   17.30 +component.ordinal=Sequenznummer
   17.31 +component.description=Beschreibung
   17.32 +
   17.33  tooltip.ordinal=\u00dcbersteuert die lexikographische Sortierung.
   17.34  
   17.35  placeholder.null-owner=Nicht Zugewiesen
   17.36 +placeholder.null-lead=Niemand
   17.37  placeholder.null-assignee=Niemandem
   17.38 +placeholder.null-component=Keine
   17.39  
   17.40  version.status.Future=Geplant
   17.41  version.status.Unreleased=Unver\u00f6ffentlicht
   17.42 @@ -70,6 +84,7 @@
   17.43  issue.without-version=Keine Version zugeordnet
   17.44  issue.id=Vorgangs-ID
   17.45  issue.project=Projekt
   17.46 +issue.component=Komponente
   17.47  issue.subject=Thema
   17.48  issue.description=Beschreibung
   17.49  issue.assignee=Zugewiesen
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/src/main/webapp/WEB-INF/jsp/component-form.jsp	Sat Oct 17 19:56:50 2020 +0200
    18.3 @@ -0,0 +1,95 @@
    18.4 +<%--
    18.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    18.6 +
    18.7 +Copyright 2020 Mike Becker. All rights reserved.
    18.8 +
    18.9 +Redistribution and use in source and binary forms, with or without
   18.10 +modification, are permitted provided that the following conditions are met:
   18.11 +
   18.12 +1. Redistributions of source code must retain the above copyright
   18.13 +notice, this list of conditions and the following disclaimer.
   18.14 +
   18.15 +2. Redistributions in binary form must reproduce the above copyright
   18.16 +notice, this list of conditions and the following disclaimer in the
   18.17 +documentation and/or other materials provided with the distribution.
   18.18 +
   18.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   18.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   18.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   18.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   18.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   18.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   18.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   18.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   18.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   18.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   18.29 +--%>
   18.30 +<%@page pageEncoding="UTF-8" %>
   18.31 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   18.32 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   18.33 +
   18.34 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ComponentEditView" scope="request" />
   18.35 +<c:set var="component" scope="page" value="${viewmodel.component}"/>
   18.36 +<c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
   18.37 +
   18.38 +<form action="./projects/commit-component" method="post">
   18.39 +    <table class="formtable" style="width: 70ch">
   18.40 +        <colgroup>
   18.41 +            <col>
   18.42 +            <col style="width: 100%">
   18.43 +        </colgroup>
   18.44 +        <tbody>
   18.45 +        <tr>
   18.46 +            <th><fmt:message key="component.project"/></th>
   18.47 +            <td>
   18.48 +                <c:out value="${project.name}" />
   18.49 +                <input type="hidden" name="pid" value="${project.id}" />
   18.50 +            </td>
   18.51 +        </tr>
   18.52 +        <tr>
   18.53 +            <th><fmt:message key="component.name"/></th>
   18.54 +            <td><input name="name" type="text" maxlength="20" required value="<c:out value="${component.name}"/>" /></td>
   18.55 +        </tr>
   18.56 +        <tr>
   18.57 +            <th><fmt:message key="component.color"/></th>
   18.58 +            <td><input name="color" type="color" required value="${component.color}" /></td>
   18.59 +        </tr>
   18.60 +        <tr>
   18.61 +            <th><fmt:message key="component.lead"/></th>
   18.62 +            <td>
   18.63 +                <select name="lead">
   18.64 +                    <option value="-1"><fmt:message key="placeholder.null-lead"/></option>
   18.65 +                    <c:forEach var="user" items="${viewmodel.users}">
   18.66 +                        <option
   18.67 +                                <c:if test="${not empty component.lead and user eq component.lead}">selected</c:if>
   18.68 +                                value="${user.id}"><c:out value="${user.displayname}"/></option>
   18.69 +                    </c:forEach>
   18.70 +                </select>
   18.71 +            </td>
   18.72 +        </tr>
   18.73 +        <tr title="<fmt:message key="tooltip.ordinal" />">
   18.74 +            <th><fmt:message key="component.ordinal"/></th>
   18.75 +            <td>
   18.76 +                <input name="ordinal" type="number" min="0" value="${component.ordinal}"/>
   18.77 +            </td>
   18.78 +        </tr>
   18.79 +        <tr>
   18.80 +            <th class="vtop"><fmt:message key="component.description"/></th>
   18.81 +            <td>
   18.82 +                <textarea name="description" rows="5"><c:out value="${component.description}"/></textarea>
   18.83 +            </td>
   18.84 +        </tr>
   18.85 +        </tbody>
   18.86 +        <tfoot>
   18.87 +        <tr>
   18.88 +            <td colspan="2">
   18.89 +                <input type="hidden" name="id" value="${component.id}"/>
   18.90 +                <a href="./projects/${project.node}/components/" class="button">
   18.91 +                    <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
   18.92 +                </a>
   18.93 +                <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
   18.94 +            </td>
   18.95 +        </tr>
   18.96 +        </tfoot>
   18.97 +    </table>
   18.98 +</form>
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/src/main/webapp/WEB-INF/jsp/components.jsp	Sat Oct 17 19:56:50 2020 +0200
    19.3 @@ -0,0 +1,95 @@
    19.4 +<%--
    19.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    19.6 +
    19.7 +Copyright 2020 Mike Becker. All rights reserved.
    19.8 +
    19.9 +Redistribution and use in source and binary forms, with or without
   19.10 +modification, are permitted provided that the following conditions are met:
   19.11 +
   19.12 +1. Redistributions of source code must retain the above copyright
   19.13 +notice, this list of conditions and the following disclaimer.
   19.14 +
   19.15 +2. Redistributions in binary form must reproduce the above copyright
   19.16 +notice, this list of conditions and the following disclaimer in the
   19.17 +documentation and/or other materials provided with the distribution.
   19.18 +
   19.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   19.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   19.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   19.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   19.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   19.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   19.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   19.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   19.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   19.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   19.29 +--%>
   19.30 +<%@page pageEncoding="UTF-8" %>
   19.31 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   19.32 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   19.33 +
   19.34 +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ComponentsView" scope="request" />
   19.35 +
   19.36 +<c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
   19.37 +
   19.38 +<%@include file="../jspf/project-header.jspf"%>
   19.39 +
   19.40 +<div id="tool-area">
   19.41 +    <a href="./projects/${project.id}/create-component" class="button"><fmt:message key="button.component.create"/></a>
   19.42 +    <a href="./projects/${project.id}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
   19.43 +</div>
   19.44 +
   19.45 +<h2><fmt:message key="progress" /></h2>
   19.46 +
   19.47 +<c:set var="summary" value="${viewmodel.projectInfo.issueSummary}" />
   19.48 +<%@include file="../jspf/issue-summary.jspf"%>
   19.49 +
   19.50 +<table id="version-list" class="datatable medskip fullwidth">
   19.51 +    <colgroup>
   19.52 +        <col>
   19.53 +        <col width="20%">
   19.54 +        <col width="44%">
   19.55 +        <col width="12%">
   19.56 +        <col width="12%">
   19.57 +        <col width="12%">
   19.58 +    </colgroup>
   19.59 +    <thead>
   19.60 +    <tr>
   19.61 +        <th colspan="3"></th>
   19.62 +        <th colspan="3" class="hcenter">
   19.63 +            <fmt:message key="issues"/>
   19.64 +        </th>
   19.65 +    </tr>
   19.66 +    <tr>
   19.67 +        <th></th>
   19.68 +        <th><fmt:message key="component.name"/></th>
   19.69 +        <th><fmt:message key="component.description"/></th>
   19.70 +        <th class="hcenter"><fmt:message key="issues.open" /></th>
   19.71 +        <th class="hcenter"><fmt:message key="issues.active" /></th>
   19.72 +        <th class="hcenter"><fmt:message key="issues.done" /></th>
   19.73 +    </tr>
   19.74 +    </thead>
   19.75 +    <tbody>
   19.76 +        <c:forEach var="componentInfo" items="${viewmodel.componentInfos}" >
   19.77 +        <tr>
   19.78 +            <td rowspan="2" style="width: 2em;"><a href="./projects/${project.node}/components/${componentInfo.component.node}/edit">&#x270e;</a></td>
   19.79 +            <td rowspan="2">
   19.80 +                <div class="navmenu-icon" style="background-color: ${componentInfo.component.color}"></div>
   19.81 +                <a href="./projects/${project.node}/${componentInfo.component.node}/all-versions/issues/">
   19.82 +                    <c:out value="${componentInfo.component.name}"/>
   19.83 +                </a>
   19.84 +            </td>
   19.85 +            <td rowspan="2"><c:out value="${componentInfo.component.description}"/> </td>
   19.86 +            <td class="hright">${componentInfo.issueSummary.open}</td>
   19.87 +            <td class="hright">${componentInfo.issueSummary.active}</td>
   19.88 +            <td class="hright">${componentInfo.issueSummary.done}</td>
   19.89 +        </tr>
   19.90 +        <tr>
   19.91 +            <td colspan="3">
   19.92 +                <c:set var="summary" value="${componentInfo.issueSummary}"/>
   19.93 +                <%@include file="../jspf/issue-progress.jspf" %>
   19.94 +            </td>
   19.95 +        </tr>
   19.96 +        </c:forEach>
   19.97 +    </tbody>
   19.98 +</table>
   19.99 \ No newline at end of file
    20.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Oct 17 15:21:56 2020 +0200
    20.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat Oct 17 19:56:50 2020 +0200
    20.3 @@ -74,6 +74,19 @@
    20.4              <td><fmt:formatDate value="${issue.updated}" /></td>
    20.5          </tr>
    20.6          <tr>
    20.7 +            <th><fmt:message key="issue.component"/></th>
    20.8 +            <td>
    20.9 +                <select name="component">
   20.10 +                    <option value="-1"><fmt:message key="placeholder.null-component"/></option>
   20.11 +                    <c:forEach var="component" items="${viewmodel.components}">
   20.12 +                        <option
   20.13 +                                <c:if test="${not empty issue.component and component eq issue.component}">selected</c:if>
   20.14 +                                value="${component.id}"><c:out value="${component.name}"/></option>
   20.15 +                    </c:forEach>
   20.16 +                </select>
   20.17 +            </td>
   20.18 +        </tr>
   20.19 +        <tr>
   20.20              <th><fmt:message key="issue.category"/></th>
   20.21              <td>
   20.22                  <select name="category">
   20.23 @@ -154,7 +167,7 @@
   20.24              <td colspan="2">
   20.25                  <input type="hidden" name="id" value="${issue.id}"/>
   20.26                  <%-- TODO: fix #14 --%>
   20.27 -                <a href="./projects/${issue.project.id}/versions/" class="button">
   20.28 +                <a href="./projects/${issue.project.node}/versions/" class="button">
   20.29                      <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
   20.30                  </a>
   20.31                  <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
    21.1 --- a/src/main/webapp/WEB-INF/jsp/issues.jsp	Sat Oct 17 15:21:56 2020 +0200
    21.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
    21.3 @@ -1,57 +0,0 @@
    21.4 -<%--
    21.5 -DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
    21.6 -
    21.7 -Copyright 2018 Mike Becker. All rights reserved.
    21.8 -
    21.9 -Redistribution and use in source and binary forms, with or without
   21.10 -modification, are permitted provided that the following conditions are met:
   21.11 -
   21.12 -1. Redistributions of source code must retain the above copyright
   21.13 -notice, this list of conditions and the following disclaimer.
   21.14 -
   21.15 -2. Redistributions in binary form must reproduce the above copyright
   21.16 -notice, this list of conditions and the following disclaimer in the
   21.17 -documentation and/or other materials provided with the distribution.
   21.18 -
   21.19 -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   21.20 -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   21.21 -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   21.22 -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
   21.23 -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   21.24 -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
   21.25 -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   21.26 -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   21.27 -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   21.28 -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   21.29 ---%>
   21.30 -<%@page pageEncoding="UTF-8" %>
   21.31 -<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   21.32 -<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   21.33 -
   21.34 -<h1>TODO: REWRITE THIS PAGE</h1>
   21.35 -<%--
   21.36 -TODO: rewrite
   21.37 -<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssuesView" scope="request"/>
   21.38 -<c:set var="project" scope="page" value="${viewmodel.project}"/>
   21.39 -<c:set var="version" scope="page" value="${viewmodel.version}"/>
   21.40 -<%@include file="../jspf/project-header.jspf"%>
   21.41 -
   21.42 -<c:if test="${not empty version}">
   21.43 -    <h2>
   21.44 -        <fmt:message key="version.name" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
   21.45 -        <a href="./projects/versions/edit?vid=${version.id}">&#x270e;</a>
   21.46 -    </h2>
   21.47 -</c:if>
   21.48 -
   21.49 -<div id="tool-area">
   21.50 -    <div>
   21.51 -        <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
   21.52 -        <c:if test="${not empty version}">
   21.53 -            <a href="./projects/issues/?pid=${project.id}&vid=-1" class="button"><fmt:message key="button.issue.all"/></a>
   21.54 -        </c:if>
   21.55 -    </div>
   21.56 -</div>
   21.57 -
   21.58 -<c:set var="issues" value="${viewmodel.issues}"/>
   21.59 -<%@include file="../jspf/issue-list.jspf"%>
   21.60 ---%>
   21.61 \ No newline at end of file
    22.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Oct 17 15:21:56 2020 +0200
    22.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat Oct 17 19:56:50 2020 +0200
    22.3 @@ -24,21 +24,26 @@
    22.4  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    22.5  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    22.6  --%>
    22.7 -<%@page pageEncoding="UTF-8" %>
    22.8 +<%@page pageEncoding="UTF-8" import="de.uapcore.lightpit.viewmodel.ProjectView" %>
    22.9  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   22.10  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   22.11  
   22.12  <jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectDetailsView" scope="request" />
   22.13  
   22.14  <c:set var="project" scope="page" value="${viewmodel.projectInfo.project}"/>
   22.15 +<c:set var="component" scope="page" value="${viewmodel.componentFilter}"/>
   22.16  <%@include file="../jspf/project-header.jspf"%>
   22.17  
   22.18  <div id="tool-area">
   22.19 -    <c:if test="${not empty viewmodel.versionFilter}">
   22.20 -        <a href="./projects/${project.id}/versions/${viewmodel.versionFilter.id}/edit" class="button"><fmt:message key="button.version.edit"/></a>
   22.21 +    <a href="./projects/${project.node}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
   22.22 +    <c:if test="${viewmodel.versionFilter.id gt 0}">
   22.23 +        <a href="./projects/${project.node}/versions/${viewmodel.versionFilter.node}/edit" class="button"><fmt:message key="button.version.edit"/></a>
   22.24      </c:if>
   22.25 -    <a href="./projects/${project.id}/create-version" class="button"><fmt:message key="button.version.create"/></a>
   22.26 -    <a href="./projects/${project.id}/create-issue" class="button"><fmt:message key="button.issue.create"/></a>
   22.27 +    <a href="./projects/${project.node}/create-version" class="button"><fmt:message key="button.version.create"/></a>
   22.28 +    <c:if test="${viewmodel.componentFilter.id gt 0}">
   22.29 +        <a href="./projects/${project.node}/components/${viewmodel.componentFilter.node}/edit" class="button"><fmt:message key="button.component.edit"/></a>
   22.30 +    </c:if>
   22.31 +    <a href="./projects/${project.node}/create-component" class="button"><fmt:message key="button.component.create"/></a>
   22.32  </div>
   22.33  
   22.34  <h2><fmt:message key="progress" /></h2>
   22.35 @@ -46,36 +51,43 @@
   22.36  <c:set var="summary" value="${viewmodel.projectInfo.issueSummary}" />
   22.37  <%@include file="../jspf/issue-summary.jspf"%>
   22.38  
   22.39 -<c:if test="${not empty viewmodel.versionFilter}">
   22.40 -    <c:set var="versionInfo" value="${viewmodel.projectDetails.versionInfo}"/>
   22.41 -    <h2>
   22.42 -        <fmt:message key="version.name" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
   22.43 -    </h2>
   22.44 +<c:choose>
   22.45 +    <c:when test="${viewmodel.versionFilter eq ProjectView.NO_VERSION or viewmodel.versionFilter eq ProjectView.ALL_VERSIONS}">
   22.46 +        <h2>
   22.47 +            <c:if test="${viewmodel.versionFilter eq ProjectView.NO_VERSION}">
   22.48 +                <fmt:message key="issue.without-version" />
   22.49 +            </c:if>
   22.50 +            <c:if test="${viewmodel.versionFilter ne ProjectView.NO_VERSION}">
   22.51 +                <fmt:message key="issues" />
   22.52 +            </c:if>
   22.53 +        </h2>
   22.54 +        <c:set var="summary" value="${viewmodel.projectDetails.issueSummary}"/>
   22.55 +        <c:set var="issues" value="${viewmodel.projectDetails.issues}"/>
   22.56 +        <%@include file="../jspf/issue-summary.jspf"%>
   22.57 +        <c:if test="${not empty issues}">
   22.58 +            <%@include file="../jspf/issue-list.jspf"%>
   22.59 +        </c:if>
   22.60 +    </c:when>
   22.61 +    <c:otherwise>
   22.62 +        <c:set var="versionInfo" value="${viewmodel.projectDetails.versionInfo}"/>
   22.63 +        <h2>
   22.64 +            <fmt:message key="version.name" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
   22.65 +        </h2>
   22.66  
   22.67 -    <h3><fmt:message key="issues.resolved"/> </h3>
   22.68 -    <c:set var="summary" value="${versionInfo.resolvedTotal}"/>
   22.69 -    <%@include file="../jspf/issue-summary.jspf"%>
   22.70 -    <c:set var="issues" value="${versionInfo.resolved}"/>
   22.71 -    <c:if test="${not empty issues}">
   22.72 -        <%@include file="../jspf/issue-list.jspf"%>
   22.73 -    </c:if>
   22.74 +        <h3><fmt:message key="issues.resolved"/> </h3>
   22.75 +        <c:set var="summary" value="${versionInfo.resolvedTotal}"/>
   22.76 +        <%@include file="../jspf/issue-summary.jspf"%>
   22.77 +        <c:set var="issues" value="${versionInfo.resolved}"/>
   22.78 +        <c:if test="${not empty issues}">
   22.79 +            <%@include file="../jspf/issue-list.jspf"%>
   22.80 +        </c:if>
   22.81  
   22.82 -    <c:set var="issues" value="${versionInfo.reported}"/>
   22.83 -    <c:if test="${not empty issues}">
   22.84 -        <h3><fmt:message key="issues.reported"/> </h3>
   22.85 -        <c:set var="summary" value="${versionInfo.reportedTotal}"/>
   22.86 -        <%@include file="../jspf/issue-summary.jspf"%>
   22.87 -        <%@include file="../jspf/issue-list.jspf"%>
   22.88 -    </c:if>
   22.89 -</c:if>
   22.90 -<c:if test="${empty viewmodel.versionFilter}">
   22.91 -    <h2>
   22.92 -        <fmt:message key="issue.without-version" />
   22.93 -    </h2>
   22.94 -    <c:set var="summary" value="${viewmodel.projectDetails.issueSummary}"/>
   22.95 -    <c:set var="issues" value="${viewmodel.projectDetails.issues}"/>
   22.96 -    <%@include file="../jspf/issue-summary.jspf"%>
   22.97 -    <c:if test="${not empty issues}">
   22.98 -        <%@include file="../jspf/issue-list.jspf"%>
   22.99 -    </c:if>
  22.100 -</c:if>
  22.101 +        <c:set var="issues" value="${versionInfo.reported}"/>
  22.102 +        <c:if test="${not empty issues}">
  22.103 +            <h3><fmt:message key="issues.reported"/> </h3>
  22.104 +            <c:set var="summary" value="${versionInfo.reportedTotal}"/>
  22.105 +            <%@include file="../jspf/issue-summary.jspf"%>
  22.106 +            <%@include file="../jspf/issue-list.jspf"%>
  22.107 +        </c:if>
  22.108 +    </c:otherwise>
  22.109 +</c:choose>
  22.110 \ No newline at end of file
    23.1 --- a/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Sat Oct 17 15:21:56 2020 +0200
    23.2 +++ b/src/main/webapp/WEB-INF/jsp/project-navmenu.jsp	Sat Oct 17 19:56:50 2020 +0200
    23.3 @@ -24,7 +24,7 @@
    23.4  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    23.5  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    23.6  --%>
    23.7 -<%@page pageEncoding="UTF-8" %>
    23.8 +<%@page pageEncoding="UTF-8" import="de.uapcore.lightpit.viewmodel.ProjectView" %>
    23.9  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
   23.10  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   23.11  
   23.12 @@ -33,20 +33,27 @@
   23.13  <c:forEach var="projectInfo" items="${viewmodel.projectList}">
   23.14      <c:set var="isActive" value="${viewmodel.projectInfo.project eq projectInfo.project}" />
   23.15      <div class="menuEntry level-0" <c:if test="${isActive}">data-active</c:if> >
   23.16 -        <a href="projects/${projectInfo.project.id}/versions/">
   23.17 +        <a href="projects/${projectInfo.project.node}/versions/">
   23.18              <c:out value="${projectInfo.project.name}"/>
   23.19          </a>
   23.20      </div>
   23.21      <c:if test="${isActive}">
   23.22          <!-- VERSIONS -->
   23.23 -        <div class="menuEntry level-1">
   23.24 -            <a href="projects/${projectInfo.project.id}/versions/">
   23.25 +        <c:set var="componentNode" value="${not empty viewmodel.componentFilter ? viewmodel.componentFilter.node : 'all-components'}"/>
   23.26 +        <div class="menuEntry level-1" <c:if test="${viewmodel.selectedPage eq ProjectView.SELECTED_PAGE_VERSIONS}">data-active</c:if> >
   23.27 +            <a href="projects/${projectInfo.project.node}/versions/">
   23.28                  <fmt:message key="navmenu.versions"/>
   23.29              </a>
   23.30          </div>
   23.31 -        <div class="menuEntry level-2">
   23.32 +        <div class="menuEntry level-2" <c:if test="${viewmodel.versionFilter eq ProjectView.ALL_VERSIONS}">data-active</c:if>>
   23.33              <div class="navmenu-icon" style="background: black"></div>
   23.34 -            <a href="projects/${projectInfo.project.id}/versions/unassigned">
   23.35 +            <a href="projects/${projectInfo.project.node}/${componentNode}/all-versions/issues/">
   23.36 +                <fmt:message key="navmenu.all" />
   23.37 +            </a>
   23.38 +        </div>
   23.39 +        <div class="menuEntry level-2" <c:if test="${viewmodel.versionFilter eq ProjectView.NO_VERSION}">data-active</c:if>>
   23.40 +            <div class="navmenu-icon" style="background: black"></div>
   23.41 +            <a href="projects/${projectInfo.project.node}/${componentNode}/no-version/issues/">
   23.42                  <fmt:message key="navmenu.unassigned" />
   23.43              </a>
   23.44          </div>
   23.45 @@ -55,39 +62,38 @@
   23.46              <div class="menuEntry level-2" <c:if test="${isVersionActive}">data-active</c:if>
   23.47                      title="<fmt:message key="version.status.${version.status}" />">
   23.48                  <div class="navmenu-icon version-${version.status}"></div>
   23.49 -                <a href="projects/${projectInfo.project.id}/versions/${version.id}">
   23.50 +                <a href="projects/${projectInfo.project.node}/${componentNode}/${version.node}/issues/">
   23.51                      <c:out value="${version.name}"/>
   23.52                  </a>
   23.53              </div>
   23.54          </c:forEach>
   23.55 -        <%-- COMPONENTS
   23.56 -        TODO: find a way to combine version and component into one URL
   23.57 -        <div class="menuEntry level-1">
   23.58 -            <a href="projects/${projectInfo.project.id}/components/">
   23.59 +        <!-- COMPONENTS -->
   23.60 +        <c:set var="versionNode" value="${not empty viewmodel.versionFilter ? viewmodel.versionFilter.node : 'all-versions'}"/>
   23.61 +        <div class="menuEntry level-1" <c:if test="${viewmodel.selectedPage eq ProjectView.SELECTED_PAGE_COMPONENTS}">data-active</c:if>>
   23.62 +            <a href="projects/${projectInfo.project.node}/components/">
   23.63                  <fmt:message key="navmenu.components"/>
   23.64              </a>
   23.65          </div>
   23.66 -        <div class="menuEntry level-2">
   23.67 +        <div class="menuEntry level-2" <c:if test="${viewmodel.componentFilter eq ProjectView.ALL_COMPONENTS}">data-active</c:if>>
   23.68              <div class="navmenu-icon" style="background: black"></div>
   23.69 -            <a href="projects/${projectInfo.project.id}/components/">
   23.70 +            <a href="projects/${projectInfo.project.node}/all-components/${versionNode}/issues/">
   23.71                  <fmt:message key="navmenu.all" />
   23.72              </a>
   23.73          </div>
   23.74 -        <div class="menuEntry level-2">
   23.75 +        <div class="menuEntry level-2"  <c:if test="${viewmodel.componentFilter eq ProjectView.NO_COMPONENT}">data-active</c:if>>
   23.76              <div class="navmenu-icon" style="background: black"></div>
   23.77 -            <a href="projects/${projectInfo.project.id}/components/unassigned">
   23.78 +            <a href="projects/${projectInfo.project.node}/no-component/${versionNode}/issues/">
   23.79                  <fmt:message key="navmenu.unassigned" />
   23.80              </a>
   23.81          </div>
   23.82          <c:forEach var="component" items="${viewmodel.projectInfo.components}">
   23.83              <c:set var="isComponentActive" value="${viewmodel.componentFilter eq component}" />
   23.84 -            <div class="menuEntry level-2" <c:if test="${isVersionActive}">data-active</c:if> >
   23.85 +            <div class="menuEntry level-2" <c:if test="${isComponentActive}">data-active</c:if> >
   23.86                  <div class="navmenu-icon" style="background-color: ${component.color}"></div>
   23.87 -                <a href="projects/view?pid=${projectInfo.project.id}&cid=${component.id}">
   23.88 +                <a href="projects/${projectInfo.project.node}/${component.node}/${versionNode}/issues/">
   23.89                      <c:out value="${component.name}"/>
   23.90                  </a>
   23.91              </div>
   23.92          </c:forEach>
   23.93 -        --%>
   23.94      </c:if>
   23.95  </c:forEach>
    24.1 --- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat Oct 17 15:21:56 2020 +0200
    24.2 +++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat Oct 17 19:56:50 2020 +0200
    24.3 @@ -68,8 +68,8 @@
    24.4          <c:forEach var="projectInfo" items="${viewmodel.projectList}">
    24.5              <c:set var="project" scope="page" value="${projectInfo.project}"/>
    24.6              <tr class="nowrap">
    24.7 -                <td style="width: 2em;"><a href="./projects/${project.id}/edit">&#x270e;</a></td>
    24.8 -                <td><a href="./projects/${project.id}/versions/"><c:out value="${project.name}"/></a>
    24.9 +                <td style="width: 2em;"><a href="./projects/${project.node}/edit">&#x270e;</a></td>
   24.10 +                <td><a href="./projects/${project.node}/versions/"><c:out value="${project.name}"/></a>
   24.11                  </td>
   24.12                  <td>
   24.13                      <c:if test="${not empty project.repoUrl}">
   24.14 @@ -79,12 +79,12 @@
   24.15                  </td>
   24.16                  <td class="hright">
   24.17                      <c:if test="${not empty projectInfo.latestVersion}">
   24.18 -                        <a href="./projects/${project.id}/versions/${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
   24.19 +                        <a href="./projects/${project.node}/all-components/${projectInfo.latestVersion.node}/issues/"><c:out value="${projectInfo.latestVersion.name}"/></a>
   24.20                      </c:if>
   24.21                  </td>
   24.22                  <td class="hright">
   24.23                      <c:if test="${not empty projectInfo.nextVersion}">
   24.24 -                        <a href="./projects/${project.id}/versions/${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
   24.25 +                        <a href="./projects/${project.node}/all-components/${projectInfo.nextVersion.node}/issues/"><c:out value="${projectInfo.nextVersion.name}"/></a>
   24.26                      </c:if>
   24.27                  </td>
   24.28                  <td class="hright">${projectInfo.issueSummary.open}</td>
    25.1 --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Oct 17 15:21:56 2020 +0200
    25.2 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat Oct 17 19:56:50 2020 +0200
    25.3 @@ -73,7 +73,7 @@
    25.4          <tr>
    25.5              <td colspan="2">
    25.6                  <input type="hidden" name="id" value="${version.id}"/>
    25.7 -                <a href="./projects/${project.id}/versions/" class="button">
    25.8 +                <a href="./projects/${project.node}/versions/" class="button">
    25.9                      <fmt:message bundle="${lightpit_bundle}" key="button.cancel"/>
   25.10                  </a>
   25.11                  <button type="submit"><fmt:message bundle="${lightpit_bundle}" key="button.okay"/></button>
    26.1 --- a/src/main/webapp/WEB-INF/jsp/versions.jsp	Sat Oct 17 15:21:56 2020 +0200
    26.2 +++ b/src/main/webapp/WEB-INF/jsp/versions.jsp	Sat Oct 17 19:56:50 2020 +0200
    26.3 @@ -76,11 +76,11 @@
    26.4      </tr>
    26.5      </thead>
    26.6      <tbody>
    26.7 -        <c:forEach var="versionInfo" items="${viewmodel.versionInfo}" >
    26.8 +        <c:forEach var="versionInfo" items="${viewmodel.versionInfos}" >
    26.9          <tr>
   26.10 -            <td rowspan="2" style="width: 2em;"><a href="./projects/${project.id}/versions/${versionInfo.version.id}/edit">&#x270e;</a></td>
   26.11 +            <td rowspan="2" style="width: 2em;"><a href="./projects/${project.node}/versions/${versionInfo.version.node}/edit">&#x270e;</a></td>
   26.12              <td rowspan="2">
   26.13 -                <a href="./projects/${project.id}/versions/${versionInfo.version.id}">
   26.14 +                <a href="./projects/${project.node}/all-components/${versionInfo.version.node}/issues/">
   26.15                      <c:out value="${versionInfo.version.name}"/>
   26.16                  </a>
   26.17                  <div class="version-tag version-${versionInfo.version.status}">
    27.1 --- a/src/main/webapp/WEB-INF/jspf/project-header.jspf	Sat Oct 17 15:21:56 2020 +0200
    27.2 +++ b/src/main/webapp/WEB-INF/jspf/project-header.jspf	Sat Oct 17 19:56:50 2020 +0200
    27.3 @@ -1,5 +1,6 @@
    27.4  <%--
    27.5  project: Project
    27.6 +component: Component (optional)
    27.7  --%>
    27.8  <div class="table project-attributes">
    27.9      <div class="row">
   27.10 @@ -21,4 +22,19 @@
   27.11              </c:if>
   27.12          </div>
   27.13      </div>
   27.14 +    <c:if test="${not empty component and component.id gt 0}">
   27.15 +        <div class="row">
   27.16 +            <div class="caption"><fmt:message key="component"/>:</div>
   27.17 +            <div><c:out value="${component.name}"/></div>
   27.18 +            <div class="caption"><fmt:message key="component.lead"/>:</div>
   27.19 +            <div>
   27.20 +                <c:if test="${not empty component.lead}">
   27.21 +                    <c:out value="${component.lead.displayname}"/>
   27.22 +                </c:if>
   27.23 +                <c:if test="${empty component.lead}">
   27.24 +                    <fmt:message key="placeholder.null-lead"/>
   27.25 +                </c:if>
   27.26 +            </div>
   27.27 +        </div>
   27.28 +    </c:if>
   27.29  </div>

mercurial