Inline description editing with HTMX view/edit swap

t-175·WorkTask·
·
·
·Omni/Jr.hs
Created3 months ago·Updated3 months ago

Description

Edit

Overview

Replace the accordion-based "Edit Description" with inline edit/view mode swapping using HTMX partials.

Current UX

Description
┌─────────────────────────────────┐
│ The task description text...    │
└─────────────────────────────────┘
▶ Edit Description  (accordion expands to show textarea)

Proposed UX

View Mode:

Description                        Edit
┌─────────────────────────────────┐
│ The task description text...    │
└─────────────────────────────────┘

Edit Mode (after clicking Edit):

Description                      Cancel
┌─────────────────────────────────┐
│ [textarea with current text]    │
└─────────────────────────────────┘
[Save]
  • Cancel shows "Discard changes?" confirmation before discarding
  • Edit/Cancel are text links, not buttons

Implementation

New API Routes

Add to API type:

:<|> "tasks" :> Capture "id" Text :> "description" :> "view" :> Get '[Lucid.HTML] DescriptionViewPartial
:<|> "tasks" :> Capture "id" Text :> "description" :> "edit" :> Get '[Lucid.HTML] DescriptionEditPartial

Modify existing POST to return the view partial instead of redirect:

:<|> "tasks" :> Capture "id" Text :> "description" :> ReqBody '[FormUrlEncoded] DescriptionForm :> Post '[Lucid.HTML] DescriptionViewPartial

New Types

data DescriptionViewPartial = DescriptionViewPartial Text Text Bool  -- taskId, description, isEpic
data DescriptionEditPartial = DescriptionEditPartial Text Text Bool  -- taskId, description, isEpic

View Partial Render

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)

Edit Partial Render

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"

Handlers

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 newDesc) = do
  liftIO (TaskCore.updateTaskDescription tid newDesc)
  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))

Update TaskDetailPage Render

Replace the <details> accordion section with initial view partial:

-- In TaskDetailFound render, replace the description detail-section with:
Lucid.div_ [Lucid.class_ "detail-section"] <| do
  Lucid.toHtml (DescriptionViewPartial (TaskCore.taskId task) (TaskCore.taskDescription task) (TaskCore.taskType task == TaskCore.Epic))

CSS

".description-block" ? do
  -- container styles if needed
  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"  -- red for cancel

Dark mode:

".cancel-link" ? color "#f87171"  -- red-400

Files to Modify

1. Omni/Jr/Web.hs:

  • Add new partial types
  • Add new routes to API type
  • Add handlers
  • Update TaskDetailPage render to use view partial
  • Remove old <details> accordion code

2. Omni/Jr/Web/Style.hs:

  • Add .description-header styles
  • Add .edit-link, .cancel-link styles
  • Dark mode overrides

Testing

1. View task with description - see "Edit" link 2. View task without description - see "Edit" link + "No description yet" 3. Click Edit - swaps to textarea with current content 4. Click Cancel without changes - shows "Discard changes?" then swaps back 5. Click Cancel after typing - shows "Discard changes?" then swaps back 6. Type new content, click Save - swaps to view with new content 7. Refresh page - changes persisted 8. Test with Epic (markdown rendered) vs WorkTask (pre text) 9. Check dark mode styling

Timeline (0)

No activity yet.