# HG changeset patch # User Mike Becker # Date 1590175437 -7200 # Node ID 33b6843fdf8a39ac8240e7f6bd1390e158fe787d # Parent 91d1fc2a3a14cb94adb443d0eded526e29f804ed adds the ability to create and edit issues diff -r 91d1fc2a3a14 -r 33b6843fdf8a setup/postgres/psql_create_tables.sql --- a/setup/postgres/psql_create_tables.sql Fri May 22 17:26:27 2020 +0200 +++ b/setup/postgres/psql_create_tables.sql Fri May 22 21:23:57 2020 +0200 @@ -10,7 +10,7 @@ ); create table lpit_project ( - id serial primary key, + projectid serial primary key, name varchar(20) not null unique, description varchar(200), repoUrl varchar(50), @@ -26,8 +26,8 @@ ); create table lpit_version ( - id serial primary key, - project integer not null references lpit_project(id), + versionid serial primary key, + project integer not null references lpit_project(projectid), name varchar(20) not null, ordinal integer not null default 0, status version_status not null default 'Future' @@ -53,15 +53,34 @@ ); create table lpit_issue ( - id serial primary key, - project integer not null references lpit_project(id), + issueid serial primary key, + project integer not null references lpit_project(projectid), status issue_status not null default 'InSpecification', category issue_category not null default 'Feature', subject varchar(20) not null, description text, - version_plan integer references lpit_version(id), - version_done integer references lpit_version(id), + assignee integer references lpit_user(userid), created timestamp with time zone not null default now(), updated timestamp with time zone not null default now(), eta date ); + +create table lpit_issue_affected_version ( + issueid integer references lpit_issue(issueid), + versionid integer references lpit_version(versionid), + primary key (issueid, versionid) +); + +create table lpit_issue_scheduled_version ( + issueid integer references lpit_issue(issueid), + versionid integer references lpit_version(versionid), + primary key (issueid, versionid) +); + +create table lpit_issue_resolved_version ( + issueid integer references lpit_issue(issueid), + versionid integer references lpit_version(versionid), + primary key (issueid, versionid) +); + + diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java Fri May 22 21:23:57 2020 +0200 @@ -295,6 +295,13 @@ final String paramValue = req.getParameter(name); if (paramValue == null) return Optional.empty(); if (clazz.equals(String.class)) return Optional.of((T) paramValue); + if (java.sql.Date.class.isAssignableFrom(clazz)) { + try { + return Optional.of((T)java.sql.Date.valueOf(paramValue)); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } try { final Constructor ctor = clazz.getConstructor(String.class); return Optional.of(ctor.newInstance(paramValue)); diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/GenericDao.java --- a/src/main/java/de/uapcore/lightpit/dao/GenericDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/GenericDao.java Fri May 22 21:23:57 2020 +0200 @@ -34,6 +34,7 @@ /** * Finds an entity by its integer ID. + * It is not guaranteed that referenced entities are automatically joined. * * @param id the id * @return the enity or null if there is no such entity @@ -43,6 +44,7 @@ /** * Inserts an instance into database. + * It is not guaranteed that generated fields will be updated in the instance. * * @param instance the instance to insert * @throws SQLException on any kind of SQL errors diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/IssueDao.java --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Fri May 22 21:23:57 2020 +0200 @@ -38,10 +38,31 @@ /** * Lists all issues for the specified project. + * This is not guaranteed to contain version information. + * Use {@link #joinVersionInformation(Issue)} to obtain this information for a specific issue. * * @param project the project * @return a list of issues * @throws SQLException on any kind of SQL error */ List list(Project project) throws SQLException; + + /** + * Saves an instances to the database. + * Implementations of this DAO must guarantee that the generated ID is stored in the instance. + * + * @param instance the instance to insert + * @throws SQLException on any kind of SQL error + * @see Issue#setId(int) + */ + @Override + void save(Issue instance) throws SQLException; + + /** + * Retrieves the affected, scheduled and resolved versions for the specified issue. + * + * @param issue the issue to join the information for + * @throws SQLException on any kind of SQL error + */ + void joinVersionInformation(Issue issue) throws SQLException; } diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Fri May 22 21:23:57 2020 +0200 @@ -43,63 +43,92 @@ public final class PGIssueDao implements IssueDao { - private final PreparedStatement insert, update, list, find; + private final PreparedStatement insert, update, list, find, affectedVersions, scheduledVersions, resolvedVersions; public PGIssueDao(Connection connection) throws SQLException { list = connection.prepareStatement( - "select issue.id, issue.project, issue.status, issue.category, issue.subject, issue.description, " + - "vplan.id, vplan.name, vdone.id, vdone.name, " + - "issue.created, issue.updated, issue.eta " + - "from lpit_issue issue " + - "left join lpit_version vplan on vplan.id = version_plan " + - "left join lpit_version vdone on vdone.id = version_done " + - "where issue.project = ? "); + "select issueid, project, status, category, subject, description, " + + "userid, username, givenname, lastname, mail, " + + "created, updated, eta " + + "from lpit_issue " + + "left join lpit_user on userid = assignee " + + "where project = ? "); find = connection.prepareStatement( - "select issue.id, issue.project, issue.status, issue.category, issue.subject, issue.description, " + - "vplan.id, vplan.name, vdone.id, vdone.name, " + - "issue.created, issue.updated, issue.eta " + - "from lpit_issue issue " + - "left join lpit_version vplan on vplan.id = version_plan " + - "left join lpit_version vdone on vdone.id = version_done " + - "where issue.id = ? "); + "select issueid, project, status, category, subject, description, " + + "userid, username, givenname, lastname, mail, " + + "created, updated, eta " + + "from lpit_issue " + + "left join lpit_user on userid = assignee " + + "where issueid = ? "); insert = connection.prepareStatement( - "insert into lpit_issue (project, status, category, subject, description, version_plan, version_done, eta) " + - "values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?, ?)" + "insert into lpit_issue (project, status, category, subject, description, assignee, eta) " + + "values (?, ?::issue_status, ?::issue_category, ?, ?, ?, ?) returning issueid" ); update = connection.prepareStatement( "update lpit_issue set updated = now(), status = ?::issue_status, category = ?::issue_category, " + - "subject = ?, description = ?, version_plan = ?, version_done = ?, eta = ? where id = ?" + "subject = ?, description = ?, assignee = ?, eta = ? where issueid = ?" + ); + + affectedVersions = connection.prepareStatement( + "select v.versionid, v.name, v.status, v.ordinal " + + "from lpit_version v join lpit_issue_affected_version using (versionid) " + + "where issueid = ? " + + "order by v.ordinal, v.name" + ); + + scheduledVersions = connection.prepareStatement( + "select v.versionid, v.name, v.status, v.ordinal " + + "from lpit_version v join lpit_issue_scheduled_version using (versionid) " + + "where issueid = ? " + + "order by v.ordinal, v.name" + ); + + resolvedVersions = connection.prepareStatement( + "select v.versionid, v.name, v.status, v.ordinal " + + "from lpit_version v join lpit_issue_resolved_version using (versionid) " + + "where issueid = ? " + + "order by v.ordinal, v.name" ); } - private Version obtainVersion(ResultSet result, Project project, String prefix) throws SQLException { - final int vplan = result.getInt(prefix + "id"); - if (vplan > 0) { - final var ver = new Version(vplan, project); - ver.setName(result.getString(prefix + "name")); - return ver; + private User obtainAssignee(ResultSet result) throws SQLException { + final int id = result.getInt("userid"); + if (id != 0) { + final var user = new User(id); + user.setUsername(result.getString("username")); + user.setGivenname(result.getString("givenname")); + user.setLastname(result.getString("lastname")); + user.setMail(result.getString("mail")); + return user; } else { return null; } } - public Issue mapColumns(ResultSet result) throws SQLException { - final var project = new Project(result.getInt("issue.project")); - final var issue = new Issue(result.getInt("issue.id"), project); - issue.setStatus(IssueStatus.valueOf(result.getString("issue.status"))); - issue.setCategory(IssueCategory.valueOf(result.getString("issue.category"))); - issue.setSubject(result.getString("issue.subject")); - issue.setDescription(result.getString("issue.description")); - issue.setScheduledVersion(obtainVersion(result, project, "vplan.")); - issue.setResolvedVersion(obtainVersion(result, project, "vdone.")); - issue.setCreated(result.getTimestamp("issue.created")); - issue.setUpdated(result.getTimestamp("issue.updated")); - issue.setEta(result.getDate("issue.eta")); + private Issue mapColumns(ResultSet result) throws SQLException { + final var project = new Project(result.getInt("project")); + final var issue = new Issue(result.getInt("issueid"), project); + issue.setStatus(IssueStatus.valueOf(result.getString("status"))); + issue.setCategory(IssueCategory.valueOf(result.getString("category"))); + issue.setSubject(result.getString("subject")); + issue.setDescription(result.getString("description")); + issue.setAssignee(obtainAssignee(result)); + issue.setCreated(result.getTimestamp("created")); + issue.setUpdated(result.getTimestamp("updated")); + issue.setEta(result.getDate("eta")); return issue; } + private Version mapVersion(ResultSet result, Project project) throws SQLException { + final var version = new Version(result.getInt("v.versionid"), project); + version.setName(result.getString("v.name")); + version.setOrdinal(result.getInt("v.ordinal")); + version.setStatus(VersionStatus.valueOf(result.getString("v.status"))); + return version; + } + @Override public void save(Issue instance) throws SQLException { Objects.requireNonNull(instance.getSubject()); @@ -109,36 +138,38 @@ insert.setString(3, instance.getCategory().name()); insert.setString(4, instance.getSubject()); setStringOrNull(insert, 5, instance.getDescription()); - setForeignKeyOrNull(insert, 6, instance.getScheduledVersion(), Version::getId); - setForeignKeyOrNull(insert, 7, instance.getResolvedVersion(), Version::getId); - setDateOrNull(insert, 8, instance.getEta()); - insert.executeUpdate(); + setForeignKeyOrNull(insert, 6, instance.getAssignee(), User::getId); + setDateOrNull(insert, 7, instance.getEta()); + // insert and retrieve the ID + final var rs = insert.executeQuery(); + rs.next(); + instance.setId(rs.getInt(1)); } @Override public boolean update(Issue instance) throws SQLException { + if (instance.getId() < 0) return false; Objects.requireNonNull(instance.getSubject()); update.setString(1, instance.getStatus().name()); update.setString(2, instance.getCategory().name()); update.setString(3, instance.getSubject()); setStringOrNull(update, 4, instance.getDescription()); - setForeignKeyOrNull(update, 5, instance.getScheduledVersion(), Version::getId); - setForeignKeyOrNull(update, 6, instance.getResolvedVersion(), Version::getId); - setDateOrNull(update, 7, instance.getEta()); - update.setInt(8, instance.getId()); + setForeignKeyOrNull(update, 5, instance.getAssignee(), User::getId); + setDateOrNull(update, 6, instance.getEta()); + update.setInt(7, instance.getId()); return update.executeUpdate() > 0; } @Override public List list(Project project) throws SQLException { list.setInt(1, project.getId()); - List versions = new ArrayList<>(); + List issues = new ArrayList<>(); try (var result = list.executeQuery()) { while (result.next()) { - versions.add(mapColumns(result)); + issues.add(mapColumns(result)); } } - return versions; + return issues; } @Override @@ -152,4 +183,23 @@ } } } + + private List listVersions(PreparedStatement stmt, Issue issue) throws SQLException { + stmt.setInt(1, issue.getId()); + List versions = new ArrayList<>(); + try (var result = stmt.executeQuery()) { + while (result.next()) { + versions.add(mapVersion(result, issue.getProject())); + } + } + return versions; + } + + @Override + public void joinVersionInformation(Issue issue) throws SQLException { + Objects.requireNonNull(issue.getProject()); + issue.setAffectedVersions(listVersions(affectedVersions, issue)); + issue.setScheduledVersions(listVersions(scheduledVersions, issue)); + issue.setResolvedVersions(listVersions(resolvedVersions, issue)); + } } diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java Fri May 22 21:23:57 2020 +0200 @@ -49,29 +49,29 @@ public PGProjectDao(Connection connection) throws SQLException { list = connection.prepareStatement( - "select id, name, description, repourl, " + + "select projectid, name, description, repourl, " + "userid, username, lastname, givenname, mail " + "from lpit_project " + "left join lpit_user owner on lpit_project.owner = owner.userid " + "order by name"); find = connection.prepareStatement( - "select id, name, description, repourl, " + + "select projectid, name, description, repourl, " + "userid, username, lastname, givenname, mail " + "from lpit_project " + "left join lpit_user owner on lpit_project.owner = owner.userid " + - "where id = ?"); + "where projectid = ?"); insert = connection.prepareStatement( "insert into lpit_project (name, description, repourl, owner) values (?, ?, ?, ?)" ); update = connection.prepareStatement( - "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where id = ?" + "update lpit_project set name = ?, description = ?, repourl = ?, owner = ? where projectid = ?" ); } public Project mapColumns(ResultSet result) throws SQLException { - final var proj = new Project(result.getInt("id")); + final var proj = new Project(result.getInt("projectid")); proj.setName(result.getString("name")); proj.setDescription(result.getString("description")); proj.setRepoUrl(result.getString("repourl")); @@ -101,6 +101,7 @@ @Override public boolean update(Project instance) throws SQLException { + if (instance.getId() < 0) return false; Objects.requireNonNull(instance.getName()); update.setString(1, instance.getName()); setStringOrNull(update, 2, instance.getDescription()); diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Fri May 22 21:23:57 2020 +0200 @@ -58,7 +58,7 @@ update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?"); } - public User mapColumns(ResultSet result) throws SQLException { + private User mapColumns(ResultSet result) throws SQLException { final int id = result.getInt("userid"); if (id == 0) return null; final var user = new User(id); @@ -81,6 +81,7 @@ @Override public boolean update(User instance) throws SQLException { + if (instance.getId() < 0) return false; setStringOrNull(update, 1, instance.getLastname()); setStringOrNull(update, 2, instance.getGivenname()); setStringOrNull(update, 3, instance.getMail()); diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java Fri May 22 21:23:57 2020 +0200 @@ -47,26 +47,27 @@ public PGVersionDao(Connection connection) throws SQLException { list = connection.prepareStatement( - "select id, project, name, ordinal, status " + + "select versionid, project, name, ordinal, status " + "from lpit_version " + "where project = ? " + "order by ordinal, lower(name)"); find = connection.prepareStatement( - "select id, project, name, ordinal, status " + + "select versionid, project, name, ordinal, status " + "from lpit_version " + - "where id = ?"); + "where versionid = ?"); insert = connection.prepareStatement( "insert into lpit_version (project, name, ordinal, status) values (?, ?, ?, ?::version_status)" ); update = connection.prepareStatement( - "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where id = ?" + "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?" ); } - public Version mapColumns(ResultSet result) throws SQLException { - final var version = new Version(result.getInt("id"), new Project(result.getInt("project"))); + private Version mapColumns(ResultSet result) throws SQLException { + final var project = new Project(result.getInt("project")); + final var version = new Version(result.getInt("versionid"), project); version.setName(result.getString("name")); version.setOrdinal(result.getInt("ordinal")); version.setStatus(VersionStatus.valueOf(result.getString("status"))); @@ -86,6 +87,7 @@ @Override public boolean update(Version instance) throws SQLException { + if (instance.getId() < 0) return false; Objects.requireNonNull(instance.getName()); update.setString(1, instance.getName()); update.setInt(2, instance.getOrdinal()); @@ -100,7 +102,9 @@ List versions = new ArrayList<>(); try (var result = list.executeQuery()) { while (result.next()) { - versions.add(mapColumns(result)); + final var v = mapColumns(result); + v.setProject(project); + versions.add(v); } } return versions; diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/entities/Issue.java --- a/src/main/java/de/uapcore/lightpit/entities/Issue.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java Fri May 22 21:23:57 2020 +0200 @@ -30,12 +30,14 @@ import java.sql.Date; import java.sql.Timestamp; +import java.time.Instant; +import java.util.Collections; import java.util.List; import java.util.Objects; public final class Issue { - private final int id; + private int id; private final Project project; private IssueStatus status; @@ -43,13 +45,14 @@ private String subject; private String description; + private User assignee; - private List affectedVersions; - private Version scheduledVersion; - private Version resolvedVersion; + private List affectedVersions = Collections.emptyList(); + private List scheduledVersions = Collections.emptyList(); + private List resolvedVersions = Collections.emptyList(); - private Timestamp created; - private Timestamp updated; + private Timestamp created = Timestamp.from(Instant.now()); + private Timestamp updated = Timestamp.from(Instant.now()); private Date eta; public Issue(int id, Project project) { @@ -61,6 +64,14 @@ return id; } + /** + * Should only be used by a DAO to store the generated ID. + * @param id the freshly generated ID returned from the database after insert + */ + public void setId(int id) { + this.id = id; + } + public Project getProject() { return project; } @@ -97,6 +108,14 @@ this.description = description; } + public User getAssignee() { + return assignee; + } + + public void setAssignee(User assignee) { + this.assignee = assignee; + } + public List getAffectedVersions() { return affectedVersions; } @@ -105,20 +124,20 @@ this.affectedVersions = affectedVersions; } - public Version getScheduledVersion() { - return scheduledVersion; + public List getScheduledVersions() { + return scheduledVersions; } - public void setScheduledVersion(Version scheduledVersion) { - this.scheduledVersion = scheduledVersion; + public void setScheduledVersions(List scheduledVersions) { + this.scheduledVersions = scheduledVersions; } - public Version getResolvedVersion() { - return resolvedVersion; + public List getResolvedVersions() { + return resolvedVersions; } - public void setResolvedVersion(Version resolvedVersion) { - this.resolvedVersion = resolvedVersion; + public void setResolvedVersions(List resolvedVersions) { + this.resolvedVersions = resolvedVersions; } public Timestamp getCreated() { diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/entities/User.java --- a/src/main/java/de/uapcore/lightpit/entities/User.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/User.java Fri May 22 21:23:57 2020 +0200 @@ -80,21 +80,26 @@ this.lastname = lastname; } - public String getDisplayname() { + public String getShortDisplayname() { StringBuilder dn = new StringBuilder(); if (givenname != null) dn.append(givenname); dn.append(' '); if (lastname != null) dn.append(lastname); - dn.append(' '); - if (mail != null && !mail.isBlank()) { - dn.append("<" + mail + ">"); - } final var str = dn.toString().trim(); return str.isBlank() ? username : str; } + public String getDisplayname() { + final String sdn = getShortDisplayname(); + if (mail != null && !mail.isBlank()) { + return sdn + " <" + mail + ">"; + } else { + return sdn; + } + } + @Override public boolean equals(Object o) { if (this == o) return true; diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/entities/Version.java --- a/src/main/java/de/uapcore/lightpit/entities/Version.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/entities/Version.java Fri May 22 21:23:57 2020 +0200 @@ -33,7 +33,7 @@ public final class Version implements Comparable { private final int id; - private final Project project; + private Project project; private String name; /** * If we do not want versions to be ordered lexicographically we may specify an order. @@ -50,6 +50,10 @@ return id; } + public void setProject(Project project) { + this.project = project; + } + public Project getProject() { return project; } diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Fri May 22 21:23:57 2020 +0200 @@ -38,12 +38,14 @@ import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import java.io.IOException; +import java.sql.Date; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.Objects; import static de.uapcore.lightpit.Functions.fqn; @@ -61,32 +63,85 @@ private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class); 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"); - private Project getSelectedProject(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - final var projectDao = dao.getProjectDao(); - final var session = req.getSession(); - final var projectSelection = getParameter(req, Integer.class, "pid"); - final Project selectedProject; - if (projectSelection.isPresent()) { - selectedProject = projectDao.find(projectSelection.get()); - } else { - final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); - selectedProject = sessionProject == null ? null : projectDao.find(sessionProject.getId()); + private class SessionSelection { + final HttpSession session; + Project project; + Version version; + Issue issue; + + SessionSelection(HttpServletRequest req, Project project) { + this.session = req.getSession(); + this.project = project; + version = null; + issue = null; + updateAttributes(); } - session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, selectedProject); - return selectedProject; + + 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()); + } + if (issue != null) { + version = null; // show the issue globally + project = projectDao.find(issue.getProject().getId()); + } + + final var projectSelection = getParameter(req, Integer.class, "pid"); + if (projectSelection.isPresent()) { + final var selectedProject = projectDao.find(projectSelection.get()); + if (!Objects.equals(selectedProject, project)) { + // reset version and issue if project changed + version = null; + issue = null; + } + project = selectedProject; + } else { + final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT); + project = sessionProject == null ? null : projectDao.find(sessionProject.getId()); + } + updateAttributes(); + } + + void selectVersion(Version version) { + if (!version.getProject().equals(project)) throw new AssertionError("Nice, you implemented a bug!"); + this.version = version; + this.issue = null; + updateAttributes(); + } + + void selectIssue(Issue issue) { + if (!issue.getProject().equals(project)) throw new AssertionError("Nice, you implemented a bug!"); + this.issue = issue; + this.version = null; + updateAttributes(); + } + + void updateAttributes() { + session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project); + session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version); + session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue); + } } /** * Creates the breadcrumb menu. * - * @param level the current active level - * @param selectedProject the selected project, if any, or null + * @param level the current active level (0: root, 1: project, 2: version, 3: issue) + * @param sessionSelection the currently selected objects * @return a dynamic breadcrumb menu trying to display as many levels as possible */ - private List getBreadcrumbs(int level, - Project selectedProject) { + private List getBreadcrumbs(int level, SessionSelection sessionSelection) { MenuEntry entry; final var breadcrumbs = new ArrayList(); @@ -95,52 +150,77 @@ breadcrumbs.add(entry); if (level == 0) entry.setActive(true); - if (selectedProject == null) - return breadcrumbs; + if (sessionSelection.project != null) { + if (sessionSelection.project.getId() < 0) { + entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"), + "projects/edit", 1); + } else { + entry = new MenuEntry(sessionSelection.project.getName(), + "projects/view?pid=" + sessionSelection.project.getId(), 1); + } + if (level == 1) entry.setActive(true); + breadcrumbs.add(entry); + } - entry = new MenuEntry(selectedProject.getName(), - "projects/view?pid=" + selectedProject.getId(), 1); - if (level == 1) entry.setActive(true); + if (sessionSelection.version != null) { + if (sessionSelection.version.getId() < 0) { + entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"), + "projects/versions/edit", 2); + } else { + entry = new MenuEntry(sessionSelection.version.getName(), + // TODO: change link to issue overview for that version + "projects/versions/edit?id=" + sessionSelection.version.getId(), 2); + } + if (level == 2) entry.setActive(true); + breadcrumbs.add(entry); + } - breadcrumbs.add(entry); + if (sessionSelection.issue != null) { + entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"), + // TODO: change link to a separate issue view (maybe depending on the selected version) + "projects/view?pid=" + sessionSelection.issue.getProject().getId(), 3); + breadcrumbs.add(entry); + if (sessionSelection.issue.getId() < 0) { + entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"), + "projects/issues/edit", 2); + } else { + entry = new MenuEntry("#" + sessionSelection.issue.getId(), + // TODO: maybe change link to a view rather than directly opening the editor + "projects/issues/edit?id=" + sessionSelection.issue.getId(), 4); + } + if (level == 3) entry.setActive(true); + breadcrumbs.add(entry); + } + return breadcrumbs; } @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); setContentPage(req, "projects"); setStylesheet(req, "projects"); - final var selectedProject = getSelectedProject(req, dao); - setBreadcrumbs(req, getBreadcrumbs(0, selectedProject)); + setBreadcrumbs(req, getBreadcrumbs(0, sessionSelection)); return ResponseType.HTML; } - private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, Optional project) throws SQLException { - if (project.isPresent()) { - req.setAttribute("project", project.get()); - setBreadcrumbs(req, getBreadcrumbs(1, project.get())); - } else { - req.setAttribute("project", new Project(-1)); - setBreadcrumbs(req, getBreadcrumbs(0, null)); - } - + private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + req.setAttribute("project", selection.project); req.setAttribute("users", dao.getUserDao().list()); setContentPage(req, "project-form"); + setBreadcrumbs(req, getBreadcrumbs(1, selection)); } @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))); - Optional project = findByParameter(req, Integer.class, "id", dao.getProjectDao()::find); - configureEditForm(req, dao, project); - if (project.isPresent()) { - req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, project.get()); - } + configureEditForm(req, dao, selection); return ResponseType.HTML; } @@ -148,7 +228,7 @@ @RequestMapping(requestPath = "commit", method = HttpMethod.POST) public ResponseType commit(HttpServletRequest req, DataAccessObjects dao) throws SQLException { - Project project = null; + Project project = new Project(-1); try { project = new Project(getParameter(req, Integer.class, "id").orElseThrow()); project.setName(getParameter(req, String.class, "name").orElseThrow()); @@ -163,11 +243,11 @@ setRedirectLocation(req, "./projects/"); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); LOG.debug("Successfully updated project {}", project.getName()); - } catch (NoSuchElementException | NumberFormatException | SQLException ex) { + } 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, Optional.ofNullable(project)); + configureEditForm(req, dao, new SessionSelection(req, project)); } return ResponseType.HTML; @@ -175,123 +255,133 @@ @RequestMapping(requestPath = "view", method = HttpMethod.GET) public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { - final var selectedProject = getSelectedProject(req, dao); - if (selectedProject == null) { - resp.sendError(HttpServletResponse.SC_FORBIDDEN); - return ResponseType.NONE; - } + final var sessionSelection = new SessionSelection(req, dao); - req.setAttribute("versions", dao.getVersionDao().list(selectedProject)); - req.setAttribute("issues", dao.getIssueDao().list(selectedProject)); + req.setAttribute("versions", dao.getVersionDao().list(sessionSelection.project)); + req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project)); - // TODO: add more levels depending on last visited location - setBreadcrumbs(req, getBreadcrumbs(1, selectedProject)); - + setBreadcrumbs(req, getBreadcrumbs(1, sessionSelection)); setContentPage(req, "project-details"); return ResponseType.HTML; } - private void configureEditVersionForm(HttpServletRequest req, Optional version, Project selectedProject) { - req.setAttribute("version", version.orElse(new Version(-1, selectedProject))); + private void configureEditVersionForm(HttpServletRequest req, SessionSelection selection) { + req.setAttribute("version", selection.version); req.setAttribute("versionStatusEnum", VersionStatus.values()); setContentPage(req, "version-form"); + setBreadcrumbs(req, getBreadcrumbs(2, selection)); } @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET) public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { - final var selectedProject = getSelectedProject(req, dao); - if (selectedProject == null) { + final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { + // TODO: remove this bullshit and only retrieve the object from session if we are creating a fresh version resp.sendError(HttpServletResponse.SC_FORBIDDEN); return ResponseType.NONE; } - configureEditVersionForm(req, - findByParameter(req, Integer.class, "id", dao.getVersionDao()::find), - selectedProject); + sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find) + .orElse(new Version(-1, sessionSelection.project))); + configureEditVersionForm(req, sessionSelection); return ResponseType.HTML; } @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST) public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { - final var selectedProject = getSelectedProject(req, dao); - if (selectedProject == null) { + final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { + // TODO: remove this bullshit and retrieve project id from hidden field resp.sendError(HttpServletResponse.SC_FORBIDDEN); return ResponseType.NONE; } - Version version = null; + var version = new Version(-1, sessionSelection.project); try { - version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject); + version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project); 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); - setRedirectLocation(req, "./projects/versions/"); + // specifying the pid parameter will purposely reset the session selected version! + setRedirectLocation(req, "./projects/view?pid="+sessionSelection.project.getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - LOG.debug("Successfully updated version {} for project {}", version.getName(), selectedProject.getName()); - } catch (NoSuchElementException | NumberFormatException | SQLException ex) { + 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); - configureEditVersionForm(req, Optional.ofNullable(version), selectedProject); + sessionSelection.selectVersion(version); + configureEditVersionForm(req, sessionSelection); } return ResponseType.HTML; } - private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, Optional issue, Project selectedProject) throws SQLException { - - req.setAttribute("issue", issue.orElse(new Issue(-1, selectedProject))); + private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException { + req.setAttribute("issue", selection.issue); req.setAttribute("issueStatusEnum", IssueStatus.values()); req.setAttribute("issueCategoryEnum", IssueCategory.values()); - req.setAttribute("versions", dao.getVersionDao().list(selectedProject)); + req.setAttribute("versions", dao.getVersionDao().list(selection.project)); + req.setAttribute("users", dao.getUserDao().list()); setContentPage(req, "issue-form"); + setBreadcrumbs(req, getBreadcrumbs(3, selection)); } @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { - final var selectedProject = getSelectedProject(req, dao); - if (selectedProject == null) { + final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { + // TODO: remove this bullshit and only retrieve the object from session if we are creating a fresh issue resp.sendError(HttpServletResponse.SC_FORBIDDEN); return ResponseType.NONE; } - configureEditIssueForm(req, dao, - findByParameter(req, Integer.class, "id", dao.getIssueDao()::find), - selectedProject); + sessionSelection.selectIssue(findByParameter(req, Integer.class, "id", + dao.getIssueDao()::find).orElse(new Issue(-1, sessionSelection.project))); + configureEditIssueForm(req, dao, sessionSelection); return ResponseType.HTML; } @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST) public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException { - final var selectedProject = getSelectedProject(req, dao); - if (selectedProject == null) { + // TODO: remove this bullshit and store the project ID as hidden field + final var sessionSelection = new SessionSelection(req, dao); + if (sessionSelection.project == null) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return ResponseType.NONE; } - Issue issue = null; + Issue issue = new Issue(-1, sessionSelection.project); try { - issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), selectedProject); - - // TODO: implement - + issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project); + 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()); + getParameter(req, Integer.class, "assignee").map( + userid -> userid >= 0 ? new User(userid) : null + ).ifPresent(issue::setAssignee); + getParameter(req, String.class, "description").ifPresent(issue::setDescription); + getParameter(req, Date.class, "eta").ifPresent(issue::setEta); dao.getIssueDao().saveOrUpdate(issue); - setRedirectLocation(req, "./projects/issues/"); + // TODO: redirect to issue overview + // specifying the issue parameter keeps the edited issue as breadcrumb + setRedirectLocation(req, "./projects/view?issue="+issue.getId()); setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); - LOG.debug("Successfully updated issue {} for project {}", issue.getId(), selectedProject.getName()); - } catch (NoSuchElementException | NumberFormatException | SQLException ex) { + 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); - configureEditIssueForm(req, dao, Optional.ofNullable(issue), selectedProject); + sessionSelection.selectIssue(issue); + configureEditIssueForm(req, dao, sessionSelection); } return ResponseType.HTML; diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/java/de/uapcore/lightpit/modules/UsersModule.java --- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Fri May 22 17:26:27 2020 +0200 +++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java Fri May 22 21:23:57 2020 +0200 @@ -91,7 +91,7 @@ setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); LOG.debug("Successfully updated user {}", user.getUsername()); - } catch (NoSuchElementException | NumberFormatException | SQLException ex) { + } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { // TODO: set request attribute with error text req.setAttribute("user", user); setContentPage(req, "user-form"); diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/resources/localization/projects.properties --- a/src/main/resources/localization/projects.properties Fri May 22 17:26:27 2020 +0200 +++ b/src/main/resources/localization/projects.properties Fri May 22 21:23:57 2020 +0200 @@ -29,6 +29,8 @@ no-projects=Welcome to LightPIT. Start off by creating a new project! +menu.issues=Issues + thead.name=Name thead.description=Description thead.repoUrl=Repository @@ -40,6 +42,7 @@ tooltip.ordinal=Use to override lexicographic ordering. placeholder.null-owner=Unassigned +placeholder.null-assignee=Unassigned version.status.Future=Future version.status.Unreleased=Unreleased @@ -48,8 +51,31 @@ version.status.Deprecated=Deprecated thead.issue.subject=Subject +thead.issue.description=Description +thead.issue.assignee=Assignee +thead.issue.affected-version=Affected Version +thead.issue.affected-versions=Affected Versions +thead.issue.scheduled-version=Scheduled for Version +thead.issue.scheduled-versions=Scheduled for Versions +thead.issue.resolved-version=Resolved in Version +thead.issue.resolved-versions=Resolved in Versions thead.issue.category=Category thead.issue.status=Status thead.issue.created=Created thead.issue.updated=Updated thead.issue.eta=ETA + +issue.category.Feature=Feature +issue.category.Improvement=Improvement +issue.category.Bug=Bug +issue.category.Task=Task +issue.category.Test=Test + +issue.status.InSpecification=Specification +issue.status.ToDo=To Do +issue.status.Scheduled=Scheduled +issue.status.InProgress=In Progress +issue.status.InReview=Review +issue.status.Done=Done +issue.status.Rejected=Rejected +issue.status.Withdrawn=Withdrawn diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/resources/localization/projects_de.properties --- a/src/main/resources/localization/projects_de.properties Fri May 22 17:26:27 2020 +0200 +++ b/src/main/resources/localization/projects_de.properties Fri May 22 21:23:57 2020 +0200 @@ -29,6 +29,8 @@ no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! +menu.issues=Vorg\u00e4nge + thead.name=Name thead.description=Beschreibung thead.repoUrl=Repository @@ -40,6 +42,7 @@ tooltip.ordinal=\u00dcbersteuert die lexikographische Sortierung. placeholder.null-owner=Nicht Zugewiesen +placeholder.null-assignee=Niemandem version.status.Future=Geplant version.status.Unreleased=Unver\u00f6ffentlicht @@ -48,8 +51,31 @@ version.status.Deprecated=Veraltet thead.issue.subject=Thema +thead.issue.description=Beschreibung +thead.issue.assignee=Zugewiesen +thead.issue.affected-version=Betroffene Version +thead.issue.affected-versions=Betroffene Versionen +thead.issue.scheduled-version=Zielversion +thead.issue.scheduled-versions=Zielversionen +thead.issue.resolved-version=Gel\u00f6st in Version +thead.issue.resolved-versions=Gel\u00f6st in Versionen thead.issue.category=Kategorie thead.issue.status=Status thead.issue.created=Erstellt thead.issue.updated=Aktualisiert thead.issue.eta=Zieldatum + +issue.category.Feature=Feature +issue.category.Improvement=Verbesserung +issue.category.Bug=Fehler +issue.category.Task=Aufgabe +issue.category.Test=Test + +issue.status.InSpecification=Spezifikation +issue.status.ToDo=Zu Erledigen +issue.status.Scheduled=Geplant +issue.status.InProgress=In Arbeit +issue.status.InReview=Im Review +issue.status.Done=Erledigt +issue.status.Rejected=Zur\u00fcckgewiesen +issue.status.Withdrawn=Zur\u00fcckgezogen diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/WEB-INF/jsp/issue-form.jsp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Fri May 22 21:23:57 2020 +0200 @@ -0,0 +1,162 @@ +<%-- +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" %> +<%@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" %> + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
" />
+ +
+ +
+ + + + + + + + + TODO
+ + + + + + + + + TODO
+ + + + + + + + + TODO
" />
+ + + + + +
+
diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/WEB-INF/jsp/project-details.jsp --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp Fri May 22 21:23:57 2020 +0200 @@ -66,8 +66,8 @@ - + @@ -76,5 +76,38 @@ - + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/WEB-INF/jsp/project-form.jsp --- a/src/main/webapp/WEB-INF/jsp/project-form.jsp Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/project-form.jsp Fri May 22 21:23:57 2020 +0200 @@ -43,15 +43,15 @@ - + " /> - + " /> - + " /> @@ -60,7 +60,7 @@ diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/WEB-INF/jsp/user-form.jsp --- a/src/main/webapp/WEB-INF/jsp/user-form.jsp Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp Fri May 22 21:23:57 2020 +0200 @@ -42,20 +42,20 @@ - " readonly /> - + " /> - + " /> - + " /> diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/WEB-INF/jsp/version-form.jsp --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp Fri May 22 21:23:57 2020 +0200 @@ -44,7 +44,7 @@ - + " /> @@ -69,8 +69,9 @@ - + + + diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/lightpit.css --- a/src/main/webapp/lightpit.css Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/lightpit.css Fri May 22 21:23:57 2020 +0200 @@ -158,6 +158,10 @@ width: 100%; } +table.formtable input[type=date] { + width: auto; +} + table.formtable tfoot td { text-align: right; } diff -r 91d1fc2a3a14 -r 33b6843fdf8a src/main/webapp/projects.css --- a/src/main/webapp/projects.css Fri May 22 17:26:27 2020 +0200 +++ b/src/main/webapp/projects.css Fri May 22 21:23:57 2020 +0200 @@ -26,3 +26,7 @@ * POSSIBILITY OF SUCH DAMAGE. * */ + +#issue-list td { + white-space: nowrap; +} \ No newline at end of file