~jan0sch/smederee
Showing details for patch bc961e8622946f8c0370cc0a41e7286a62398870.
diff -rN -u old-smederee/modules/hub/src/main/resources/messages.properties new-smederee/modules/hub/src/main/resources/messages.properties --- old-smederee/modules/hub/src/main/resources/messages.properties 2025-01-16 12:55:19.025847706 +0000 +++ new-smederee/modules/hub/src/main/resources/messages.properties 2025-01-16 12:55:19.025847706 +0000 @@ -321,3 +321,16 @@ project.tickets.view.title=Tickets project.tickets.list.empty=There are no tickets defined for this project. project.tickets.list.title={0} tickets found. + +ticket.status.confirmed=Confirmed +ticket.status.inprogress=In progress +ticket.status.pending=Pending +ticket.status.resolved=Resolved +ticket.status.resolved.bydesign=By design +ticket.status.resolved.closed=Closed +ticket.status.resolved.duplicate=Duplicate +ticket.status.resolved.fixed=Fixed +ticket.status.resolved.implemented=Implemented +ticket.status.resolved.invalid=Invalid +ticket.status.resolved.wontfix=Won''t fix +ticket.status.submitted=Submitted diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala 2025-01-16 12:55:19.025847706 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/MarkdownRenderer.scala 2025-01-16 12:55:19.025847706 +0000 @@ -20,6 +20,7 @@ import java.util.Locale import cats.syntax.all._ +import de.smederee.tickets.TicketContent import org.commonmark.ext.gfm.tables.TablesExtension import org.commonmark.ext.heading.anchor.HeadingAnchorExtension import org.commonmark.ext.task.list.items.TaskListItemsExtension @@ -40,6 +41,25 @@ private val MarkdownExtensions = List(TablesExtension.create(), HeadingAnchorExtension.create(), TaskListItemsExtension.create()) + /** Render the given ticket content using markdown. + * + * @param markdownSource + * The content of a ticket description. + * @return + * A string containing the rendered markdown (HTML). + */ + def renderTicketContent(markdownSource: TicketContent): String = { + val parser = Parser.builder().extensions(MarkdownExtensions.asJava).build() + val markdown = parser.parse(markdownSource.toString) + val renderer = HtmlRenderer + .builder() + .escapeHtml(true) + .extensions(MarkdownExtensions.asJava) + .sanitizeUrls(true) + .build() + renderer.render(markdown) + } + /** Render the given markdown sources and adjust all relative links by prefixing them with the path for the repostiory * file browsing (`repo-name/files`). * diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala 2025-01-16 12:55:19.025847706 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala 2025-01-16 12:55:19.025847706 +0000 @@ -36,6 +36,7 @@ import org.http4s.headers.Location import org.http4s.twirl.TwirlInstances._ import org.slf4j.LoggerFactory +import de.smederee.hub.MarkdownRenderer /** Routes for managing tickets. * @@ -101,6 +102,59 @@ } } yield projectAndId + /** Logic for rendering a detail page for a single ticket. + * + * @param csrf + * An optional CSRF-Token that shall be used. + * @param user + * An optional user account for whom the list of tickets shall be rendered. + * @param projectOwnerName + * The username of the account who owns the project. + * @param projectName + * The name of the project. + * @param ticketNumber + * The number of the ticket that shall be rendered. + * @return + * An HTTP response containing the rendered HTML. + */ + private def doShowTicket(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)( + projectName: ProjectName + )(ticketNumber: TicketNumber): F[Response[F]] = + for { + _ <- Sync[F].delay(log.debug(s"doShowTicket: $csrf, $user, $projectOwnerName, $projectName, $ticketNumber")) + language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en"))) + projectAndId <- loadProject(user)(projectOwnerName, projectName) + ticket <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber)) + resp <- (projectAndId, ticket.getOrElse(None)) match { + case (Some((project, projectId)), Some(ticket)) => + for { + projectBaseUri <- Sync[F].delay( + linkConfig.createFullUri( + Uri(path = + Uri.Path( + Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString)) + ) + ) + ) + ) + renderedTicketContent <- Sync[F].delay(ticket.content.map(MarkdownRenderer.renderTicketContent)) + resp <- Ok( + views.html.showTicket(lang = language)( + projectBaseUri.addSegment("tickets"), + csrf, + ticket, + renderedTicketContent, + projectBaseUri, + ticket.title.toString.some, + user, + project + ) + ) + } yield resp + case _ => NotFound() + } + } yield resp + /** Logic for rendering a list of all tickets for a project and optionally management functionality. * * @param filter @@ -265,6 +319,16 @@ } yield resp } + private val showTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of { + case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter( + projectName + ) / "tickets" / TicketNumberPathParameter(ticketNumber) as user => + for { + csrf <- Sync[F].delay(ar.req.getCsrfToken) + resp <- doShowTicket(csrf)(user.some)(projectOwnerName)(projectName)(ticketNumber) + } yield resp + } + private val showTicketsPage: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter( projectName @@ -285,7 +349,7 @@ } yield resp } - val protectedRoutes = addTicket <+> showCreateTicketPage <+> showTicketsPage + val protectedRoutes = addTicket <+> showCreateTicketPage <+> showTicketPage <+> showTicketsPage val routes = showTicketsForGuests diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDateTime.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDateTime.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDateTime.scala.html 2025-01-16 12:55:19.025847706 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDateTime.scala.html 2025-01-16 12:55:19.029847715 +0000 @@ -3,4 +3,4 @@ @import java.util.Locale @(timestamp: OffsetDateTime, style: FormatStyle = FormatStyle.MEDIUM)(implicit locale: Locale) -(@DateTimeFormatter.ofLocalizedDateTime(style).withLocale(locale).format(timestamp)) +@DateTimeFormatter.ofLocalizedDateTime(style).withLocale(locale).format(timestamp) diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketStatus.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketStatus.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketStatus.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketStatus.scala.html 2025-01-16 12:55:19.029847715 +0000 @@ -0,0 +1,13 @@ +@import java.util.Locale +@import de.smederee.tickets.Ticket + +@(ticket: Ticket)(implicit locale: Locale) +@defining(ticket.status.toString.toLowerCase(Locale.ROOT)) { status => + @if(ticket.resolution.nonEmpty) { + @for(resolution <- ticket.resolution) { + @Messages(s"ticket.status.${status}") (@Messages(s"ticket.status.${status}.${resolution.toString.toLowerCase(Locale.ROOT)}")) + } + } else { + @Messages(s"ticket.status.${status}") + } +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketSubmitter.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketSubmitter.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketSubmitter.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatTicketSubmitter.scala.html 2025-01-16 12:55:19.029847715 +0000 @@ -0,0 +1,7 @@ +@import java.util.Locale +@import de.smederee.tickets.Ticket + +@(baseUri: Uri)(ticket: Ticket)(implicit locale: Locale) +@for(submitter <- ticket.submitter) { + <a href="@baseUri.addSegment(s"~${submitter.name}")">@submitter.name</a> +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html 2025-01-16 12:55:19.029847715 +0000 @@ -0,0 +1,49 @@ +@import de.smederee.hub.Account +@import de.smederee.hub.views.html.main +@import de.smederee.tickets._ +@import de.smederee.tickets.views.html.format._ + +@(baseUri: Uri = Uri(path = Uri.Path.Root), + lang: LanguageCode = LanguageCode("en") +)(action: Uri, + csrf: Option[CsrfToken] = None, + ticket: Ticket, + renderedTicketContent: Option[String], + projectBaseUri: Uri, + title: Option[String] = None, + user: Option[Account], + project: Project +) +@main(baseUri, lang)()(csrf, title, user) { +@defining(lang.toLocale) { implicit locale => +<div class="content"> + <div class="pure-g"> + <div class="pure-u-1"> + <div class="l-box-left-right"> + <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <div class="project-summary-description">@ticket.number created by @formatTicketSubmitter(baseUri)(ticket) at @formatDateTime(ticket.createdAt)</div> + </div> + </div> + </div> + <div class="pure-g @if(ticket.status === TicketStatus.Resolved){ticket-resolved}else{ticket}"> + <div class="pure-u-18-24 pure-u-md-18-24"> + <div class="l-box"> + <h1>@ticket.title</h1> + <div class="ticket-content">@Html(renderedTicketContent)</div> + </div> + </div> + <div class="pure-u-5-24 pure-u-md-5-24"> + <div class="l-box"> + <div class="pure-g"> + <div class="pure-u-2-5">Status</div><div class="pure-u-3-5">@formatTicketStatus(ticket)</div> + <div class="pure-u-2-5">Assigned</div><div class="pure-u-3-5">...</div> + <div class="pure-u-2-5">Reported</div><div class="pure-u-3-5">@formatTicketSubmitter(baseUri)(ticket) at @formatDateTime(ticket.createdAt)</div> + <div class="pure-u-2-5">Updated</div><div class="pure-u-3-5">@formatDateTime(ticket.updatedAt)</div> + </div> + </div> + </div> + </div> +</div> +} +} diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala --- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala 2025-01-16 12:55:19.025847706 +0000 +++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala 2025-01-16 12:55:19.029847715 +0000 @@ -130,6 +130,12 @@ } } +/** Extractor to retrieve a TicketNumber from a path parameter. + */ +object TicketNumberPathParameter { + def unapply(str: String): Option[TicketNumber] = Option(str).flatMap(TicketNumber.fromString) +} + /** Possible states of a ticket which shall model the life cycle from ticket creation until it is closed. To keep things * simple we do not model the kind of a resolution (e.g. fixed, won't fix or so) for a ticket. */