~jan0sch/smederee

Showing details for patch 7c6959c534e27d410c18cdcab64fca32b82ee2b5.
2023-05-28 (Sun), 1:05 PM - Jens Grassel - 7c6959c534e27d410c18cdcab64fca32b82ee2b5

Tickets: Path changes, ticket details and milestone rendering.

- rename render function in `MarkdownRenderer` to be more generic
- adjust paths for labels and milestones to be consistent with tickets
- add guest routes
- remove parentheses around date in `formatDate`
- add detail page for a milestone
- add translations
- fix some CSS glitches
Summary of changes
1 files added
  • modules/hub/src/main/twirl/de/smederee/tickets/views/showMilestone.scala.html
11 files modified with 172 lines added and 45 lines removed
  • modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala with 3 added and 3 removed lines
  • modules/hub/src/main/resources/assets/css/main.css with 15 added and 0 removed lines
  • modules/hub/src/main/resources/messages.properties with 8 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala with 5 added and 5 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala with 96 added and 8 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala with 1 added and 3 removed lines
  • modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html with 2 added and 2 removed lines
  • modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html with 10 added and 10 removed lines
  • modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html with 7 added and 3 removed lines
  • modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDate.scala.html with 1 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html with 24 added and 10 removed lines
diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala
--- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala	2025-01-16 07:42:48.524310723 +0000
+++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala	2025-01-16 07:42:48.528310730 +0000
@@ -40,14 +40,14 @@
   private val MarkdownExtensions =
     List(TablesExtension.create(), HeadingAnchorExtension.create(), TaskListItemsExtension.create())
 
-  /** Render the given ticket content using markdown.
+  /** Render the given markdown content into HTML.
     *
     * @param markdownSource
-    *   The content of a ticket description.
+    *   Markdown source code that shall be rendered.
     * @return
     *   A string containing the rendered markdown (HTML).
     */
-  def renderTicketContent(markdownSource: String): String = {
+  def render(markdownSource: String): String = {
     val parser   = Parser.builder().extensions(MarkdownExtensions.asJava).build()
     val markdown = parser.parse(markdownSource)
     val renderer = HtmlRenderer
diff -rN -u old-smederee/modules/hub/src/main/resources/assets/css/main.css new-smederee/modules/hub/src/main/resources/assets/css/main.css
--- old-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-01-16 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-01-16 07:42:48.528310730 +0000
@@ -81,6 +81,14 @@
   border-bottom: 1px solid rgba(0,0,0,0.1);
 }
 
+.s-box {
+  padding: 0.25em;
+}
+
+.s-box-left-right {
+  padding: 0em 0.25em 0em 0.25em;
+}
+
 .is-center {
   text-align: center;
 }
@@ -220,6 +228,12 @@
   padding: 0.25em 0 0 0;
 }
 
+.milestone-sidebar {
+  background: var(--background2);
+  line-height: 1.5em;
+  word-break: break-word;
+}
+
 .milestone-title {
   margin: auto;
   overflow: overlay;
@@ -347,6 +361,7 @@
 .ticket-sidebar {
   background: var(--background2);
   line-height: 1.5em;
+  word-break: break-word;
 }
 
 .todo-default {
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 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/resources/messages.properties	2025-01-16 07:42:48.528310730 +0000
@@ -305,6 +305,8 @@
 form.ticket.title.placeholder=Short concise summary what the ticket is about.
 form.ticket.title=Title
 
+milestone.due-date=Due date
+
 project.label.edit.link=Edit
 project.label.edit.title=Edit label ''{0}''.
 project.labels.add.title=Add a new label.
@@ -318,6 +320,7 @@
 project.menu.tickets=Tickets
 project.milestone.edit.link=Edit
 project.milestone.edit.title=Edit milestone ''{0}''.
+project.milestone.title=Milestone {0}
 project.milestones.add.title=Add a new milestone.
 project.milestones.edit.title=Edit project milestones.
 project.milestones.list.empty=There are no milestones defined for this project.
@@ -331,6 +334,10 @@
 project.tickets.list.empty=There are no tickets defined for this project.
 project.tickets.list.title={0} tickets found.
 
+ticket.assigned=Assigned to
+ticket.reported=Reported by
+ticket.milestones=Milestones
+ticket.status=Status
 ticket.status.confirmed=Confirmed
 ticket.status.inprogress=In progress
 ticket.status.pending=Pending
@@ -343,3 +350,4 @@
 ticket.status.resolved.invalid=Invalid
 ticket.status.resolved.wontfix=Won''t fix
 ticket.status.submitted=Submitted
+ticket.updated=Last updated at
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala	2025-01-16 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala	2025-01-16 07:42:48.528310730 +0000
@@ -237,7 +237,7 @@
   private val deleteLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "label" / LabelNamePathParameter(labelName) / "delete" as user =>
+        ) / "labels" / LabelNamePathParameter(labelName) / "delete" as user =>
       ar.req.decodeStrict[F, UrlForm] { urlForm =>
         for {
           csrf         <- Sync[F].delay(ar.req.getCsrfToken)
@@ -304,7 +304,7 @@
   private val editLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "label" / LabelNamePathParameter(labelName) as user =>
+        ) / "labels" / LabelNamePathParameter(labelName) as user =>
       ar.req.decodeStrict[F, UrlForm] { urlForm =>
         val response =
           for {
@@ -330,7 +330,7 @@
                       )
                     )
                   )
-                  actionUri <- Sync[F].delay(projectBaseUri.addSegment("label").addSegment(label.name.toString))
+                  actionUri <- Sync[F].delay(projectBaseUri.addSegment("labels").addSegment(label.name.toString))
                   formData  <- Sync[F].delay(urlForm.values)
 //                labelIdMatches <- Sync[F].delay(
 //                  formData
@@ -415,7 +415,7 @@
   private val showEditLabelForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "label" / LabelNamePathParameter(labelName) / "edit" as user =>
+        ) / "labels" / LabelNamePathParameter(labelName) / "edit" as user =>
       for {
         csrf         <- Sync[F].delay(ar.req.getCsrfToken)
         projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
@@ -435,7 +435,7 @@
                   )
                 )
               )
-              actionUri <- Sync[F].delay(projectBaseUri.addSegment("label").addSegment(label.name.toString))
+              actionUri <- Sync[F].delay(projectBaseUri.addSegment("labels").addSegment(label.name.toString))
               formData  <- Sync[F].delay(LabelForm.fromLabel(label))
               resp <- Ok(
                 views.html
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala	2025-01-16 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala	2025-01-16 07:42:48.528310730 +0000
@@ -33,6 +33,7 @@
 import org.http4s.dsl.Http4sDsl
 import org.http4s.headers.Location
 import org.http4s.twirl.TwirlInstances._
+import org.slf4j.LoggerFactory
 
 /** Routes for managing milestones (basically CRUD functionality).
   *
@@ -50,12 +51,71 @@
     milestoneRepo: MilestoneRepository[F],
     projectRepo: ProjectRepository[F]
 ) extends Http4sDsl[F] {
+  private val log = LoggerFactory.getLogger(getClass)
 
   given CsrfProtectionConfiguration = configuration.csrfProtection
 
   private val linkToHubService = configuration.hub.baseUri
   private val linkConfig       = configuration.externalUrl
 
+  /** Logic for rendering a detail page for a single milestone.
+    *
+    * @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 milestoneTitle
+    *   The title of the milestone that shall be rendered.
+    * @return
+    *   An HTTP response containing the rendered HTML.
+    */
+  private def doShowMilestone(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)(
+      projectName: ProjectName
+  )(milestoneTitle: MilestoneTitle): F[Response[F]] =
+    for {
+      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+      projectAndId <- loadProject(user)(projectOwnerName, projectName)
+      milestone <- projectAndId match {
+        case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
+        case _                    => Sync[F].delay(None)
+      }
+      resp <- (projectAndId, milestone) match {
+        case (Some(project, _), Some(milestone)) =>
+          for {
+            projectBaseUri <- Sync[F].delay(
+              linkConfig.createFullUri(
+                Uri(path =
+                  Uri.Path(
+                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
+                  )
+                )
+              )
+            )
+            actionUri <- Sync[F].delay(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
+            renderedDescription <- Sync[F].delay(milestone.description.map(_.toString).map(MarkdownRenderer.render))
+            resp <- Ok(
+              views.html.showMilestone(lang = language)(
+                actionUri,
+                csrf,
+                linkToHubService,
+                milestone,
+                renderedDescription,
+                projectBaseUri,
+                Nil,
+                s"Milestone ${milestone.title}".some,
+                user,
+                project
+              )
+            )
+          } yield resp
+        case _ => NotFound()
+      }
+    } yield resp
+
   /** Logic for rendering a list of all milestones for a project and optionally management functionality.
     *
     * @param csrf
@@ -228,7 +288,7 @@
   private val deleteMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "milestone" / MilestoneTitlePathParameter(milestoneTitle) / "delete" as user =>
+        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "delete" as user =>
       ar.req.decodeStrict[F, UrlForm] { urlForm =>
         for {
           csrf         <- Sync[F].delay(ar.req.getCsrfToken)
@@ -294,7 +354,7 @@
   private val editMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "milestone" / MilestoneTitlePathParameter(milestoneTitle) as user =>
+        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
       ar.req.decodeStrict[F, UrlForm] { urlForm =>
         for {
           csrf         <- Sync[F].delay(ar.req.getCsrfToken)
@@ -320,7 +380,7 @@
                   )
                 )
                 actionUri <- Sync[F].delay(
-                  projectBaseUri.addSegment("milestone").addSegment(milestone.title.toString)
+                  projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)
                 )
                 formData <- Sync[F].delay(urlForm.values)
                 milestoneIdMatches <- Sync[F].delay(
@@ -399,7 +459,7 @@
   private val showEditMilestoneForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
-        ) / "milestone" / MilestoneTitlePathParameter(milestoneTitle) / "edit" as user =>
+        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "edit" as user =>
       for {
         csrf         <- Sync[F].delay(ar.req.getCsrfToken)
         language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
@@ -420,7 +480,7 @@
                   )
                 )
               )
-              actionUri <- Sync[F].delay(projectBaseUri.addSegment("milestone").addSegment(milestone.title.toString))
+              actionUri <- Sync[F].delay(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
               formData  <- Sync[F].delay(MilestoneForm.fromMilestone(milestone))
               resp <- Ok(
                 views.html.editMilestone(lang = language)(
@@ -452,6 +512,34 @@
       } yield resp
   }
 
+  private val showMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+          projectName
+        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
+      val response = for {
+        csrf <- Sync[F].delay(ar.req.getCsrfToken)
+        resp <- doShowMilestone(csrf)(user.some)(projectOwnerName)(projectName)(milestoneTitle)
+      } yield resp
+      response.recoverWith { error =>
+        log.error("Internal Server Error", error)
+        for {
+          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+        } yield resp
+      }
+  }
+
+  private val showMilestoneForGuests: HttpRoutes[F] = HttpRoutes.of {
+    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+          projectName
+        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) =>
+      for {
+        csrf <- Sync[F].delay(req.getCsrfToken)
+        resp <- doShowMilestone(csrf)(None)(projectOwnerName)(projectName)(milestoneTitle)
+      } yield resp
+  }
+
   private val showMilestonesForGuests: HttpRoutes[F] = HttpRoutes.of {
     case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
           projectName
@@ -463,8 +551,8 @@
   }
 
   val protectedRoutes =
-    addMilestone <+> deleteMilestone <+> editMilestone <+> showEditMilestoneForm <+> showEditMilestonesPage
+    addMilestone <+> deleteMilestone <+> editMilestone <+> showEditMilestoneForm <+> showEditMilestonesPage <+> showMilestone
 
-  val routes = showMilestonesForGuests
+  val routes = showMilestoneForGuests <+> showMilestonesForGuests
 
 }
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 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala	2025-01-16 07:42:48.528310730 +0000
@@ -142,9 +142,7 @@
                 )
               )
             )
-            renderedTicketContent <- Sync[F].delay(
-              ticket.content.map(_.toString).map(MarkdownRenderer.renderTicketContent)
-            )
+            renderedTicketContent <- Sync[F].delay(ticket.content.map(_.toString).map(MarkdownRenderer.render))
             resp <- Ok(
               views.html.showTicket(lang = language)(
                 projectBaseUri.addSegment("tickets"),
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html	2025-01-16 07:42:48.524310723 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -101,12 +101,12 @@
                   <div class="pure-u-8-24 label-description" style="height: @{lineHeight}px; line-height: @{lineHeight}px;">@label.description</div>
                   <div class="pure-u-2-24" style="height: @{lineHeight}px; line-height: @{lineHeight}px; margin: auto;">
                     @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) {
-                    <a class="pure-button" href="@projectBaseUri.addSegment("label").addSegment(label.name.toString).addSegment("edit")" title="@Messages("project.label.edit.title", label.name)">@Messages("project.label.edit.link")</a>
+                    <a class="pure-button" href="@projectBaseUri.addSegment("labels").addSegment(label.name.toString).addSegment("edit")" title="@Messages("project.label.edit.title", label.name)">@Messages("project.label.edit.link")</a>
                     } else { }
                   </div>
                   <div class="pure-u-8-24 label-form" style="height: @{lineHeight}px; line-height: @{lineHeight}px; padding-left: 1em;">
                     @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) {
-                    <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("label").addSegment(label.name.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8">
+                    <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("labels").addSegment(label.name.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8">
                       <fieldset>
                         <input type="hidden" id="@fieldId-@label.name" name="@fieldId" readonly="" value="@label.id">
                         <input type="hidden" id="@fieldName-@label.name" name="@fieldName" readonly="" value="@label.name">
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html	2025-01-16 07:42:48.528310730 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -50,25 +50,25 @@
           }
         </div>
         <div class="edit-milestones-form">
-          <form action="@{linkToHubService.addPath(action.path.toString)}" class="pure-form pure-form-aligned" method="POST" accept-charset="UTF-8">
+          <form action="@{linkToHubService.addPath(action.path.toString)}" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8">
             <fieldset>
               <input type="hidden" id="@fieldId" name="@fieldId" readonly="" value="@milestone.id">
               <div class="pure-control-group">
-                <milestone for="@{fieldTitle}">@Messages("form.milestone.title")</milestone>
-                <input id="@{fieldTitle}" name="@{fieldTitle}" maxlength="40" placeholder="@Messages("form.milestone.title.placeholder")" required="" type="text" value="@{formData(fieldTitle).headOption}">
-                <span class="pure-form-message" id="@{fieldTitle}.help" style="margin-left: 13em;">@Messages("form.milestone.title.help")</span>
+                <label for="@{fieldTitle}">@Messages("form.milestone.title")</label>
+                <input class="pure-input-3-4" id="@{fieldTitle}" name="@{fieldTitle}" maxlength="40" placeholder="@Messages("form.milestone.title.placeholder")" required="" type="text" value="@{formData.get(fieldTitle).headOption}">
+                <span class="pure-form-message" id="@{fieldTitle}.help">@Messages("form.milestone.title.help")</span>
                 @renderFormErrors(fieldTitle, formErrors)
               </div>
               <div class="pure-control-group">
-                <milestone for="@{fieldDescription}">@Messages("form.milestone.description")</milestone>
-                <input class="pure-input-3-4" id="@{fieldDescription}" name="@{fieldDescription}" maxlength="254" placeholder="@Messages("form.milestone.description.placeholder")" type="text" value="@{formData(fieldDescription).headOption}">
-                <span class="pure-form-message" id="@{fieldDescription}.help" style="margin-left: 13em;">@Messages("form.milestone.description.help")</span>
+                <label for="@{fieldDescription}">@Messages("form.milestone.description")</label>
+                <textarea class="pure-input-3-4" id="@{fieldDescription}" name="@{fieldDescription}" placeholder="@Messages("form.milestone.description.placeholder")" rows="4" value="@{formData.get(fieldDescription).headOption}"></textarea>
+                <span class="pure-form-message" id="@{fieldDescription}.help">@Messages("form.milestone.description.help")</span>
                 @renderFormErrors(fieldDescription, formErrors)
               </div>
               <div class="pure-control-group">
-                <milestone for="@{fieldDueDate}">@Messages("form.milestone.due-date")</milestone>
-                <input id="@{fieldDueDate}" name="@{fieldDueDate}" type="date" value="@{formData(fieldDueDate).headOption}">
-                <span class="pure-form-message" id="@{fieldDueDate}.help" style="margin-left: 13em;">@Messages("form.milestone.due-date.help")</span>
+                <label for="@{fieldDueDate}">@Messages("form.milestone.due-date")</label>
+                <input id="@{fieldDueDate}" name="@{fieldDueDate}" type="date" value="@{formData.get(fieldDueDate).headOption}">
+                <span class="pure-form-message" id="@{fieldDueDate}.help">@Messages("form.milestone.due-date.help")</span>
                 @renderFormErrors(fieldDueDate, formErrors)
               </div>
               @csrfToken(csrf)
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html	2025-01-16 07:42:48.528310730 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -100,15 +100,19 @@
                     @icon(baseUri)("flag", lineHeight.some)
                   </div>
                   <div class="pure-u-5-24 milestone-title" style="height: @{lineHeight}px; line-height: @{lineHeight}px;">@milestone.title @for(dueDate <- milestone.dueDate) { @formatDate(dueDate) }</div>
-                  <div class="pure-u-8-24 milestone-description" style="height: @{lineHeight}px; line-height: @{lineHeight}px;">@milestone.description</div>
+                  <div class="pure-u-8-24 milestone-description" style="height: @{lineHeight}px; line-height: @{lineHeight}px;">
+                  @for(description <- milestone.description) {
+                    @if(description.toString.length < 180) { @description } else { @description.toString.take(176)) ...}
+                  }
+                  </div>
                   <div class="pure-u-2-24" style="height: @{lineHeight}px; line-height: @{lineHeight}px; margin: auto;">
                     @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) {
-                    <a class="pure-button" href="@projectBaseUri.addSegment("milestone").addSegment(milestone.title.toString).addSegment("edit")" title="@Messages("project.milestone.edit.title", milestone.title)">@Messages("project.milestone.edit.link")</a>
+                    <a class="pure-button" href="@projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString).addSegment("edit")" title="@Messages("project.milestone.edit.title", milestone.title)">@Messages("project.milestone.edit.link")</a>
                     } else { }
                   </div>
                   <div class="pure-u-8-24 milestone-form" style="height: @{lineHeight}px; line-height: @{lineHeight}px; padding-left: 1em;">
                     @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) {
-                    <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("milestone").addSegment(milestone.title.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8">
+                    <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("milestones").addSegment(milestone.title.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8">
                       <fieldset>
                         <input type="hidden" id="@fieldId-@milestone.title" name="@fieldId" readonly="" value="@milestone.id">
                         <input type="hidden" id="@fieldTitle-@milestone.title" name="@fieldTitle" readonly="" value="@milestone.title">
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDate.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDate.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDate.scala.html	2025-01-16 07:42:48.528310730 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/format/formatDate.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -3,4 +3,4 @@
 @import java.util.Locale
 
 @(date: LocalDate, style: FormatStyle = FormatStyle.MEDIUM)(implicit locale: Locale)
-(@DateTimeFormatter.ofLocalizedDate(style).withLocale(locale).format(date))
+@{DateTimeFormatter.ofLocalizedDate(style).withLocale(locale).format(date)}
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showMilestone.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showMilestone.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showMilestone.scala.html	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showMilestone.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -0,0 +1,66 @@
+@import de.smederee.hub.Account
+@import de.smederee.tickets.MilestoneForm._
+@import de.smederee.tickets._
+@import de.smederee.tickets.forms._
+@import de.smederee.tickets.forms.types._
+@import de.smederee.tickets.views.html.forms.renderFormErrors
+@import de.smederee.tickets.views.html.format._
+@import de.smederee.tickets.views.html.showProjectMenu
+
+@(baseUri: Uri = Uri(path = Uri.Path.Root),
+  lang: LanguageCode = LanguageCode("en")
+)(action: Uri,
+  csrf: Option[CsrfToken] = None,
+  linkToHubService: Uri,
+  milestone: Milestone,
+  renderedMilestoneDescription: Option[String],
+  projectBaseUri: Uri,
+  tickets: List[Ticket],
+  title: Option[String] = None,
+  user: Option[Account],
+  project: Project
+)
+@main(linkToHubService, 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="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2>
+        @showProjectMenu(baseUri, linkToHubService)(projectBaseUri.addSegment("milestones").some, projectBaseUri, user, project)
+        <div class="project-summary-description">
+          @Messages("project.milestone.title", milestone.title)
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="pure-g">
+    <div class="pure-u-16-24 pure-u-md-16-24">
+      <div class="l-box">
+        @if(user.nonEmpty) {
+          <div class="milestone-buttons">
+            <a class="pure-button" href="@projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString).addSegment("edit")" title="@Messages("project.milestone.edit.title", milestone.title)">@Messages("project.milestone.edit.link")</a>
+          </div>
+        } else {}
+        <h1>@milestone.title</h1>
+        <div class="milestone-description">@Html(renderedMilestoneDescription)</div>
+      </div>
+    </div>
+    <div class="pure-u-8-24 pure-u-md-8-24">
+      <div class="l-box">
+        <div class="pure-g milestone-sidebar">
+          <div class="pure-u-7-24"><div class="s-box-left-right">@Messages("milestone.due-date")</div></div>
+          <div class="pure-u-17-24">@for(date <- milestone.dueDate){@formatDate(date)}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="pure-g">
+    <div class="pure-u-1-1 pure-u-md-1-1">
+      <div class="l-box">
+      </div>
+    </div>
+  </div>
+</div>
+}
+}
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	2025-01-16 07:42:48.528310730 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html	2025-01-16 07:42:48.528310730 +0000
@@ -46,18 +46,32 @@
     <div class="pure-u-8-24 pure-u-md-8-24">
       <div class="l-box">
         <div class="pure-g ticket-sidebar">
-          <div class="pure-u-1-5">Status</div><div class="pure-u-4-5">@formatTicketStatus(ticket)</div>
-          <div class="pure-u-1-5">Assigned</div><div class="pure-u-4-5"></div>
-          <div class="pure-u-1-5">Reported</div><div class="pure-u-4-5">@formatTicketSubmitter(linkToHubService)(ticket) at @formatDateTime(ticket.createdAt)</div>
+          <div class="pure-u-7-24">
+            <div class="s-box-left-right">@Messages("ticket.status")</div>
+          </div>
+          <div class="pure-u-17-24">@formatTicketStatus(ticket)</div>
+          <div class="pure-u-7-24">
+            <div class="s-box-left-right">@Messages("ticket.assigned")</div>
+          </div>
+          <div class="pure-u-17-24"></div>
+          <div class="pure-u-7-24">
+            <div class="s-box-left-right">@Messages("ticket.reported")</div>
+          </div>
+          <div class="pure-u-17-24">@formatTicketSubmitter(linkToHubService)(ticket) at @formatDateTime(ticket.createdAt)</div>
           @if(milestones.nonEmpty) {
-            <div class="pure-u-1-5">Milestones</div>
-            <div class="pure-u-4-5">
-              @for(milestone <- milestones) {
-                <a href="@projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)">@milestone.title</a><br/>
-              }
-            </div>
+          <div class="pure-u-7-24">
+            <div class="s-box-left-right">@Messages("ticket.milestones")</div>
+          </div>
+          <div class="pure-u-17-24">
+            @for(milestone <- milestones) {
+              <a href="@projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)">@milestone.title</a><br/>
+            }
+          </div>
           } else {}
-          <div class="pure-u-1-5">Updated</div><div class="pure-u-4-5">@formatDateTime(ticket.updatedAt)</div>
+          <div class="pure-u-7-24">
+            <div class="s-box-left-right">@Messages("ticket.updated")</div>
+          </div>
+          <div class="pure-u-17-24">@formatDateTime(ticket.updatedAt)</div>
         </div>
       </div>
     </div>