diff options
| author | Ben Sima <ben@bensima.com> | 2025-11-29 18:57:21 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-29 18:57:21 -0500 |
| commit | ddc0ba8090f07a997d7ccb215eaf0a8c7aef6169 (patch) | |
| tree | 5a569be56a45fbdd15913d3713911abd4d995f49 /Omni | |
| parent | b46cd3444933e54ec2fa095cfeb45913427a44f9 (diff) | |
Inline description editing with HTMX view/edit swap
The implementation is complete. Here's a summary of the changes:
**Omni/Jr/Web.hs:** 1. Added new API routes for
`/tasks/:id/description/view` and `/tasks/:i 2. Added
`DescriptionViewPartial` and `DescriptionEditPartial` data
type 3. Added `ToHtml` instances for both partials with HTMX
attributes for v 4. Added handlers: `descriptionViewHandler`,
`descriptionEditHandler`, ` 5. Updated the TaskDetailPage render to
use the view partial instead of
**Omni/Jr/Web/Style.hs:** 1. Added `.description-block` container
styles 2. Added `.description-header` flex styles for header with
title and edi 3. Added `.edit-link` and `.cancel-link` styles (12px
font, blue for edi 4. Added dark mode overrides for the links
Task-Id: t-175
Diffstat (limited to 'Omni')
| -rw-r--r-- | Omni/Jr/Web.hs | 136 | ||||
| -rw-r--r-- | Omni/Jr/Web/Style.hs | 15 |
2 files changed, 102 insertions, 49 deletions
diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs index e73b550..a4fdee1 100644 --- a/Omni/Jr/Web.hs +++ b/Omni/Jr/Web.hs @@ -110,7 +110,9 @@ type API = :<|> "epics" :> Get '[Lucid.HTML] EpicsPage :<|> "tasks" :> Capture "id" Text :> Get '[Lucid.HTML] TaskDetailPage :<|> "tasks" :> Capture "id" Text :> "status" :> ReqBody '[FormUrlEncoded] StatusForm :> Post '[Lucid.HTML] StatusBadgePartial - :<|> "tasks" :> Capture "id" Text :> "description" :> ReqBody '[FormUrlEncoded] DescriptionForm :> PostRedirect + :<|> "tasks" :> Capture "id" Text :> "description" :> "view" :> Get '[Lucid.HTML] DescriptionViewPartial + :<|> "tasks" :> Capture "id" Text :> "description" :> "edit" :> Get '[Lucid.HTML] DescriptionEditPartial + :<|> "tasks" :> Capture "id" Text :> "description" :> ReqBody '[FormUrlEncoded] DescriptionForm :> Post '[Lucid.HTML] DescriptionViewPartial :<|> "tasks" :> Capture "id" Text :> "notes" :> ReqBody '[FormUrlEncoded] NotesForm :> PostRedirect :<|> "tasks" :> Capture "id" Text :> "review" :> Get '[Lucid.HTML] TaskReviewPage :<|> "tasks" :> Capture "id" Text :> "diff" :> Capture "commit" Text :> Get '[Lucid.HTML] TaskDiffPage @@ -215,6 +217,10 @@ newtype TaskListPartial = TaskListPartial [TaskCore.Task] data TaskMetricsPartial = TaskMetricsPartial Text [TaskCore.TaskActivity] (Maybe TaskCore.RetryContext) UTCTime +data DescriptionViewPartial = DescriptionViewPartial Text Text Bool + +data DescriptionEditPartial = DescriptionEditPartial Text Text Bool + newtype RejectForm = RejectForm (Maybe Text) instance FromForm RejectForm where @@ -1128,50 +1134,11 @@ instance Lucid.ToHtml TaskDetailPage where Lucid.ul_ [Lucid.class_ "dep-list"] <| do traverse_ renderDependency deps - case TaskCore.taskType task of - TaskCore.Epic -> do - for_ maybeAggMetrics (renderAggregatedMetrics allTasks task) - Lucid.div_ [Lucid.class_ "detail-section"] <| do - Lucid.h3_ "Design" - if Text.null (TaskCore.taskDescription task) - then Lucid.p_ [Lucid.class_ "empty-msg"] "No design document yet." - else Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown (TaskCore.taskDescription task)) - Lucid.details_ [Lucid.class_ "edit-description"] <| do - Lucid.summary_ "Edit Design" - Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description")] <| do - Lucid.textarea_ - [ Lucid.name_ "description", - Lucid.class_ "description-textarea", - Lucid.rows_ "15", - Lucid.placeholder_ "Enter design in Markdown format..." - ] - (Lucid.toHtml (TaskCore.taskDescription task)) - Lucid.div_ [Lucid.class_ "form-actions"] <| do - Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Design" - _ -> - Lucid.div_ [Lucid.class_ "detail-section"] <| do - Lucid.h3_ "Description" - if Text.null (TaskCore.taskDescription task) - then Lucid.p_ [Lucid.class_ "empty-msg"] "No description yet." - else Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml (TaskCore.taskDescription task)) - Lucid.details_ [Lucid.class_ "edit-description"] <| do - Lucid.summary_ "Edit Description" - Lucid.form_ - [ Lucid.method_ "POST", - Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/description"), - Lucid.makeAttribute "hx-post" ("/tasks/" <> TaskCore.taskId task <> "/description"), - Lucid.makeAttribute "hx-swap" "none" - ] - <| do - Lucid.textarea_ - [ Lucid.name_ "description", - Lucid.class_ "description-textarea", - Lucid.rows_ "10", - Lucid.placeholder_ "Enter description..." - ] - (Lucid.toHtml (TaskCore.taskDescription task)) - Lucid.div_ [Lucid.class_ "form-actions"] <| do - Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Save Description" + when (TaskCore.taskType task == TaskCore.Epic) <| do + for_ maybeAggMetrics (renderAggregatedMetrics allTasks task) + + Lucid.div_ [Lucid.class_ "detail-section"] <| do + Lucid.toHtml (DescriptionViewPartial (TaskCore.taskId task) (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic)) let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks unless (null children) <| do @@ -1844,6 +1811,58 @@ instance Lucid.ToHtml TaskMetricsPartial where let dollars = fromIntegral cents / 100.0 :: Double in "$" <> Text.pack (showFFloat (Just 2) dollars "") +instance Lucid.ToHtml DescriptionViewPartial where + toHtmlRaw = Lucid.toHtml + toHtml (DescriptionViewPartial tid desc isEpic) = + Lucid.div_ [Lucid.id_ "description-block", Lucid.class_ "description-block"] <| do + Lucid.div_ [Lucid.class_ "description-header"] <| do + Lucid.h3_ (if isEpic then "Design" else "Description") + Lucid.a_ + [ Lucid.href_ "#", + Lucid.class_ "edit-link", + Lucid.makeAttribute "hx-get" ("/tasks/" <> tid <> "/description/edit"), + Lucid.makeAttribute "hx-target" "#description-block", + Lucid.makeAttribute "hx-swap" "outerHTML" + ] + "Edit" + if Text.null desc + then Lucid.p_ [Lucid.class_ "empty-msg"] (if isEpic then "No design document yet." else "No description yet.") + else + if isEpic + then Lucid.div_ [Lucid.class_ "markdown-content"] (renderMarkdown desc) + else Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml desc) + +instance Lucid.ToHtml DescriptionEditPartial where + toHtmlRaw = Lucid.toHtml + toHtml (DescriptionEditPartial tid desc isEpic) = + Lucid.div_ [Lucid.id_ "description-block", Lucid.class_ "description-block editing"] <| do + Lucid.div_ [Lucid.class_ "description-header"] <| do + Lucid.h3_ (if isEpic then "Design" else "Description") + Lucid.a_ + [ Lucid.href_ "#", + Lucid.class_ "cancel-link", + Lucid.makeAttribute "hx-get" ("/tasks/" <> tid <> "/description/view"), + Lucid.makeAttribute "hx-target" "#description-block", + Lucid.makeAttribute "hx-swap" "outerHTML", + Lucid.makeAttribute "hx-confirm" "Discard changes?" + ] + "Cancel" + Lucid.form_ + [ Lucid.makeAttribute "hx-post" ("/tasks/" <> tid <> "/description"), + Lucid.makeAttribute "hx-target" "#description-block", + Lucid.makeAttribute "hx-swap" "outerHTML" + ] + <| do + Lucid.textarea_ + [ Lucid.name_ "description", + Lucid.class_ "description-textarea", + Lucid.rows_ (if isEpic then "15" else "10"), + Lucid.placeholder_ (if isEpic then "Enter design in Markdown..." else "Enter description...") + ] + (Lucid.toHtml desc) + Lucid.div_ [Lucid.class_ "form-actions"] <| do + Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "btn btn-primary"] "Save" + -- | Simple markdown renderer for epic descriptions -- Supports: headers (#, ##, ###), lists (- or *), code blocks (```), inline code (`) renderMarkdown :: (Monad m) => Text -> Lucid.HtmlT m () @@ -1975,7 +1994,9 @@ server = :<|> epicsHandler :<|> taskDetailHandler :<|> taskStatusHandler - :<|> taskDescriptionHandler + :<|> descriptionViewHandler + :<|> descriptionEditHandler + :<|> descriptionPostHandler :<|> taskNotesHandler :<|> taskReviewHandler :<|> taskDiffHandler @@ -2140,11 +2161,28 @@ server = liftIO <| TaskCore.updateTaskStatus tid newStatus [] pure (StatusBadgePartial newStatus tid) - taskDescriptionHandler :: Text -> DescriptionForm -> Servant.Handler (Headers '[Header "Location" Text] NoContent) - taskDescriptionHandler tid (DescriptionForm desc) = do + descriptionViewHandler :: Text -> Servant.Handler DescriptionViewPartial + descriptionViewHandler tid = do + tasks <- liftIO TaskCore.loadTasks + case TaskCore.findTask tid tasks of + Nothing -> throwError err404 + Just task -> pure (DescriptionViewPartial tid (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic)) + + descriptionEditHandler :: Text -> Servant.Handler DescriptionEditPartial + descriptionEditHandler tid = do + tasks <- liftIO TaskCore.loadTasks + case TaskCore.findTask tid tasks of + Nothing -> throwError err404 + Just task -> pure (DescriptionEditPartial tid (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic)) + + descriptionPostHandler :: Text -> DescriptionForm -> Servant.Handler DescriptionViewPartial + descriptionPostHandler tid (DescriptionForm desc) = do let descText = Text.strip desc _ <- liftIO <| TaskCore.editTask tid (\t -> t {TaskCore.taskDescription = descText}) - pure <| addHeader ("/tasks/" <> tid) NoContent + tasks <- liftIO TaskCore.loadTasks + case TaskCore.findTask tid tasks of + Nothing -> throwError err404 + Just task -> pure (DescriptionViewPartial tid (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic)) taskNotesHandler :: Text -> NotesForm -> Servant.Handler (Headers '[Header "Location" Text] NoContent) taskNotesHandler tid (NotesForm notes) = do diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs index 10f7f73..c8bfaa5 100644 --- a/Omni/Jr/Web/Style.hs +++ b/Omni/Jr/Web/Style.hs @@ -395,6 +395,19 @@ cardStyles = do margin (px 0) (px 0) (px 0) (px 0) color "#374151" fontSize (px 13) + ".description-block" ? do + pure () + ".description-header" ? do + display flex + justifyContent spaceBetween + alignItems center + marginBottom (px 8) + (".description-header" |> "h3") ? do + margin (px 0) (px 0) (px 0) (px 0) + ".edit-link" <> ".cancel-link" ? do + fontSize (px 12) + color "#0066cc" + ".cancel-link" ? color "#dc2626" ".diff-block" ? do maxHeight (px 600) overflowY auto @@ -1412,6 +1425,8 @@ darkModeStyles = color "#f3f4f6" ".edit-description" ? borderTopColor "#374151" (".edit-description" |> "summary") ? color "#60a5fa" + ".edit-link" ? color "#60a5fa" + ".cancel-link" ? color "#f87171" ".description-textarea" ? do backgroundColor "#374151" borderColor "#4b5563" |
