Mon, 01 Jun 2020 14:46:58 +0200
improves issue overview and adds progress information
--- a/pom.xml Sat May 30 18:12:38 2020 +0200 +++ b/pom.xml Mon Jun 01 14:46:58 2020 +0200 @@ -4,7 +4,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>de.uapcore</groupId> <artifactId>lightpit</artifactId> - <version>0.1-SNAPSHOT</version> + <version>0.2</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Mon Jun 01 14:46:58 2020 +0200 @@ -88,6 +88,7 @@ /** * Returns the name of the resource bundle associated with this servlet. + * * @return the resource bundle base name */ protected abstract String getResourceBundleName(); @@ -266,6 +267,17 @@ } /** + * Sets the view model object. + * The type must match the expected type in the JSP file. + * + * @param req the servlet request object + * @param viewModel the view model object + */ + public void setViewModel(HttpServletRequest req, Object viewModel) { + req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel); + } + + /** * Obtains a request parameter of the specified type. * The specified type must have a single-argument constructor accepting a string to perform conversion. * The constructor of the specified type may throw an exception on conversion failures. @@ -281,7 +293,7 @@ final String[] paramValues = req.getParameterValues(name); int len = paramValues == null ? 0 : paramValues.length; final var array = (T) Array.newInstance(clazz.getComponentType(), len); - for (int i = 0 ; i < len ; i++) { + for (int i = 0; i < len; i++) { try { final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class); Array.set(array, i, ctor.newInstance(paramValues[i]));
--- a/src/main/java/de/uapcore/lightpit/Constants.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/Constants.java Mon Jun 01 14:46:58 2020 +0200 @@ -92,6 +92,11 @@ public static final String REQ_ATTR_CONTENT_PAGE = fqn(AbstractLightPITServlet.class, "content-page"); /** + * Key for the view model object (the type depends on the rendered site). + */ + public static final String REQ_ATTR_VIEWMODEL = "viewmodel"; + + /** * Key for the name of the additional stylesheet used by a module. */ public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss");
--- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -30,6 +30,7 @@ import de.uapcore.lightpit.entities.Issue; import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; import java.sql.SQLException; import java.util.List; @@ -48,6 +49,15 @@ List<Issue> list(Project project) throws SQLException; /** + * Lists all issues that are somehow related to the specified version. + * + * @param version the version + * @return a list of issues + * @throws SQLException on any kind of SQL error + */ + List<Issue> list(Version version) throws SQLException; + + /** * Saves an instances to the database. * Implementations of this DAO must guarantee that the generated ID is stored in the instance. *
--- a/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -28,6 +28,7 @@ */ package de.uapcore.lightpit.dao; +import de.uapcore.lightpit.entities.IssueSummary; import de.uapcore.lightpit.entities.Project; import java.sql.SQLException; @@ -35,4 +36,6 @@ public interface ProjectDao extends GenericDao<Project> { List<Project> list() throws SQLException; + + IssueSummary getIssueSummary(Project project) throws SQLException; }
--- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -30,7 +30,6 @@ import de.uapcore.lightpit.entities.Project; import de.uapcore.lightpit.entities.Version; -import de.uapcore.lightpit.entities.VersionStatistics; import java.sql.SQLException; import java.util.List; @@ -45,31 +44,4 @@ * @throws SQLException on any kind of SQL error */ List<Version> list(Project project) throws SQLException; - - /** - * Retrieves statistics about issues that arose in a version. - * - * @param version the version - * @return version statistics - * @throws SQLException on any kind of SQL error - */ - VersionStatistics statsOpenedIssues(Version version) throws SQLException; - - /** - * Retrieves statistics about issues that are scheduled for a version. - * - * @param version the version - * @return version statistics - * @throws SQLException on any kind of SQL error - */ - VersionStatistics statsScheduledIssues(Version version) throws SQLException; - - /** - * Retrieves statistics about issues that are resolved in a version. - * - * @param version the version - * @return version statistics - * @throws SQLException on any kind of SQL error - */ - VersionStatistics statsResolvedIssues(Version version) throws SQLException; }
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -43,7 +43,7 @@ public final class PGIssueDao implements IssueDao { - private final PreparedStatement insert, update, list, find; + private final PreparedStatement insert, update, list, listForVersion, find; private final PreparedStatement affectedVersions, scheduledVersions, resolvedVersions; private final PreparedStatement clearAffected, clearScheduled, clearResolved; private final PreparedStatement insertAffected, insertScheduled, insertResolved; @@ -56,7 +56,24 @@ "from lpit_issue i " + "left join lpit_project p on project = projectid " + "left join lpit_user on userid = assignee " + - "where project = ? "); + "where project = ? "+ + "order by eta asc, updated desc"); + + listForVersion = connection.prepareStatement( + "with issue_version as ( "+ + "select issueid, versionid from lpit_issue_affected_version union "+ + "select issueid, versionid from lpit_issue_scheduled_version union "+ + "select issueid, versionid from lpit_issue_resolved_version) "+ + "select issueid, project, p.name as projectname, status, category, subject, i.description, " + + "userid, username, givenname, lastname, mail, " + + "created, updated, eta " + + "from lpit_issue i " + + "join issue_version using (issueid) "+ + "left join lpit_project p on project = projectid " + + "left join lpit_user on userid = assignee " + + "where versionid = ? "+ + "order by eta asc, updated desc" + ); find = connection.prepareStatement( "select issueid, project, p.name as projectname, status, category, subject, i.description, " + @@ -121,7 +138,8 @@ private Issue mapColumns(ResultSet result) throws SQLException { final var project = new Project(result.getInt("project")); project.setName(result.getString("projectname")); - final var issue = new Issue(result.getInt("issueid"), project); + final var issue = new Issue(result.getInt("issueid")); + issue.setProject(project); issue.setStatus(IssueStatus.valueOf(result.getString("status"))); issue.setCategory(IssueCategory.valueOf(result.getString("category"))); issue.setSubject(result.getString("subject")); @@ -133,8 +151,8 @@ return issue; } - private Version mapVersion(ResultSet result, Project project) throws SQLException { - final var version = new Version(result.getInt("versionid"), project); + private Version mapVersion(ResultSet result) throws SQLException { + final var version = new Version(result.getInt("versionid")); version.setName(result.getString("name")); version.setOrdinal(result.getInt("ordinal")); version.setStatus(VersionStatus.valueOf(result.getString("status"))); @@ -203,11 +221,10 @@ } } - @Override - public List<Issue> list(Project project) throws SQLException { - list.setInt(1, project.getId()); + private List<Issue> list(PreparedStatement query, int arg) throws SQLException { + query.setInt(1, arg); List<Issue> issues = new ArrayList<>(); - try (var result = list.executeQuery()) { + try (var result = query.executeQuery()) { while (result.next()) { issues.add(mapColumns(result)); } @@ -216,6 +233,16 @@ } @Override + public List<Issue> list(Project project) throws SQLException { + return list(list, project.getId()); + } + + @Override + public List<Issue> list(Version version) throws SQLException { + return list(listForVersion, version.getId()); + } + + @Override public Issue find(int id) throws SQLException { find.setInt(1, id); try (var result = find.executeQuery()) { @@ -232,7 +259,7 @@ List<Version> versions = new ArrayList<>(); try (var result = stmt.executeQuery()) { while (result.next()) { - versions.add(mapVersion(result, issue.getProject())); + versions.add(mapVersion(result)); } } return versions;
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -29,6 +29,7 @@ package de.uapcore.lightpit.dao.postgres; import de.uapcore.lightpit.dao.ProjectDao; +import de.uapcore.lightpit.entities.IssueSummary; import de.uapcore.lightpit.entities.Project; import de.uapcore.lightpit.entities.User; @@ -98,24 +99,26 @@ return proj; } - private void mapIssueSummary(Project proj) throws SQLException { - issue_summary.setInt(1, proj.getId()); + public IssueSummary getIssueSummary(Project project) throws SQLException { + issue_summary.setInt(1, project.getId()); final var result = issue_summary.executeQuery(); + final var summary = new IssueSummary(); while (result.next()) { final var phase = result.getInt("phase"); final var total = result.getInt("total"); switch(phase) { case 0: - proj.setOpenIssues(total); + summary.setOpen(total); break; case 1: - proj.setActiveIssues(total); + summary.setActive(total); break; case 2: - proj.setDoneIssues(total); + summary.setDone(total); break; } } + return summary; } @Override @@ -146,7 +149,6 @@ try (var result = list.executeQuery()) { while (result.next()) { final var project = mapColumns(result); - mapIssueSummary(project); projects.add(project); } } @@ -159,7 +161,6 @@ try (var result = find.executeQuery()) { if (result.next()) { final var project = mapColumns(result); - mapIssueSummary(project); return project; } else { return null;
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Mon Jun 01 14:46:58 2020 +0200 @@ -29,7 +29,9 @@ package de.uapcore.lightpit.dao.postgres; import de.uapcore.lightpit.dao.VersionDao; -import de.uapcore.lightpit.entities.*; +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; +import de.uapcore.lightpit.entities.VersionStatus; import java.sql.Connection; import java.sql.PreparedStatement; @@ -42,7 +44,6 @@ public final class PGVersionDao implements VersionDao { private final PreparedStatement insert, update, list, find; - private final PreparedStatement issuesAffected, issuesScheduled, issuesResolved; public PGVersionDao(Connection connection) throws SQLException { list = connection.prepareStatement( @@ -64,54 +65,19 @@ update = connection.prepareStatement( "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?" ); - - issuesAffected = connection.prepareStatement( - "select category, status, count(*) as issuecount " + - "from lpit_issue_affected_version " + - "join lpit_issue using (issueid) " + - "where versionid = ? " + - "group by category, status" - ); - issuesScheduled = connection.prepareStatement( - "select category, status, count(*) as issuecount " + - "from lpit_issue_scheduled_version " + - "join lpit_issue using (issueid) " + - "where versionid = ? " + - "group by category, status" - ); - issuesResolved = connection.prepareStatement( - "select category, status, count(*) as issuecount " + - "from lpit_issue_resolved_version " + - "join lpit_issue using (issueid) " + - "where versionid = ? " + - "group by category, status" - ); } private Version mapColumns(ResultSet result) throws SQLException { final var project = new Project(result.getInt("project")); project.setName(result.getString("projectname")); - final var version = new Version(result.getInt("versionid"), project); + final var version = new Version(result.getInt("versionid")); + version.setProject(project); version.setName(result.getString("name")); version.setOrdinal(result.getInt("ordinal")); version.setStatus(VersionStatus.valueOf(result.getString("status"))); return version; } - private VersionStatistics versionStatistics(Version version, PreparedStatement stmt) throws SQLException { - stmt.setInt(1, version.getId()); - final var result = stmt.executeQuery(); - final var stats = new VersionStatistics(version); - while (result.next()) { - stats.setIssueCount( - IssueCategory.valueOf(result.getString("category")), - IssueStatus.valueOf(result.getString("status")), - result.getInt("issuecount") - ); - } - return stats; - } - @Override public void save(Version instance) throws SQLException { Objects.requireNonNull(instance.getName()); @@ -159,19 +125,4 @@ } } } - - @Override - public VersionStatistics statsOpenedIssues(Version version) throws SQLException { - return versionStatistics(version, issuesAffected); - } - - @Override - public VersionStatistics statsScheduledIssues(Version version) throws SQLException { - return versionStatistics(version, issuesScheduled); - } - - @Override - public VersionStatistics statsResolvedIssues(Version version) throws SQLException { - return versionStatistics(version, issuesResolved); - } }
--- a/src/main/java/de/uapcore/lightpit/entities/Issue.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java Mon Jun 01 14:46:58 2020 +0200 @@ -38,7 +38,7 @@ public final class Issue { private int id; - private final Project project; + private Project project; private IssueStatus status; private IssueCategory category; @@ -55,9 +55,8 @@ private Timestamp updated = Timestamp.from(Instant.now()); private Date eta; - public Issue(int id, Project project) { + public Issue(int id) { this.id = id; - this.project = project; } public int getId() { @@ -72,6 +71,10 @@ this.id = id; } + public void setProject(Project project) { + this.project = project; + } + public Project getProject() { return project; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueSummary.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,67 @@ +package de.uapcore.lightpit.entities; + +public class IssueSummary { + private int open = 0; + private int active = 0; + private int done = 0; + + public int getOpen() { + return open; + } + + public void setOpen(int open) { + this.open = open; + } + + public int getActive() { + return active; + } + + public void setActive(int active) { + this.active = active; + } + + public int getDone() { + return done; + } + + public void setDone(int done) { + this.done = done; + } + + public int getTotal() { + return open+active+done; + } + + public int getOpenPercent() { + return 100-getActivePercent()-getDonePercent(); + } + + public int getActivePercent() { + int total = getTotal(); + return total > 0 ? 100*active/total : 0; + } + + public int getDonePercent() { + int total = getTotal(); + return total > 0 ? 100*done/total : 0; + } + + /** + * Adds the specified issue to the summary by increming the respective counter. + * @param issue the issue + */ + public void add(Issue issue) { + switch (issue.getStatus().getPhase()) { + case 0: + open++; + break; + case 1: + active++; + break; + case 2: + done++; + break; + } + } +}
--- a/src/main/java/de/uapcore/lightpit/entities/Project.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Project.java Mon Jun 01 14:46:58 2020 +0200 @@ -38,10 +38,6 @@ private String repoUrl; private User owner; - private int openIssues; - private int activeIssues; - private int doneIssues; - public Project(int id) { this.id = id; } @@ -82,30 +78,6 @@ this.owner = owner; } - public int getOpenIssues() { - return openIssues; - } - - public void setOpenIssues(int openIssues) { - this.openIssues = openIssues; - } - - public int getActiveIssues() { - return activeIssues; - } - - public void setActiveIssues(int activeIssues) { - this.activeIssues = activeIssues; - } - - public int getDoneIssues() { - return doneIssues; - } - - public void setDoneIssues(int doneIssues) { - this.doneIssues = doneIssues; - } - @Override public boolean equals(Object o) { if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/entities/Version.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Version.java Mon Jun 01 14:46:58 2020 +0200 @@ -41,9 +41,8 @@ private int ordinal = 0; private VersionStatus status = VersionStatus.Future; - public Version(int id, Project project) { + public Version(int id) { this.id = id; - this.project = project; } public int getId() {
--- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java Mon Jun 01 14:46:58 2020 +0200 @@ -29,6 +29,7 @@ package de.uapcore.lightpit.modules; import de.uapcore.lightpit.*; +import de.uapcore.lightpit.viewmodel.LanguageView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,9 +87,12 @@ @RequestMapping(method = HttpMethod.GET) public ResponseType handle(HttpServletRequest req) { - req.setAttribute("languages", languages); - req.setAttribute("browserLanguage", req.getLocale()); + final var viewModel = new LanguageView(); + viewModel.setLanguages(languages); + viewModel.setBrowserLanguage(req.getLocale()); + viewModel.setCurrentLanguage((Locale)req.getSession().getAttribute(Constants.SESSION_ATTR_LANGUAGE)); + setViewModel(req, viewModel); setStylesheet(req, "language"); setContentPage(req, "language"); return ResponseType.HTML;
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Mon Jun 01 14:46:58 2020 +0200 @@ -32,6 +32,7 @@ import de.uapcore.lightpit.*; import de.uapcore.lightpit.dao.DataAccessObjects; import de.uapcore.lightpit.entities.*; +import de.uapcore.lightpit.viewmodel.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +43,10 @@ import java.io.IOException; import java.sql.Date; import java.sql.SQLException; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -59,41 +63,81 @@ public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project"); public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue"); public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version"); - public static final String SESSION_ATTR_HIDE_ZEROS = fqn(ProjectsModule.class, "stats_hide_zeros"); private class SessionSelection { final HttpSession session; + final HttpServletRequest req; + final DataAccessObjects dao; Project project; Version version; Issue issue; - SessionSelection(HttpServletRequest req, Project project) { - this.session = req.getSession(); - this.project = project; + SessionSelection(HttpServletRequest req, DataAccessObjects dao) { + this.req = req; + this.dao = dao; + session = req.getSession(); + } + + void newProject() { + project = null; version = null; issue = null; updateAttributes(); + project = new Project(-1); + updateAttributes(); + } + + void newVersion() throws SQLException { + project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + syncProject(); + version = null; + issue = null; + updateAttributes(); + version = new Version(-1); + version.setProject(project); + updateAttributes(); } - SessionSelection(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - this.session = req.getSession(); - final var issueDao = dao.getIssueDao(); - final var projectDao = dao.getProjectDao(); - final var issueSelection = getParameter(req, Integer.class, "issue"); - if (issueSelection.isPresent()) { - issue = issueDao.find(issueSelection.get()); - } else { - final var issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE); - this.issue = issue == null ? null : issueDao.find(issue.getId()); + void newIssue() throws SQLException { + project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + syncProject(); + version = null; + issue = null; + updateAttributes(); + issue = new Issue(-1); + issue.setProject(project); + updateAttributes(); + } + + void selectVersion(Version selectedVersion) throws SQLException { + issue = null; + version = selectedVersion; + if (!version.getProject().equals(project)) { + project = dao.getProjectDao().find(version.getProject().getId()); } - if (issue != null) { - version = null; // show the issue globally - project = projectDao.find(issue.getProject().getId()); + // our object contains more details + version.setProject(project); + updateAttributes(); + } + + void selectIssue(Issue selectedIssue) throws SQLException { + issue = selectedIssue; + if (!issue.getProject().equals(project)) { + project = dao.getProjectDao().find(issue.getProject().getId()); } + // our object contains more details + issue.setProject(project); + if (!issue.getResolvedVersions().contains(version) && !issue.getScheduledVersions().contains(version) + && !issue.getAffectedVersions().contains(version)) { + version = null; + } + updateAttributes(); + } + void syncProject() throws SQLException { final var projectSelection = getParameter(req, Integer.class, "pid"); if (projectSelection.isPresent()) { - final var selectedProject = projectDao.find(projectSelection.get()); + final var selectedProject = dao.getProjectDao().find(projectSelection.get()); if (!Objects.equals(selectedProject, project)) { // reset version and issue if project changed version = null; @@ -101,51 +145,57 @@ } project = selectedProject; } else { - final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); - project = sessionProject == null ? null : projectDao.find(sessionProject.getId()); + project = project == null ? null : dao.getProjectDao().find(project.getId()); + } + } + + void syncVersion() throws SQLException { + final var versionSelection = getParameter(req, Integer.class, "vid"); + if (versionSelection.isPresent()) { + if (versionSelection.get() < 0) { + version = null; + } else { + final var selectedVersion = dao.getVersionDao().find(versionSelection.get()); + if (!Objects.equals(selectedVersion, version)) { + issue = null; + } + selectVersion(selectedVersion); + } + } else { + version = version == null ? null : dao.getVersionDao().find(version.getId()); } + } + + void syncIssue() throws SQLException { + final var issueSelection = getParameter(req, Integer.class, "issue"); + if (issueSelection.isPresent()) { + final var selectedIssue = dao.getIssueDao().find(issueSelection.get()); + dao.getIssueDao().joinVersionInformation(selectedIssue); + selectIssue(selectedIssue); + } else { + issue = issue == null ? null : dao.getIssueDao().find(issue.getId()); + } + } + + void sync() throws SQLException { + project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION); + issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE); + + syncProject(); + syncVersion(); + syncIssue(); + updateAttributes(); } - void selectVersion(Version version) { - this.project = version.getProject(); - this.version = version; - this.issue = null; - updateAttributes(); - } - - void selectIssue(Issue issue) { - this.project = issue.getProject(); - this.issue = issue; - this.version = null; - updateAttributes(); - } - - void updateAttributes() { + private void updateAttributes() { session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project); session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version); session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue); } } - private void setAttributeHideZeros(HttpServletRequest req) { - final Boolean value; - final var param = getParameter(req, Boolean.class, "reduced"); - if (param.isPresent()) { - value = param.get(); - req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value); - } else { - final var sessionValue = req.getSession().getAttribute(SESSION_ATTR_HIDE_ZEROS); - if (sessionValue != null) { - value = (Boolean) sessionValue; - } else { - value = false; - req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value); - } - } - req.setAttribute("statsHideZeros", value); - } - @Override protected String getResourceBundleName() { return "localization.projects"; @@ -162,10 +212,10 @@ * Creates the breadcrumb menu. * * @param level the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue) - * @param sessionSelection the currently selected objects + * @param selection the currently selected objects * @return a dynamic breadcrumb menu trying to display as many levels as possible */ - private List<MenuEntry> getBreadcrumbs(int level, SessionSelection sessionSelection) { + private List<MenuEntry> getBreadcrumbs(int level, SessionSelection selection) { MenuEntry entry; final var breadcrumbs = new ArrayList<MenuEntry>(); @@ -174,47 +224,49 @@ breadcrumbs.add(entry); if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true); - if (sessionSelection.project != null) { - if (sessionSelection.project.getId() < 0) { + if (selection.project != null) { + if (selection.project.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"), "projects/edit"); } else { - entry = new MenuEntry(sessionSelection.project.getName(), - "projects/view?pid=" + sessionSelection.project.getId()); + entry = new MenuEntry(selection.project.getName(), + "projects/view?pid=" + selection.project.getId()); } if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true); breadcrumbs.add(entry); } - if (sessionSelection.version != null) { - if (sessionSelection.version.getId() < 0) { + if (selection.version != null) { + if (selection.version.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"), "projects/versions/edit"); } else { - entry = new MenuEntry(sessionSelection.version.getName(), - // TODO: change link to issue overview for that version - "projects/versions/edit?id=" + sessionSelection.version.getId()); + entry = new MenuEntry(selection.version.getName(), + "projects/versions/view?vid=" + selection.version.getId()); } if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true); breadcrumbs.add(entry); } - if (sessionSelection.project != null) { + if (selection.project != null) { + String path = "projects/issues/?pid=" + selection.project.getId(); + if (selection.version != null) { + path += "&vid="+selection.version.getId(); + } entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"), - // TODO: maybe also add selected version - "projects/issues/?pid=" + sessionSelection.project.getId()); + path); if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true); breadcrumbs.add(entry); } - if (sessionSelection.issue != null) { - if (sessionSelection.issue.getId() < 0) { + if (selection.issue != null) { + if (selection.issue.getId() < 0) { entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"), "projects/issues/edit"); } else { - entry = new MenuEntry("#" + sessionSelection.issue.getId(), + entry = new MenuEntry("#" + selection.issue.getId(), // TODO: maybe change link to a view rather than directly opening the editor - "projects/issues/edit?id=" + sessionSelection.issue.getId()); + "projects/issues/edit?issue=" + selection.issue.getId()); } if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true); breadcrumbs.add(entry); @@ -226,8 +278,22 @@ @RequestMapping(method = HttpMethod.GET) public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException { final var sessionSelection = new SessionSelection(req, dao); - final var projectList = dao.getProjectDao().list(); - req.setAttribute("projects", projectList); + sessionSelection.sync(); + + final var projectDao = dao.getProjectDao(); + final var versionDao = dao.getVersionDao(); + + final var projectList = projectDao.list(); + + final var viewModel = new ProjectIndexView(); + for (var project : projectList) { + final var info = new ProjectInfo(project); + info.setVersions(versionDao.list(project)); + info.setIssueSummary(projectDao.getIssueSummary(project)); + viewModel.getProjects().add(info); + } + + setViewModel(req, viewModel); setContentPage(req, "projects"); setStylesheet(req, "projects"); @@ -236,17 +302,24 @@ return ResponseType.HTML; } - private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { - req.setAttribute("project", selection.project); - req.setAttribute("users", dao.getUserDao().list()); + private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + final var viewModel = new ProjectEditView(); + viewModel.setProject(selection.project); + viewModel.setUsers(dao.getUserDao().list()); + setViewModel(req, viewModel); setContentPage(req, "project-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection)); + return viewModel; } @RequestMapping(requestPath = "edit", method = HttpMethod.GET) public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - final var selection = new SessionSelection(req, findByParameter(req, Integer.class, "id", - dao.getProjectDao()::find).orElse(new Project(-1))); + final var selection = new SessionSelection(req, dao); + if (getParameter(req, Integer.class, "pid").isEmpty()) { + selection.newProject(); + } else { + selection.sync(); + } configureEditForm(req, dao, selection); @@ -272,10 +345,12 @@ setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); LOG.debug("Successfully updated project {}", project.getName()); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { - // TODO: set request attribute with error text LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); - configureEditForm(req, dao, new SessionSelection(req, project)); + final var selection = new SessionSelection(req, dao); + selection.project = project; + final var vm = configureEditForm(req, dao, selection); + vm.setErrorText(ex.getMessage()); // TODO: error text } return ResponseType.HTML; @@ -283,127 +358,143 @@ @RequestMapping(requestPath = "view", method = HttpMethod.GET) public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { - final var sessionSelection = new SessionSelection(req, dao); - if (sessionSelection.project == null) { + final var selection = new SessionSelection(req, dao); + selection.sync(); + + if (selection.project == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); return ResponseType.NONE; } final var versionDao = dao.getVersionDao(); - final var versions = versionDao.list(sessionSelection.project); - final var statsAffected = new ArrayList<VersionStatistics>(); - final var statsScheduled = new ArrayList<VersionStatistics>(); - final var statsResolved = new ArrayList<VersionStatistics>(); - for (Version version : versions) { - statsAffected.add(versionDao.statsOpenedIssues(version)); - statsScheduled.add(versionDao.statsScheduledIssues(version)); - statsResolved.add(versionDao.statsResolvedIssues(version)); - } + final var issueDao = dao.getIssueDao(); - setAttributeHideZeros(req); + final var viewModel = new ProjectView(selection.project); + final var issues = issueDao.list(selection.project); + for (var issue : issues) issueDao.joinVersionInformation(issue); + viewModel.setIssues(issues); + viewModel.setVersions(versionDao.list(selection.project)); + viewModel.updateVersionInfo(); + setViewModel(req, viewModel); - req.setAttribute("project", sessionSelection.project); - req.setAttribute("versions", versions); - req.setAttribute("statsAffected", statsAffected); - req.setAttribute("statsScheduled", statsScheduled); - req.setAttribute("statsResolved", statsResolved); - - req.setAttribute("issueStatusEnum", IssueStatus.values()); - req.setAttribute("issueCategoryEnum", IssueCategory.values()); - - setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, sessionSelection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection)); setContentPage(req, "project-details"); setStylesheet(req, "projects"); return ResponseType.HTML; } - private void configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { - final var versionDao = dao.getVersionDao(); - req.setAttribute("projects", dao.getProjectDao().list()); - req.setAttribute("version", selection.version); - req.setAttribute("versionStatusEnum", VersionStatus.values()); + @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET) + public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { + final var selection = new SessionSelection(req, dao); + selection.sync(); + if (selection.version == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return ResponseType.NONE; + } - req.setAttribute("issueStatusEnum", IssueStatus.values()); - req.setAttribute("issueCategoryEnum", IssueCategory.values()); - req.setAttribute("statsAffected", versionDao.statsOpenedIssues(selection.version)); - req.setAttribute("statsScheduled", versionDao.statsScheduledIssues(selection.version)); - req.setAttribute("statsResolved", versionDao.statsResolvedIssues(selection.version)); - setAttributeHideZeros(req); + final var issueDao = dao.getIssueDao(); + final var viewModel = new VersionView(selection.version); + final var issues = issueDao.list(selection.version); + for (var issue : issues) issueDao.joinVersionInformation(issue); + viewModel.setIssues(issues); + setViewModel(req, viewModel); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection)); + setContentPage(req, "version"); + setStylesheet(req, "projects"); + + return ResponseType.HTML; + } + + private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + final var viewModel = new VersionEditView(selection.version); + if (selection.version.getProject() == null) { + viewModel.setProjects(dao.getProjectDao().list()); + } + setViewModel(req, viewModel); setContentPage(req, "version-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection)); + return viewModel; } @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET) - public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { - final var sessionSelection = new SessionSelection(req, dao); + public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException { + final var selection = new SessionSelection(req, dao); + if (getParameter(req, Integer.class, "vid").isEmpty()) { + selection.newVersion(); + } else { + selection.sync(); + } - sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find) - .orElse(new Version(-1, sessionSelection.project))); - configureEditVersionForm(req, dao, sessionSelection); + configureEditVersionForm(req, dao, selection); return ResponseType.HTML; } @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST) public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { - final var sessionSelection = new SessionSelection(req, dao); - var version = new Version(-1, sessionSelection.project); + var version = new Version(-1); try { - version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project); + version = new Version(getParameter(req, Integer.class, "id").orElseThrow()); + version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow())); version.setName(getParameter(req, String.class, "name").orElseThrow()); getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal); version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow())); dao.getVersionDao().saveOrUpdate(version); // specifying the pid parameter will purposely reset the session selected version! - setRedirectLocation(req, "./projects/view?pid="+sessionSelection.project.getId()); + setRedirectLocation(req, "./projects/view?pid="+version.getProject().getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - LOG.debug("Successfully updated version {} for project {}", version.getName(), sessionSelection.project.getName()); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { - // TODO: set request attribute with error text LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); - sessionSelection.selectVersion(version); - configureEditVersionForm(req, dao, sessionSelection); + final var selection = new SessionSelection(req, dao); + selection.selectVersion(version); + final var viewModel = configureEditVersionForm(req, dao, selection); + // TODO: set Error Text } return ResponseType.HTML; } - private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + final var viewModel = new IssueEditView(selection.issue); - if (selection.issue.getProject() == null || selection.issue.getProject().getId() < 0) { - req.setAttribute("projects", dao.getProjectDao().list()); - req.setAttribute("versions", Collections.<Version>emptyList()); + if (selection.issue.getProject() == null) { + viewModel.setProjects(dao.getProjectDao().list()); } else { - req.setAttribute("projects", Collections.<Project>emptyList()); - req.setAttribute("versions", dao.getVersionDao().list(selection.issue.getProject())); + viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject())); } - - dao.getIssueDao().joinVersionInformation(selection.issue); - req.setAttribute("issue", selection.issue); - req.setAttribute("issueStatusEnum", IssueStatus.values()); - req.setAttribute("issueCategoryEnum", IssueCategory.values()); - req.setAttribute("users", dao.getUserDao().list()); + viewModel.setUsers(dao.getUserDao().list()); + setViewModel(req, viewModel); setContentPage(req, "issue-form"); setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection)); + return viewModel; } @RequestMapping(requestPath = "issues/", method = HttpMethod.GET) public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { - final var sessionSelection = new SessionSelection(req, dao); - if (sessionSelection.project == null) { + final var selection = new SessionSelection(req, dao); + selection.sync(); + if (selection.project == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected."); return ResponseType.NONE; } - req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project)); + final var viewModel = new IssuesView(); + viewModel.setProject(selection.project); + if (selection.version == null) { + viewModel.setIssues(dao.getIssueDao().list(selection.project)); + } else { + viewModel.setVersion(selection.version); + viewModel.setIssues(dao.getIssueDao().list(selection.version)); + } + setViewModel(req, viewModel); - setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection)); + setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, selection)); setContentPage(req, "issues"); setStylesheet(req, "projects"); @@ -412,22 +503,25 @@ @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { - final var sessionSelection = new SessionSelection(req, dao); + final var selection = new SessionSelection(req, dao); + if (getParameter(req, Integer.class, "issue").isEmpty()) { + selection.newIssue(); + } else { + selection.sync(); + } - sessionSelection.selectIssue(findByParameter(req, Integer.class, "id", - dao.getIssueDao()::find).orElse(new Issue(-1, sessionSelection.project))); - configureEditIssueForm(req, dao, sessionSelection); + configureEditIssueForm(req, dao, selection); return ResponseType.HTML; } @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST) public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { - final var sessionSelection = new SessionSelection(req, dao); - Issue issue = new Issue(-1, sessionSelection.project); + Issue issue = new Issue(-1); try { - issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project); + issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow()); + issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow())); getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory); getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus); issue.setSubject(getParameter(req, String.class, "subject").orElseThrow()); @@ -440,17 +534,17 @@ getParameter(req, Integer[].class, "affected") .map(Stream::of) .map(stream -> - stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList()) + stream.map(Version::new).collect(Collectors.toList()) ).ifPresent(issue::setAffectedVersions); getParameter(req, Integer[].class, "scheduled") .map(Stream::of) .map(stream -> - stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList()) + stream.map(Version::new).collect(Collectors.toList()) ).ifPresent(issue::setScheduledVersions); getParameter(req, Integer[].class, "resolved") .map(Stream::of) .map(stream -> - stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList()) + stream.map(Version::new).collect(Collectors.toList()) ).ifPresent(issue::setResolvedVersions); dao.getIssueDao().saveOrUpdate(issue); @@ -458,13 +552,14 @@ // specifying the issue parameter keeps the edited issue as breadcrumb setRedirectLocation(req, "./projects/issues/?issue="+issue.getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - LOG.debug("Successfully updated issue {} for project {}", issue.getId(), sessionSelection.project.getName()); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { // TODO: set request attribute with error text LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex); - sessionSelection.selectIssue(issue); - configureEditIssueForm(req, dao, sessionSelection); + final var selection = new SessionSelection(req, dao); + selection.selectIssue(issue); + final var viewModel = configureEditIssueForm(req, dao, selection); + // TODO: set Error Text } return ResponseType.HTML;
--- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Sat May 30 18:12:38 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Mon Jun 01 14:46:58 2020 +0200 @@ -32,6 +32,8 @@ import de.uapcore.lightpit.*; import de.uapcore.lightpit.dao.DataAccessObjects; import de.uapcore.lightpit.entities.User; +import de.uapcore.lightpit.viewmodel.UsersEditView; +import de.uapcore.lightpit.viewmodel.UsersView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +59,9 @@ public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException { final var userDao = dao.getUserDao(); - req.setAttribute("users", userDao.list()); + final var viewModel = new UsersView(); + viewModel.setUsers(userDao.list()); + setViewModel(req, viewModel); setContentPage(req, "users"); return ResponseType.HTML; @@ -66,9 +70,11 @@ @RequestMapping(requestPath = "edit", method = HttpMethod.GET) public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - req.setAttribute("user", findByParameter(req, Integer.class, "id", + final var viewModel = new UsersEditView(); + viewModel.setUser(findByParameter(req, Integer.class, "id", dao.getUserDao()::find).orElse(new User(-1))); + setViewModel(req, viewModel); setContentPage(req, "user-form"); return ResponseType.HTML; @@ -92,8 +98,10 @@ LOG.debug("Successfully updated user {}", user.getUsername()); } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { - // TODO: set request attribute with error text - req.setAttribute("user", user); + final var viewModel = new UsersEditView(); + viewModel.setUser(user); + // TODO: viewModel.setErrorText() + setViewModel(req, viewModel); setContentPage(req, "user-form"); LOG.warn("Form validation failure: {}", ex.getMessage()); LOG.debug("Details:", ex);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,54 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.*; + +import java.util.Collections; +import java.util.List; + +public class IssueEditView { + private final Issue issue; + + private List<Project> projects = Collections.emptyList(); + private List<Version> versions = Collections.emptyList(); + private List<User> users; + + public IssueEditView(Issue issue) { + this.issue = issue; + } + + public Issue getIssue() { + return issue; + } + + public List<Project> getProjects() { + return projects; + } + + public void setProjects(List<Project> projects) { + this.projects = projects; + } + + public List<Version> getVersions() { + return versions; + } + + public void setVersions(List<Version> versions) { + this.versions = versions; + } + + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } + + public IssueStatus[] getIssueStatus() { + return IssueStatus.values(); + } + + public IssueCategory[] getIssueCategory() { + return IssueCategory.values(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,37 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Issue; +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; + +import java.util.List; + +public class IssuesView { + private List<Issue> issues; + private Project project; + private Version version; + + public List<Issue> getIssues() { + return issues; + } + + public void setIssues(List<Issue> issues) { + this.issues = issues; + } + + public Version getVersion() { + return version; + } + + public void setVersion(Version version) { + this.version = version; + } + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,36 @@ +package de.uapcore.lightpit.viewmodel; + +import java.util.List; +import java.util.Locale; + +public class LanguageView { + + private List<Locale> languages; + private Locale browserLanguage; + private Locale currentLanguage; + + + public List<Locale> getLanguages() { + return languages; + } + + public void setLanguages(List<Locale> languages) { + this.languages = languages; + } + + public Locale getBrowserLanguage() { + return browserLanguage; + } + + public void setBrowserLanguage(Locale browserLanguage) { + this.browserLanguage = browserLanguage; + } + + public Locale getCurrentLanguage() { + return currentLanguage; + } + + public void setCurrentLanguage(Locale currentLanguage) { + this.currentLanguage = currentLanguage; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,37 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.User; + +import java.util.List; + +public class ProjectEditView { + + private Project project; + private List<User> users; + private String errorText; + + public Project getProject() { + return project; + } + + public void setProject(Project project) { + this.project = project; + } + + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } + + public String getErrorText() { + return errorText; + } + + public void setErrorText(String errorText) { + this.errorText = errorText; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,17 @@ +package de.uapcore.lightpit.viewmodel; + +import java.util.ArrayList; +import java.util.List; + +public class ProjectIndexView { + + private List<ProjectInfo> projects = new ArrayList<>(); + + public List<ProjectInfo> getProjects() { + return projects; + } + + public void setProjects(List<ProjectInfo> projects) { + this.projects = projects; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,58 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.IssueSummary; +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; +import de.uapcore.lightpit.entities.VersionStatus; + +import java.util.Collections; +import java.util.List; + +public class ProjectInfo { + + private final Project project; + private List<Version> versions = Collections.emptyList(); + private IssueSummary issueSummary = new IssueSummary(); + + public ProjectInfo(Project project) { + this.project = project; + } + + public Project getProject() { + return project; + } + + public List<Version> getVersions() { + return versions; + } + + public void setVersions(List<Version> versions) { + this.versions = versions; + } + + public Version getLatestVersion() { + for (var v : versions) { + if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal()) + return v; + } + return null; + } + + public Version getNextVersion() { + Version next = null; + for (var v : versions) { + if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal()) + break; + next = v; + } + return next; + } + + public IssueSummary getIssueSummary() { + return issueSummary; + } + + public void setIssueSummary(IssueSummary issueSummary) { + this.issueSummary = issueSummary; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,81 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Issue; +import de.uapcore.lightpit.entities.IssueSummary; +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ProjectView { + + private final Project project; + private List<Version> versions = Collections.emptyList(); + private List<Issue> issues = Collections.emptyList(); + + private IssueSummary issuesTotal; + private List<Issue> issuesWithoutVersion; + private IssueSummary issuesWithoutVersionTotal; + private List<VersionInfo> versionInfos = Collections.emptyList(); + + public ProjectView(Project project) { + this.project = project; + } + + public Project getProject() { + return project; + } + + public List<Issue> getIssues() { + return issues; + } + + public void setIssues(List<Issue> issues) { + this.issues = issues; + issuesTotal = new IssueSummary(); + issuesWithoutVersion = new ArrayList<>(); + issuesWithoutVersionTotal = new IssueSummary(); + for (Issue issue : issues) { + issuesTotal.add(issue); + if (issue.getResolvedVersions().isEmpty() && issue.getScheduledVersions().isEmpty() && issue.getResolvedVersions().isEmpty()) { + issuesWithoutVersion.add(issue); + issuesWithoutVersionTotal.add(issue); + } + } + } + + public List<Version> getVersions() { + return versions; + } + + public void setVersions(List<Version> versions) { + this.versions = versions; + } + + public void updateVersionInfo() { + versionInfos = new ArrayList<>(); + for (Version version : versions) { + final var info = new VersionInfo(version); + info.collectIssues(issues); + versionInfos.add(info); + } + } + + public IssueSummary getIssuesTotal() { + return issuesTotal; + } + + public List<Issue> getIssuesWithoutVersion() { + return issuesWithoutVersion; + } + + public IssueSummary getIssuesWithoutVersionTotal() { + return issuesWithoutVersionTotal; + } + + public List<VersionInfo> getVersionInfos() { + return versionInfos; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,24 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.User; + +public class UsersEditView { + private User user; + private String errorText; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getErrorText() { + return errorText; + } + + public void setErrorText(String errorText) { + this.errorText = errorText; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,17 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.User; + +import java.util.List; + +public class UsersView { + private List<User> users; + + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,42 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Project; +import de.uapcore.lightpit.entities.Version; +import de.uapcore.lightpit.entities.VersionStatus; + +import java.util.Collections; +import java.util.List; + +public class VersionEditView { + private final Version version; + private List<Project> projects = Collections.emptyList(); + private String errorText; + + public VersionEditView(Version version) { + this.version = version; + } + + public Version getVersion() { + return version; + } + + public List<Project> getProjects() { + return projects; + } + + public void setProjects(List<Project> projects) { + this.projects = projects; + } + + public VersionStatus[] getVersionStatus() { + return VersionStatus.values(); + } + + public String getErrorText() { + return errorText; + } + + public void setErrorText(String errorText) { + this.errorText = errorText; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,82 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Issue; +import de.uapcore.lightpit.entities.IssueSummary; +import de.uapcore.lightpit.entities.Version; + +import java.util.ArrayList; +import java.util.List; + +public class VersionInfo { + + private final Version version; + + private final IssueSummary reportedTotal = new IssueSummary(); + private final IssueSummary scheduledTotal = new IssueSummary(); + private final IssueSummary resolvedTotal = new IssueSummary(); + + private final List<Issue> reported = new ArrayList<>(); + private final List<Issue> scheduled = new ArrayList<>(); + private final List<Issue> resolved = new ArrayList<>(); + + public VersionInfo(Version version) { + this.version = version; + } + + public Version getVersion() { + return version; + } + + public void addReported(Issue issue) { + reportedTotal.add(issue); + reported.add(issue); + } + + public void addScheduled(Issue issue) { + scheduledTotal.add(issue); + scheduled.add(issue); + } + + public void addResolved(Issue issue) { + resolvedTotal.add(issue); + resolved.add(issue); + } + + public IssueSummary getReportedTotal() { + return reportedTotal; + } + + public IssueSummary getScheduledTotal() { + return scheduledTotal; + } + + public IssueSummary getResolvedTotal() { + return resolvedTotal; + } + + public List<Issue> getReported() { + return reported; + } + + public List<Issue> getScheduled() { + return scheduled; + } + + public List<Issue> getResolved() { + return resolved; + } + + public void collectIssues(List<Issue> issues) { + for (Issue issue : issues) { + if (issue.getAffectedVersions().contains(version)) { + addReported(issue); + } + if (issue.getScheduledVersions().contains(version)) { + addScheduled(issue); + } + if (issue.getResolvedVersions().contains(version)) { + addResolved(issue); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionView.java Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,23 @@ +package de.uapcore.lightpit.viewmodel; + +import de.uapcore.lightpit.entities.Issue; +import de.uapcore.lightpit.entities.Version; + +import java.util.List; + +public class VersionView { + + private final VersionInfo versionInfo; + + public VersionView(Version version) { + this.versionInfo = new VersionInfo(version); + } + + public VersionInfo getVersionInfo() { + return versionInfo; + } + + public void setIssues(List<Issue> issues) { + versionInfo.collectIssues(issues); + } +}
--- a/src/main/resources/localization/projects.properties Sat May 30 18:12:38 2020 +0200 +++ b/src/main/resources/localization/projects.properties Mon Jun 01 14:46:58 2020 +0200 @@ -26,7 +26,7 @@ button.create=New Project button.version.create=New Version button.issue.create=New Issue -button.issue.list=Show Issues +button.issue.all=All Issues button.stats.hidezeros=Reduced View button.stats.showzeros=Full View @@ -39,9 +39,18 @@ description=Description repoUrl=Repository owner=Project Lead +version.latest=Latest Version +version.next=Next Version + +progress=Overall Progress + issues.open=Open issues.active=In Progress issues.done=Done +issues.total=Total +issues.reported=Reported Issues +issues.scheduled=Scheduled Issues +issues.resolved=Resolved Issues version.project=Project version.name=Version @@ -59,11 +68,8 @@ version.status.LTS=LTS version.status.Deprecated=Deprecated -version.statistics.affected=Affected by Issues -version.statistics.scheduled=Scheduled Issues -version.statistics.resolved=Resolved Issues -version.statistics.total=Total +issue.without-version=Issues w/o Assigned Version issue.project=Project issue.subject=Subject issue.description=Description
--- a/src/main/resources/localization/projects_de.properties Sat May 30 18:12:38 2020 +0200 +++ b/src/main/resources/localization/projects_de.properties Mon Jun 01 14:46:58 2020 +0200 @@ -26,7 +26,7 @@ button.create=Neues Projekt button.version.create=Neue Version button.issue.create=Neuer Vorgang -button.issue.list=Vorg\u00e4nge +button.issue.all=Alle Vorg\u00e4nge button.stats.hidezeros=Reduzierte Ansicht button.stats.showzeros=Komplettansicht @@ -39,9 +39,18 @@ description=Beschreibung repoUrl=Repository owner=Projektleitung +version.latest=Neuste Version +version.next=N\u00e4chste Version + +progress=Gesamtfortschritt + issues.open=Offen issues.active=In Arbeit issues.done=Erledigt +issues.reported=Er\u00f6ffnete Vorg\u00e4nge +issues.scheduled=Geplante Vorg\u00e4nge +issues.resolved=Gel\u00f6ste Vorg\u00e4nge +issues.total=Summe version.project=Projekt version.name=Version @@ -59,11 +68,7 @@ version.status.LTS=Langzeitsupport version.status.Deprecated=Veraltet -version.statistics.affected=Betroffen von Vorg\u00e4ngen -version.statistics.scheduled=Geplante Vorg\u00e4nge -version.statistics.resolved=Gel\u00f6ste Vorg\u00e4nge -version.statistics.total=Summe - +issue.without-version=Vorg\u00e4nge ohne Version issue.project=Projekt issue.subject=Thema issue.description=Beschreibung
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,12 +28,9 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" /> -<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request" /> -<jsp:useBean id="issue" type="de.uapcore.lightpit.entities.Issue" scope="request"/> -<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/> -<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/> -<jsp:useBean id="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueEditView" scope="request"/> +<c:set var="issue" scope="page" value="${viewmodel.issue}" /> +<c:set var="versions" value="${viewmodel.versions}" /> <form action="./projects/issues/commit" method="post"> <table class="formtable"> @@ -45,26 +42,28 @@ <tr> <th><fmt:message key="issue.project"/></th> <td> - <c:if test="${issue.project.id ge 0}"> - <c:out value="${issue.project.name}" /> - <input type="hidden" name="pid" value="${issue.project.id}" /> - </c:if> - <c:if test="${empty issue.project or issue.project.id lt 0}"> - <select name="pid" required> - <c:forEach var="project" items="${projects}"> - <option value="${project.id}"> - <c:out value="${project.name}" /> - </option> - </c:forEach> - </select> - </c:if> + <c:choose> + <c:when test="${not empty issue.project}"> + <c:out value="${issue.project.name}" /> + <input type="hidden" name="pid" value="${issue.project.id}" /> + </c:when> + <c:otherwise> + <select name="pid" required> + <c:forEach var="project" items="${viewmodel.projects}"> + <option value="${project.id}"> + <c:out value="${project.name}" /> + </option> + </c:forEach> + </select> + </c:otherwise> + </c:choose> </td> </tr> <tr> <th><fmt:message key="issue.category"/></th> <td> <select name="category"> - <c:forEach var="category" items="${issueCategoryEnum}"> + <c:forEach var="category" items="${viewmodel.issueCategory}"> <option <c:if test="${category eq issue.category}">selected</c:if> value="${category}"> @@ -78,7 +77,7 @@ <th><fmt:message key="issue.status"/></th> <td> <select name="status"> - <c:forEach var="status" items="${issueStatusEnum}"> + <c:forEach var="status" items="${viewmodel.issueStatus}"> <option <c:if test="${status eq issue.status}">selected</c:if> value="${status}"> @@ -95,7 +94,7 @@ <tr> <th class="vtop"><fmt:message key="issue.description"/></th> <td> - <textarea name="description"><c:out value="${issue.description}"/></textarea> + <textarea name="description" rows="10"><c:out value="${issue.description}"/></textarea> </td> </tr> <tr> @@ -103,7 +102,7 @@ <td> <select name="assignee"> <option value="-1"><fmt:message key="placeholder.null-assignee"/></option> - <c:forEach var="user" items="${users}"> + <c:forEach var="user" items="${viewmodel.users}"> <option <c:if test="${not empty issue.assignee and user eq issue.assignee}">selected</c:if> value="${user.id}"><c:out value="${user.displayname}"/></option> @@ -155,7 +154,7 @@ <td colspan="2"> <input type="hidden" name="id" value="${issue.id}"/> <c:choose> - <c:when test="${not empty issue.project and issue.project.id ge 0}"> + <c:when test="${not empty issue.project}"> <c:set var="cancelUrl">./projects/issues/?pid=${issue.project.id}</c:set> </c:when> <c:otherwise>
--- a/src/main/webapp/WEB-INF/jsp/issues.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/issues.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,60 +28,26 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssuesView" scope="request"/> +<c:set var="project" scope="page" value="${viewmodel.project}"/> +<c:set var="version" scope="page" value="${viewmodel.version}"/> +<%@include file="../jspf/project-header.jsp"%> + +<c:if test="${not empty version}"> + <h2> + <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/> + <a href="./projects/versions/edit?vid=${version.id}">✎</a> + </h2> +</c:if> <div id="tool-area"> <div> <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a> + <c:if test="${not empty version}"> + <a href="./projects/issues/?pid=${project.id}&vid=-1" class="button"><fmt:message key="button.issue.all"/></a> + </c:if> </div> </div> -<table id="issue-list" class="datatable medskip"> - <thead> - <tr> - <th><fmt:message key="issue.subject"/></th> - <th><fmt:message key="issue.assignee"/></th> - <th><fmt:message key="issue.category"/></th> - <th><fmt:message key="issue.status"/></th> - <th><fmt:message key="issue.created"/></th> - <th><fmt:message key="issue.updated"/></th> - <th><fmt:message key="issue.eta"/></th> - </tr> - </thead> - <tbody> - <c:forEach var="issue" items="${issues}"> - <tr> - <td> - <span class="phase-${issue.status.phase}"> - <a href="./projects/issues/edit?id=${issue.id}"> - <c:out value="${issue.subject}" /> - </a> - </span> - </td> - <td> - <c:if test="${not empty issue.assignee}"> - <c:out value="${issue.assignee.shortDisplayname}" /> - </c:if> - <c:if test="${empty issue.assignee}"> - <fmt:message key="placeholder.null-assignee" /> - </c:if> - </td> - <td> - <fmt:message key="issue.category.${issue.category}" /> - </td> - <td> - <fmt:message key="issue.status.${issue.status}" /> - </td> - <td> - <fmt:formatDate value="${issue.created}" type="BOTH"/> - </td> - <td> - <fmt:formatDate value="${issue.updated}" type="BOTH"/> - </td> - <td> - <fmt:formatDate value="${issue.eta}" /> - </td> - </tr> - </c:forEach> - </tbody> -</table> +<c:set var="issues" value="${viewmodel.issues}"/> +<%@include file="../jspf/issue-list.jsp"%> \ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/language.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/language.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -25,23 +25,19 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --%> <%@page pageEncoding="UTF-8" %> -<%@page import="de.uapcore.lightpit.Constants" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="languages" type="java.util.List<java.util.Locale>" scope="request"/> -<jsp:useBean id="browserLanguage" type="java.util.Locale" scope="request"/> - -<c:set scope="page" var="currentLanguage" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.LanguageView" scope="request"/> <form method="POST" id="lang-selector"> - <c:forEach items="${languages}" var="l"> + <c:forEach items="${viewmodel.languages}" var="l"> <label> <input type="radio" name="language" value="${l.language}" - <c:if test="${l.language eq currentLanguage.language}">checked</c:if>/> + <c:if test="${l.language eq viewmodel.currentLanguage.language}">checked</c:if>/> ${l.displayLanguage} - (${l.getDisplayLanguage(currentLanguage)} - <c:if test="${not empty browserLanguage and l.language eq browserLanguage.language}"><c:set + (${l.getDisplayLanguage(viewmodel.currentLanguage)} + <c:if test="${not empty viewmodel.browserLanguage and l.language eq viewmodel.browserLanguage.language}"><c:set var="browserLanguagePresent" value="true"/> - <fmt:message key="browserLanguage"/></c:if>) </label> </c:forEach>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,66 +28,46 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="project" type="de.uapcore.lightpit.entities.Project" scope="request" /> -<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/> -<jsp:useBean id="statsAffected" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> -<jsp:useBean id="statsScheduled" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> -<jsp:useBean id="statsResolved" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/> -<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/> -<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/> -<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request" /> -<div id="project-attributes"> - <div class="row"> - <div class="caption"><fmt:message key="name"/>:</div> - <div><c:out value="${project.name}"/></div> - <div class="caption"><fmt:message key="description"/>:</div> - <div><c:out value="${project.description}"/></div> - </div> - <div class="row"> - <div class="caption"><fmt:message key="owner"/>:</div> - <div> - <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if> - </div> - <div class="caption"><fmt:message key="repoUrl"/>:</div> - <div> - <c:if test="${not empty project.repoUrl}"> - <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out - value="${project.repoUrl}"/></a> - </c:if> - </div> - </div> -</div> +<c:set var="project" scope="page" value="${viewmodel.project}"/> +<%@include file="../jspf/project-header.jsp"%> <div id="tool-area"> <a href="./projects/versions/edit" class="button"><fmt:message key="button.version.create"/></a> - <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a> - <a href="./projects/issues/" class="button"><fmt:message key="button.issue.list"/></a> - <c:if test="${not statsHideZeros}"> - <a href="./projects/view?reduced=1" class="button"><fmt:message key="button.stats.hidezeros"/></a> - </c:if> - <c:if test="${statsHideZeros}"> - <a href="./projects/view?reduced=0" class="button"><fmt:message key="button.stats.showzeros"/></a> - </c:if> + <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a> </div> -<div id="version-stats"> -<c:forEach var="version" items="${versions}" varStatus="iter"> +<h2><fmt:message key="progress" /></h2> + +<c:set var="summary" value="${viewmodel.issuesTotal}" /> +<%@include file="../jspf/issue-summary.jsp"%> + +<h2><fmt:message key="issue.without-version" /></h2> + +<c:set var="issues" value="${viewmodel.issuesWithoutVersion}"/> +<c:set var="summary" value="${viewmodel.issuesWithoutVersionTotal}" /> +<%@include file="../jspf/issue-summary.jsp"%> +<%@include file="../jspf/issue-list.jsp"%> + +<c:forEach var="versionInfo" items="${viewmodel.versionInfos}"> <h2> - <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/> - <a href="./projects/versions/edit?id=${version.id}">✎</a> + <fmt:message key="version.label" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/> + (<a href="./projects/versions/view?vid=${versionInfo.version.id}">open</a>) </h2> - <h3><fmt:message key="version.statistics.affected" /></h3> - <c:set var="stats" value="${statsAffected[iter.index]}" /> - <%@include file="../jspf/version-stats.jsp" %> + <h3><fmt:message key="issues.reported"/> </h3> + <c:set var="summary" value="${versionInfo.reportedTotal}"/> + <c:set var="issues" value="${versionInfo.reported}"/> + <%@include file="../jspf/issue-summary.jsp"%> - <h3><fmt:message key="version.statistics.scheduled" /></h3> - <c:set var="stats" value="${statsScheduled[iter.index]}" /> - <%@include file="../jspf/version-stats.jsp" %> + <h3><fmt:message key="issues.scheduled"/> </h3> + <c:set var="summary" value="${versionInfo.scheduledTotal}"/> + <c:set var="issues" value="${versionInfo.scheduled}"/> + <%@include file="../jspf/issue-summary.jsp"%> - <h3><fmt:message key="version.statistics.resolved" /></h3> - <c:set var="stats" value="${statsResolved[iter.index]}" /> - <%@include file="../jspf/version-stats.jsp" %> -</c:forEach> -</div> + <h3><fmt:message key="issues.resolved"/> </h3> + <c:set var="summary" value="${versionInfo.resolvedTotal}"/> + <c:set var="issues" value="${versionInfo.resolved}"/> + <%@include file="../jspf/issue-summary.jsp"%> +</c:forEach> \ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/projects.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/projects.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -25,15 +25,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --%> <%@page pageEncoding="UTF-8" %> -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectIndexView" scope="request"/> -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request"/> - -<c:if test="${empty projects}"> +<c:if test="${empty viewmodel.projects}"> <div class="info-box"> <fmt:message key="no-projects"/> </div> @@ -43,30 +40,35 @@ <a href="./projects/edit" class="button"><fmt:message key="button.create"/></a> </div> -<c:if test="${not empty projects}"> +<c:if test="${not empty viewmodel.projects}"> <table id="project-list" class="datatable medskip"> <colgroup> <col> <col width="20%"> <col width="50%"> - <col width="10%"> - <col width="10%"> - <col width="10%"> + <col width="6%"> + <col width="6%"> + <col width="6%"> + <col width="6%"> + <col width="6%"> </colgroup> <thead> <tr> <th></th> <th><fmt:message key="name"/></th> <th><fmt:message key="repoUrl"/></th> - <th><fmt:message key="issues.open"/></th> - <th><fmt:message key="issues.active"/></th> - <th><fmt:message key="issues.done"/></th> + <th class="hcenter"><fmt:message key="version.latest"/></th> + <th class="hcenter"><fmt:message key="version.next"/></th> + <th class="hcenter"><fmt:message key="issues.open"/></th> + <th class="hcenter"><fmt:message key="issues.active"/></th> + <th class="hcenter"><fmt:message key="issues.done"/></th> </tr> </thead> <tbody> - <c:forEach var="project" items="${projects}"> + <c:forEach var="projectInfo" items="${viewmodel.projects}"> + <c:set var="project" scope="page" value="${projectInfo.project}"/> <tr class="nowrap"> - <td style="width: 2em;"><a href="./projects/edit?id=${project.id}">✎</a></td> + <td style="width: 2em;"><a href="./projects/edit?pid=${project.id}">✎</a></td> <td><a href="./projects/view?pid=${project.id}"><c:out value="${project.name}"/></a> </td> <td> @@ -75,9 +77,19 @@ value="${project.repoUrl}"/></a> </c:if> </td> - <td>${project.openIssues}</td> - <td>${project.activeIssues}</td> - <td>${project.doneIssues}</td> + <td class="hright"> + <c:if test="${not empty projectInfo.latestVersion}"> + <a href="./projects/versions/view?vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a> + </c:if> + </td> + <td class="hright"> + <c:if test="${not empty projectInfo.nextVersion}"> + <a href="./projects/versions/view?vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a> + </c:if> + </td> + <td class="hright">${projectInfo.issueSummary.open}</td> + <td class="hright">${projectInfo.issueSummary.active}</td> + <td class="hright">${projectInfo.issueSummary.done}</td> </tr> </c:forEach> </tbody>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/site.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -26,7 +26,6 @@ --%> <%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %> <%@page import="de.uapcore.lightpit.Constants" %> -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> @@ -60,9 +59,6 @@ <fmt:setLocale scope="request" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/> </c:if> -<%-- Selected project, if any --%> -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/> - <%-- Load resource bundles --%> <fmt:setBundle scope="request" basename="${bundleName}"/> <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/>
--- a/src/main/webapp/WEB-INF/jsp/user-form.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,7 +28,8 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="user" type="de.uapcore.lightpit.entities.User" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersEditView" scope="request"/> +<c:set var="user" scope="page" value="${viewmodel.user}" /> <form action="./teams/commit" method="post"> <table class="formtable">
--- a/src/main/webapp/WEB-INF/jsp/users.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/users.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,9 +28,9 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersView" scope="request"/> -<c:if test="${empty users}"> +<c:if test="${empty viewmodel.users}"> <div class="info-box"> <fmt:message key="no-users"/> </div> @@ -40,7 +40,7 @@ <a href="./teams/edit" class="button"><fmt:message key="button.create"/></a> </div> -<c:if test="${not empty users}"> +<c:if test="${not empty viewmodel.users}"> <table class="datatable medskip"> <thead> <tr> @@ -49,7 +49,7 @@ </tr> </thead> <tbody> - <c:forEach var="user" items="${users}"> + <c:forEach var="user" items="${viewmodel.users}"> <tr> <td><a href="./teams/edit?id=${user.id}">✎</a></td> <td><c:out value="${user.displayname}"/></td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -28,14 +28,8 @@ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" /> -<jsp:useBean id="version" type="de.uapcore.lightpit.entities.Version" scope="request"/> -<jsp:useBean id="versionStatusEnum" type="de.uapcore.lightpit.entities.VersionStatus[]" scope="request"/> - -<jsp:useBean id="statsAffected" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> -<jsp:useBean id="statsScheduled" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> -<jsp:useBean id="statsResolved" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/> -<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/> +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionEditView" scope="request" /> +<c:set var="version" scope="page" value="${viewmodel.version}"/> <form action="./projects/versions/commit" method="post"> <table class="formtable" style="width: 35ch"> @@ -47,19 +41,21 @@ <tr> <th><fmt:message key="version.project"/></th> <td> - <c:if test="${version.project.id ge 0}"> - <c:out value="${version.project.name}" /> - <input type="hidden" name="pid" value="${version.project.id}" /> - </c:if> - <c:if test="${empty version.project or version.project.id lt 0}"> - <select name="pid" required> - <c:forEach var="project" items="${projects}"> - <option value="${project.id}"> - <c:out value="${project.name}" /> - </option> - </c:forEach> - </select> - </c:if> + <c:choose> + <c:when test="${not empty version.project}"> + <c:out value="${version.project.name}" /> + <input type="hidden" name="pid" value="${version.project.id}" /> + </c:when> + <c:otherwise> + <select name="pid" required> + <c:forEach var="project" items="${viewmodel.projects}"> + <option value="${project.id}"> + <c:out value="${project.name}" /> + </option> + </c:forEach> + </select> + </c:otherwise> + </c:choose> </td> </tr> <tr> @@ -70,7 +66,7 @@ <th><fmt:message key="version.status"/></th> <td> <select name="status" required> - <c:forEach var="elem" items="${versionStatusEnum}"> + <c:forEach var="elem" items="${viewmodel.versionStatus}"> <option <c:if test="${elem eq version.status}">selected</c:if> value="${elem}"><fmt:message key="version.status.${elem}"/></option> @@ -90,7 +86,7 @@ <td colspan="2"> <input type="hidden" name="id" value="${version.id}"/> <c:choose> - <c:when test="${not empty version.project and version.project.id ge 0}"> + <c:when test="${not empty version.project}"> <c:set var="cancelUrl">./projects/view?pid=${version.project.id}</c:set> </c:when> <c:otherwise>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/version.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,65 @@ +<%-- +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + +Copyright 2018 Mike Becker. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--%> +<%@page pageEncoding="UTF-8" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionView" scope="request"/> +<c:set var="version" scope="page" value="${viewmodel.versionInfo.version}"/> + +<c:set var="project" scope="page" value="${version.project}"/> +<%@include file="../jspf/project-header.jsp"%> + + +<div id="tool-area"> + <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a> +</div> + +<h2> +<fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/> +<a href="./projects/versions/edit?vid=${version.id}">✎</a> +</h2> + +<h3><fmt:message key="issues.reported"/> </h3> + +<c:set var="summary" value="${viewmodel.versionInfo.reportedTotal}"/> +<c:set var="issues" value="${viewmodel.versionInfo.reported}"/> +<%@include file="../jspf/issue-summary.jsp"%> +<%@include file="../jspf/issue-list.jsp"%> + +<h3><fmt:message key="issues.scheduled"/> </h3> +<c:set var="summary" value="${viewmodel.versionInfo.scheduledTotal}"/> +<c:set var="issues" value="${viewmodel.versionInfo.scheduled}"/> +<%@include file="../jspf/issue-summary.jsp"%> +<%@include file="../jspf/issue-list.jsp"%> + +<h3><fmt:message key="issues.resolved"/> </h3> +<c:set var="summary" value="${viewmodel.versionInfo.resolvedTotal}"/> +<c:set var="issues" value="${viewmodel.versionInfo.resolved}"/> +<%@include file="../jspf/issue-summary.jsp"%> +<%@include file="../jspf/issue-list.jsp"%> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/issue-list.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,53 @@ +<%-- +issues: List<Issue> +--%> + +<table class="fullwidth datatable medskip"> + <thead> + <tr> + <th><fmt:message key="issue.subject"/></th> + <th><fmt:message key="issue.assignee"/></th> + <th><fmt:message key="issue.category"/></th> + <th><fmt:message key="issue.status"/></th> + <th><fmt:message key="issue.created"/></th> + <th><fmt:message key="issue.updated"/></th> + <th><fmt:message key="issue.eta"/></th> + </tr> + </thead> + <tbody> + <c:forEach var="issue" items="${issues}"> + <tr> + <td> + <span class="phase-${issue.status.phase}"> + <a href="./projects/issues/edit?issue=${issue.id}"> + <c:out value="${issue.subject}" /> + </a> + </span> + </td> + <td> + <c:if test="${not empty issue.assignee}"> + <c:out value="${issue.assignee.shortDisplayname}" /> + </c:if> + <c:if test="${empty issue.assignee}"> + <fmt:message key="placeholder.null-assignee" /> + </c:if> + </td> + <td> + <fmt:message key="issue.category.${issue.category}" /> + </td> + <td> + <fmt:message key="issue.status.${issue.status}" /> + </td> + <td> + <fmt:formatDate value="${issue.created}" type="BOTH"/> + </td> + <td> + <fmt:formatDate value="${issue.updated}" type="BOTH"/> + </td> + <td> + <fmt:formatDate value="${issue.eta}" /> + </td> + </tr> + </c:forEach> + </tbody> +</table>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/issue-summary.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,20 @@ +<%-- +summary: IssueSummary +--%> + +<div class="issue-summary"> + <div class="row"> + <div class="caption"><fmt:message key="issues.open"/>:</div> + <div><c:out value="${summary.open}"/></div> + <div class="caption"><fmt:message key="issues.active"/>:</div> + <div><c:out value="${summary.active}"/></div> + <div class="caption"><fmt:message key="issues.done"/>:</div> + <div><c:out value="${summary.done}"/></div> + </div> +</div> + +<div class="issue-progress-bar"> + <div class="open" style="width: ${summary.openPercent}%"></div> + <div class="active" style="width: ${summary.activePercent}%"></div> + <div class="done" style="width: ${summary.donePercent}%"></div> +</div> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jspf/project-header.jsp Mon Jun 01 14:46:58 2020 +0200 @@ -0,0 +1,24 @@ +<%-- +project: Project +--%> +<div class="project-attributes"> + <div class="row"> + <div class="caption"><fmt:message key="name"/>:</div> + <div><c:out value="${project.name}"/></div> + <div class="caption"><fmt:message key="description"/>:</div> + <div><c:out value="${project.description}"/></div> + </div> + <div class="row"> + <div class="caption"><fmt:message key="owner"/>:</div> + <div> + <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if> + </div> + <div class="caption"><fmt:message key="repoUrl"/>:</div> + <div> + <c:if test="${not empty project.repoUrl}"> + <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out + value="${project.repoUrl}"/></a> + </c:if> + </div> + </div> +</div>
--- a/src/main/webapp/WEB-INF/jspf/version-stats.jsp Sat May 30 18:12:38 2020 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,54 +0,0 @@ -<%@ taglib uri = "http://java.sun.com/jsp/jstl/functions" prefix = "fn" %> - -<table class="datatable"> - <c:if test="${statsHideZeros}"> - <c:set var="visibleColumns" value="0"/> - <c:forEach var="idx" begin="0" end="${fn:length(issueStatusEnum)-1}"> - <c:set var="visibleColumns" value="${visibleColumns + (stats.columnTotals[idx] eq 0 ? 0 :1)}"/> - </c:forEach> - </c:if> - <c:if test="${not statsHideZeros}"> - <c:set var="visibleColumns" value="${fn:length(issueStatusEnum)}" /> - </c:if> - <c:set var="colwidth"><fmt:formatNumber value="${100/(visibleColumns+2)}" maxFractionDigits="0" /></c:set> - <colgroup> - <c:forEach var="idx" begin="1" end="${visibleColumns+2}"> - <col width="${colwidth}%"> - </c:forEach> - </colgroup> - <thead> - <tr> - <th></th> - <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> - <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> - <th class="hcenter"><fmt:message key="issue.status.${issueStatus}"/></th> - </c:if> - </c:forEach> - <th class="hcenter"><fmt:message key="version.statistics.total"/> </th> - </tr> - </thead> - <tbody> - <c:forEach var="issueCategory" items="${issueCategoryEnum}" varStatus="categoryIter"> - <c:if test="${not statsHideZeros or stats.rowTotals[categoryIter.index] gt 0}"> - <tr> - <th><fmt:message key="issue.category.${issueCategory}" /></th> - <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> - <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> - <td>${stats.issueCount[categoryIter.index][statusIter.index]}</td> - </c:if> - </c:forEach> - <td>${stats.rowTotals[categoryIter.index]}</td> - </tr> - </c:if> - </c:forEach> - <tr> - <th><fmt:message key="version.statistics.total"/> </th> - <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter"> - <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}"> - <td>${stats.columnTotals[statusIter.index]}</td> - </c:if> - </c:forEach> - <td>${stats.total}</td> - </tr> - </tbody> -</table> \ No newline at end of file
--- a/src/main/webapp/lightpit.css Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/lightpit.css Mon Jun 01 14:46:58 2020 +0200 @@ -117,7 +117,6 @@ } table.datatable { - width: auto; border-style: solid; border-width: 1pt; border-color: silver;
--- a/src/main/webapp/projects.css Sat May 30 18:12:38 2020 +0200 +++ b/src/main/webapp/projects.css Mon Jun 01 14:46:58 2020 +0200 @@ -27,21 +27,12 @@ * */ -#issue-list td { - white-space: nowrap; -} - -#version-stats h2 { - white-space: nowrap; +.project-attributes, .issue-summary { + display: table; } -#version-stats td { - text-align: right; -} - -#project-attributes { +.project-attributes { margin-bottom: 2em; - display: table; } .row { @@ -63,3 +54,28 @@ span.phase-2 { text-decoration: line-through; } + +.issue-progress-bar { + display: flex; + position: relative; + width: 100ex; + height: 2em; + border-style: inset; + border-width: 2pt; + border-color: #6060cc; +} + +.issue-progress-bar .open { + height: 100%; + background: steelblue; +} + +.issue-progress-bar .active { + height: 100%; + background: gold; +} + +.issue-progress-bar .done { + height: 100%; + background: green; +}