adds issue comments

Fri, 09 Oct 2020 19:07:05 +0200

author
Mike Becker <universe@uap-core.de>
date
Fri, 09 Oct 2020 19:07:05 +0200
changeset 124
ed2e7aef2a3e
parent 123
c27eee1259bd
child 125
decc4c3854a1

adds issue comments

setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/IssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/UserDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/IssueComment.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
src/main/webapp/projects.css file | annotate | diff | comparison | revisions
     1.1 --- a/setup/postgres/psql_create_tables.sql	Fri Oct 09 19:06:51 2020 +0200
     1.2 +++ b/setup/postgres/psql_create_tables.sql	Fri Oct 09 19:07:05 2020 +0200
     1.3 @@ -83,4 +83,12 @@
     1.4      primary key (issueid, versionid)
     1.5  );
     1.6  
     1.7 -
     1.8 +create table lpit_issue_comment (
     1.9 +    commentid       serial          primary key,
    1.10 +    issueid         integer         not null references lpit_issue(issueid),
    1.11 +    userid          integer         references lpit_user(userid),
    1.12 +    created         timestamp       with time zone not null default now(),
    1.13 +    updated         timestamp       with time zone not null default now(),
    1.14 +    updatecount     integer         not null default 0,
    1.15 +    comment         text            not null
    1.16 +);
     2.1 --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Fri Oct 09 19:06:51 2020 +0200
     2.2 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Fri Oct 09 19:07:05 2020 +0200
     2.3 @@ -29,6 +29,7 @@
     2.4  package de.uapcore.lightpit.dao;
     2.5  
     2.6  import de.uapcore.lightpit.entities.Issue;
     2.7 +import de.uapcore.lightpit.entities.IssueComment;
     2.8  import de.uapcore.lightpit.entities.Project;
     2.9  import de.uapcore.lightpit.entities.Version;
    2.10  
    2.11 @@ -59,6 +60,24 @@
    2.12      List<Issue> list(Version version) throws SQLException;
    2.13  
    2.14      /**
    2.15 +     * Lists all comments for a specific issue in chronological order.
    2.16 +     *
    2.17 +     * @param issue the issue
    2.18 +     * @return the list of comments
    2.19 +     * @throws SQLException on any kind of SQL error
    2.20 +     */
    2.21 +    List<IssueComment> listComments(Issue issue) throws SQLException;
    2.22 +
    2.23 +    /**
    2.24 +     * Stores the specified comment in database.
    2.25 +     * This is an update-or-insert operation.
    2.26 +     *
    2.27 +     * @param comment the comment to save
    2.28 +     * @throws SQLException on any kind of SQL error
    2.29 +     */
    2.30 +    void saveComment(IssueComment comment) throws SQLException;
    2.31 +
    2.32 +    /**
    2.33       * Saves an instances to the database.
    2.34       * Implementations of this DAO must guarantee that the generated ID is stored in the instance.
    2.35       *
     3.1 --- a/src/main/java/de/uapcore/lightpit/dao/UserDao.java	Fri Oct 09 19:06:51 2020 +0200
     3.2 +++ b/src/main/java/de/uapcore/lightpit/dao/UserDao.java	Fri Oct 09 19:07:05 2020 +0200
     3.3 @@ -32,8 +32,19 @@
     3.4  
     3.5  import java.sql.SQLException;
     3.6  import java.util.List;
     3.7 +import java.util.Optional;
     3.8  
     3.9  public interface UserDao extends GenericDao<User> {
    3.10  
    3.11      List<User> list() throws SQLException;
    3.12 +
    3.13 +    /**
    3.14 +     * Tries to find a user by their username.
    3.15 +     * The search is case-insensitive.
    3.16 +     *
    3.17 +     * @param username the username
    3.18 +     * @return the user object or an empty optional if no such user exists
    3.19 +     * @throws SQLException
    3.20 +     */
    3.21 +    Optional<User> findByUsername(String username) throws SQLException;
    3.22  }
     4.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Fri Oct 09 19:06:51 2020 +0200
     4.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Fri Oct 09 19:07:05 2020 +0200
     4.3 @@ -47,6 +47,7 @@
     4.4      private final PreparedStatement affectedVersions, resolvedVersions;
     4.5      private final PreparedStatement clearAffected, clearResolved;
     4.6      private final PreparedStatement insertAffected, insertResolved;
     4.7 +    private final PreparedStatement insertComment, updateComment, listComments;
     4.8  
     4.9      public PGIssueDao(Connection connection) throws SQLException {
    4.10          list = connection.prepareStatement(
    4.11 @@ -107,9 +108,19 @@
    4.12          );
    4.13          clearResolved = connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?");
    4.14          insertResolved = connection.prepareStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)");
    4.15 +
    4.16 +        insertComment = connection.prepareStatement(
    4.17 +                "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)"
    4.18 +        );
    4.19 +        updateComment = connection.prepareStatement(
    4.20 +                "update lpit_issue_comment set comment = ?, updated = now(), updatecount = updatecount+1 where commentid = ?"
    4.21 +        );
    4.22 +        listComments = connection.prepareStatement(
    4.23 +                "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created"
    4.24 +        );
    4.25      }
    4.26  
    4.27 -    private User obtainAssignee(ResultSet result) throws SQLException {
    4.28 +    private User obtainUser(ResultSet result) throws SQLException {
    4.29          final int id = result.getInt("userid");
    4.30          if (id != 0) {
    4.31              final var user = new User(id);
    4.32 @@ -132,7 +143,7 @@
    4.33          issue.setCategory(IssueCategory.valueOf(result.getString("category")));
    4.34          issue.setSubject(result.getString("subject"));
    4.35          issue.setDescription(result.getString("description"));
    4.36 -        issue.setAssignee(obtainAssignee(result));
    4.37 +        issue.setAssignee(obtainUser(result));
    4.38          issue.setCreated(result.getTimestamp("created"));
    4.39          issue.setUpdated(result.getTimestamp("updated"));
    4.40          issue.setEta(result.getDate("eta"));
    4.41 @@ -252,4 +263,38 @@
    4.42          issue.setAffectedVersions(listVersions(affectedVersions, issue));
    4.43          issue.setResolvedVersions(listVersions(resolvedVersions, issue));
    4.44      }
    4.45 +
    4.46 +    @Override
    4.47 +    public List<IssueComment> listComments(Issue issue) throws SQLException {
    4.48 +        listComments.setInt(1, issue.getId());
    4.49 +        List<IssueComment> comments = new ArrayList<>();
    4.50 +        try (var result = listComments.executeQuery()) {
    4.51 +            while (result.next()) {
    4.52 +                final var comment = new IssueComment(result.getInt("commentid"), issue);
    4.53 +                comment.setCreated(result.getTimestamp("created"));
    4.54 +                comment.setUpdated(result.getTimestamp("updated"));
    4.55 +                comment.setUpdateCount(result.getInt("updatecount"));
    4.56 +                comment.setComment(result.getString("comment"));
    4.57 +                comment.setAuthor(obtainUser(result));
    4.58 +                comments.add(comment);
    4.59 +            }
    4.60 +        }
    4.61 +        return comments;
    4.62 +    }
    4.63 +
    4.64 +    @Override
    4.65 +    public void saveComment(IssueComment comment) throws SQLException {
    4.66 +        Objects.requireNonNull(comment.getComment());
    4.67 +        Objects.requireNonNull(comment.getIssue());
    4.68 +        if (comment.getId() >= 0) {
    4.69 +            updateComment.setString(1, comment.getComment());
    4.70 +            updateComment.setInt(2, comment.getId());
    4.71 +            updateComment.execute();
    4.72 +        } else {
    4.73 +            insertComment.setInt(1, comment.getIssue().getId());
    4.74 +            insertComment.setString(2, comment.getComment());
    4.75 +            setForeignKeyOrNull(insertComment, 3, comment.getAuthor(), User::getId);
    4.76 +            insertComment.execute();
    4.77 +        }
    4.78 +    }
    4.79  }
     5.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Fri Oct 09 19:06:51 2020 +0200
     5.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java	Fri Oct 09 19:07:05 2020 +0200
     5.3 @@ -38,12 +38,13 @@
     5.4  import java.util.ArrayList;
     5.5  import java.util.List;
     5.6  import java.util.Objects;
     5.7 +import java.util.Optional;
     5.8  
     5.9  import static de.uapcore.lightpit.dao.Functions.setStringOrNull;
    5.10  
    5.11  public final class PGUserDao implements UserDao {
    5.12  
    5.13 -    private final PreparedStatement insert, update, list, find;
    5.14 +    private final PreparedStatement insert, update, list, find, findByUsername;
    5.15  
    5.16      public PGUserDao(Connection connection) throws SQLException {
    5.17          list = connection.prepareStatement(
    5.18 @@ -54,6 +55,10 @@
    5.19                  "select userid, username, lastname, givenname, mail " +
    5.20                          "from lpit_user where userid = ? ");
    5.21  
    5.22 +        findByUsername = connection.prepareStatement(
    5.23 +                "select userid, username, lastname, givenname, mail " +
    5.24 +                        "from lpit_user where lower(username) = lower(?) ");
    5.25 +
    5.26          insert = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)");
    5.27          update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?");
    5.28      }
    5.29 @@ -111,4 +116,16 @@
    5.30              }
    5.31          }
    5.32      }
    5.33 +
    5.34 +    @Override
    5.35 +    public Optional<User> findByUsername(String username) throws SQLException {
    5.36 +        findByUsername.setString(1, username);
    5.37 +        try (var result = findByUsername.executeQuery()) {
    5.38 +            if (result.next()) {
    5.39 +                return Optional.of(mapColumns(result));
    5.40 +            } else {
    5.41 +                return Optional.empty();
    5.42 +            }
    5.43 +        }
    5.44 +    }
    5.45  }
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueComment.java	Fri Oct 09 19:07:05 2020 +0200
     6.3 @@ -0,0 +1,85 @@
     6.4 +package de.uapcore.lightpit.entities;
     6.5 +
     6.6 +import java.sql.Timestamp;
     6.7 +import java.time.Instant;
     6.8 +import java.util.Objects;
     6.9 +
    6.10 +public class IssueComment {
    6.11 +
    6.12 +    private final Issue issue;
    6.13 +    private final int commentid;
    6.14 +
    6.15 +    private User author;
    6.16 +    private String comment;
    6.17 +
    6.18 +    private Timestamp created = Timestamp.from(Instant.now());
    6.19 +    private Timestamp updated = Timestamp.from(Instant.now());
    6.20 +    private int updatecount = 0;
    6.21 +
    6.22 +
    6.23 +    public IssueComment(int id, Issue issue) {
    6.24 +        this.commentid = id;
    6.25 +        this.issue = issue;
    6.26 +    }
    6.27 +
    6.28 +    public Issue getIssue() {
    6.29 +        return issue;
    6.30 +    }
    6.31 +
    6.32 +    public int getId() {
    6.33 +        return commentid;
    6.34 +    }
    6.35 +
    6.36 +    public User getAuthor() {
    6.37 +        return author;
    6.38 +    }
    6.39 +
    6.40 +    public void setAuthor(User author) {
    6.41 +        this.author = author;
    6.42 +    }
    6.43 +
    6.44 +    public String getComment() {
    6.45 +        return comment;
    6.46 +    }
    6.47 +
    6.48 +    public void setComment(String comment) {
    6.49 +        this.comment = comment;
    6.50 +    }
    6.51 +
    6.52 +    public Timestamp getCreated() {
    6.53 +        return created;
    6.54 +    }
    6.55 +
    6.56 +    public void setCreated(Timestamp created) {
    6.57 +        this.created = created;
    6.58 +    }
    6.59 +
    6.60 +    public Timestamp getUpdated() {
    6.61 +        return updated;
    6.62 +    }
    6.63 +
    6.64 +    public void setUpdated(Timestamp updated) {
    6.65 +        this.updated = updated;
    6.66 +    }
    6.67 +
    6.68 +    public int getUpdateCount() {
    6.69 +        return updatecount;
    6.70 +    }
    6.71 +
    6.72 +    public void setUpdateCount(int updatecount) {
    6.73 +        this.updatecount = updatecount;
    6.74 +    }
    6.75 +
    6.76 +    @Override
    6.77 +    public boolean equals(Object o) {
    6.78 +        if (this == o) return true;
    6.79 +        if (o == null || getClass() != o.getClass()) return false;
    6.80 +        IssueComment that = (IssueComment) o;
    6.81 +        return commentid == that.commentid;
    6.82 +    }
    6.83 +
    6.84 +    @Override
    6.85 +    public int hashCode() {
    6.86 +        return Objects.hash(commentid);
    6.87 +    }
    6.88 +}
     7.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Fri Oct 09 19:06:51 2020 +0200
     7.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Fri Oct 09 19:07:05 2020 +0200
     7.3 @@ -282,11 +282,15 @@
     7.4          viewModel.setIssue(issue);
     7.5          viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions());
     7.6          viewModel.setUsers(dao.getUserDao().list());
     7.7 +        if (issue.getId() >= 0) {
     7.8 +            viewModel.setComments(dao.getIssueDao().listComments(issue));
     7.9 +        }
    7.10      }
    7.11  
    7.12      @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
    7.13      public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
    7.14          final var viewModel = new IssueEditView();
    7.15 +        populate(viewModel, req, dao);
    7.16  
    7.17          final var issueParam = getParameter(req, Integer.class, "issue");
    7.18          if (issueParam.isPresent()) {
    7.19 @@ -294,10 +298,8 @@
    7.20              final var issue = issueDao.find(issueParam.get());
    7.21              issueDao.joinVersionInformation(issue);
    7.22              req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId());
    7.23 -            populate(viewModel, req, dao);
    7.24              configure(viewModel, issue, dao);
    7.25          } else {
    7.26 -            populate(viewModel, req, dao);
    7.27              configure(viewModel, new Issue(-1), dao);
    7.28          }
    7.29  
    7.30 @@ -305,7 +307,7 @@
    7.31      }
    7.32  
    7.33      @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
    7.34 -    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
    7.35 +    public ResponseType commitIssue(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
    7.36          Issue issue = new Issue(-1);
    7.37          try {
    7.38              issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
    7.39 @@ -335,16 +337,57 @@
    7.40              // specifying the issue parameter keeps the edited issue as menu item
    7.41              setRedirectLocation(req, "./projects/view?pid=" + issue.getProject().getId());
    7.42              setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
    7.43 +
    7.44 +            return ResponseType.HTML;
    7.45          } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
    7.46              // TODO: set request attribute with error text
    7.47              LOG.warn("Form validation failure: {}", ex.getMessage());
    7.48              LOG.debug("Details:", ex);
    7.49              final var viewModel = new IssueEditView();
    7.50 +            populate(viewModel, req, dao);
    7.51              configure(viewModel, issue, dao);
    7.52              // TODO: set Error Text
    7.53              return forwardView(req, viewModel, "issue-form");
    7.54          }
    7.55 +    }
    7.56  
    7.57 -        return ResponseType.HTML;
    7.58 +    @RequestMapping(requestPath = "issues/comment", method = HttpMethod.POST)
    7.59 +    public ResponseType commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
    7.60 +        final var issueIdParam = getParameter(req, Integer.class, "issueid");
    7.61 +        if (issueIdParam.isEmpty()) {
    7.62 +            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form.");
    7.63 +            return ResponseType.NONE;
    7.64 +        }
    7.65 +        final var issue = new Issue(issueIdParam.get());
    7.66 +        try {
    7.67 +            final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue);
    7.68 +            issueComment.setComment(getParameter(req, String.class, "comment").orElse(""));
    7.69 +
    7.70 +            if (issueComment.getComment().isBlank()) {
    7.71 +                throw new IllegalArgumentException("comment.null");
    7.72 +            }
    7.73 +
    7.74 +            LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId());
    7.75 +            if (req.getRemoteUser() != null) {
    7.76 +                dao.getUserDao().findByUsername(req.getRemoteUser()).ifPresent(issueComment::setAuthor);
    7.77 +            }
    7.78 +
    7.79 +            dao.getIssueDao().saveComment(issueComment);
    7.80 +
    7.81 +            // specifying the issue parameter keeps the edited issue as menu item
    7.82 +            setRedirectLocation(req, "./projects/issues/edit?issue=" + issue.getId());
    7.83 +            setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
    7.84 +
    7.85 +            return ResponseType.HTML;
    7.86 +        } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
    7.87 +            // TODO: set request attribute with error text
    7.88 +            LOG.warn("Form validation failure: {}", ex.getMessage());
    7.89 +            LOG.debug("Details:", ex);
    7.90 +            final var viewModel = new IssueEditView();
    7.91 +            populate(viewModel, req, dao);
    7.92 +            configure(viewModel, issue, dao);
    7.93 +            // TODO: set Error Text
    7.94 +            return forwardView(req, viewModel, "issue-form");
    7.95 +        }
    7.96      }
    7.97  }
     8.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Fri Oct 09 19:06:51 2020 +0200
     8.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Fri Oct 09 19:07:05 2020 +0200
     8.3 @@ -11,6 +11,7 @@
     8.4      private Set<Version> versionsUpcoming = new HashSet<>();
     8.5      private Set<Version> versionsRecent = new HashSet<>();
     8.6      private List<User> users;
     8.7 +    private List<IssueComment> comments;
     8.8  
     8.9      public void setIssue(Issue issue) {
    8.10          this.issue = issue;
    8.11 @@ -66,4 +67,12 @@
    8.12      public IssueCategory[] getIssueCategory() {
    8.13          return IssueCategory.values();
    8.14      }
    8.15 +
    8.16 +    public List<IssueComment> getComments() {
    8.17 +        return comments;
    8.18 +    }
    8.19 +
    8.20 +    public void setComments(List<IssueComment> comments) {
    8.21 +        this.comments = comments;
    8.22 +    }
    8.23  }
     9.1 --- a/src/main/resources/localization/projects.properties	Fri Oct 09 19:06:51 2020 +0200
     9.2 +++ b/src/main/resources/localization/projects.properties	Fri Oct 09 19:07:05 2020 +0200
     9.3 @@ -28,6 +28,7 @@
     9.4  button.version.edit=Edit Version
     9.5  button.issue.create=New Issue
     9.6  button.issue.all=All Issues
     9.7 +button.comment=Comment
     9.8  
     9.9  no-projects=Welcome to LightPIT. Start off by creating a new project!
    9.10  
    9.11 @@ -95,3 +96,6 @@
    9.12  issue.status.Rejected=Rejected
    9.13  issue.status.Withdrawn=Withdrawn
    9.14  issue.status.Duplicate=Duplicate
    9.15 +
    9.16 +issue.comments=Comments
    9.17 +issue.comments.anonauthor=Anonymous Author
    10.1 --- a/src/main/resources/localization/projects_de.properties	Fri Oct 09 19:06:51 2020 +0200
    10.2 +++ b/src/main/resources/localization/projects_de.properties	Fri Oct 09 19:07:05 2020 +0200
    10.3 @@ -28,6 +28,7 @@
    10.4  button.version.edit=Version Bearbeiten
    10.5  button.issue.create=Neuer Vorgang
    10.6  button.issue.all=Alle Vorg\u00e4nge
    10.7 +button.comment=Kommentieren
    10.8  
    10.9  no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
   10.10  
   10.11 @@ -94,3 +95,6 @@
   10.12  issue.status.Rejected=Zur\u00fcckgewiesen
   10.13  issue.status.Withdrawn=Zur\u00fcckgezogen
   10.14  issue.status.Duplicate=Duplikat
   10.15 +
   10.16 +issue.comments=Kommentare
   10.17 +issue.comments.anonauthor=Anonymer Autor
   10.18 \ No newline at end of file
    11.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Fri Oct 09 19:06:51 2020 +0200
    11.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Fri Oct 09 19:07:05 2020 +0200
    11.3 @@ -32,10 +32,10 @@
    11.4  <c:set var="issue" scope="page" value="${viewmodel.issue}" />
    11.5  
    11.6  <form action="./projects/issues/commit" method="post">
    11.7 -    <table class="formtable">
    11.8 +    <table class="formtable fullwidth">
    11.9          <colgroup>
   11.10              <col>
   11.11 -            <col style="width: 75ch">
   11.12 +            <col style="width: 100%">
   11.13          </colgroup>
   11.14          <tbody>
   11.15          <c:if test="${viewmodel.issue.id ge 0}">
   11.16 @@ -169,3 +169,46 @@
   11.17          </tfoot>
   11.18      </table>
   11.19  </form>
   11.20 +<hr class="comments-separator"/>
   11.21 +<h2><fmt:message key="issue.comments"/></h2>
   11.22 +<c:if test="${viewmodel.issue.id ge 0}">
   11.23 +<form id="comment-form" action="./projects/issues/comment" method="post">
   11.24 +    <table class="formtable fullwidth">
   11.25 +        <tbody>
   11.26 +            <tr>
   11.27 +                <td><textarea rows="5" name="comment"></textarea></td>
   11.28 +            </tr>
   11.29 +        </tbody>
   11.30 +        <tfoot>
   11.31 +            <tr>
   11.32 +                <td>
   11.33 +                    <input type="hidden" name="issueid" value="${issue.id}"/>
   11.34 +                    <button type="submit"><fmt:message key="button.comment"/></button>
   11.35 +                </td>
   11.36 +            </tr>
   11.37 +        </tfoot>
   11.38 +    </table>
   11.39 +</form>
   11.40 +    <c:forEach var="comment" items="${viewmodel.comments}">
   11.41 +        <div class="comment">
   11.42 +            <div class="caption">
   11.43 +                <c:if test="${not empty comment.author}">
   11.44 +                    <c:out value="${comment.author.displayname}"/>
   11.45 +                </c:if>
   11.46 +                <c:if test="${empty comment.author}">
   11.47 +                    <fmt:message key="issue.comments.anonauthor"/>
   11.48 +                </c:if>
   11.49 +            </div>
   11.50 +            <div class="smalltext">
   11.51 +                <fmt:formatDate type="BOTH" value="${comment.created}" />
   11.52 +                <c:if test="${comment.updateCount gt 0}">
   11.53 +                    <!-- TODO: update count -->
   11.54 +                </c:if>
   11.55 +            </div>
   11.56 +            <div class="medskip">
   11.57 +                <c:out value="${comment.comment}"/>
   11.58 +            </div>
   11.59 +        </div>
   11.60 +    </c:forEach>
   11.61 +</c:if>
   11.62 +
    12.1 --- a/src/main/webapp/lightpit.css	Fri Oct 09 19:06:51 2020 +0200
    12.2 +++ b/src/main/webapp/lightpit.css	Fri Oct 09 19:07:05 2020 +0200
    12.3 @@ -65,7 +65,7 @@
    12.4      flex-flow: column;
    12.5      position: fixed;
    12.6      height: 100%;
    12.7 -    width: 30ch;
    12.8 +    width: 40ch; /* adjust with sidebar-spacing.margin-left */
    12.9      padding-top: 2.25rem;
   12.10      color: #3060f8;
   12.11      border-image-source: linear-gradient(to bottom, #606060, rgba(60, 60, 60, .25));
   12.12 @@ -75,7 +75,7 @@
   12.13  }
   12.14  
   12.15  #content-area.sidebar-spacing {
   12.16 -    margin-left: 30ch;
   12.17 +    margin-left: 40ch; /* adjust with sideMenu.width */
   12.18  }
   12.19  
   12.20  #mainMenu {
   12.21 @@ -197,6 +197,7 @@
   12.22  
   12.23  table.formtable tbody td > * {
   12.24      width: 100%;
   12.25 +    box-sizing: border-box;
   12.26  }
   12.27  
   12.28  table.formtable input[type=date] {
    13.1 --- a/src/main/webapp/projects.css	Fri Oct 09 19:06:51 2020 +0200
    13.2 +++ b/src/main/webapp/projects.css	Fri Oct 09 19:07:05 2020 +0200
    13.3 @@ -137,3 +137,15 @@
    13.4      color: lightgray;
    13.5      background: darkgray;
    13.6  }
    13.7 +
    13.8 +hr.comments-separator {
    13.9 +    border-image-source: linear-gradient(to right, rgba(60, 60, 60, .1), rgba(96, 96, 96, 1), rgba(60, 60, 60, .1));
   13.10 +    border-image-slice: 1;
   13.11 +    border-width: 1pt;
   13.12 +    border-style: none;
   13.13 +    border-top-style: solid;
   13.14 +}
   13.15 +
   13.16 +div.comment {
   13.17 +    margin-bottom: 1.25em;
   13.18 +}

mercurial