Fix Recent Activity pagination and refresh bugs

t-169·WorkTask·
·
·
·Omni/Jr.hs
Created5 months ago·Updated5 months ago·pipeline runs →

Description

Edit

Problem

Two bugs in Recent Activity section on dashboard:

1. Load More button duplicates: When clicking "Load More", the partial returns both items AND a new button, and hx-swap="beforeend" appends both, resulting in multiple buttons.

2. Auto-refresh loses loaded content: The 10-second refresh replaces the entire container with first 5 items, losing any items loaded via "Load More".

Solution

Split into two endpoints with proper targeting.

HTML Structure (HomePage render)

Lucid.div_
  [ Lucid.class_ "recent-activity",
    Lucid.id_ "recent-activity",
    Lucid.makeAttribute "data-newest-ts" (tshow newestTimestamp),
    Lucid.makeAttribute "hx-get" "/partials/recent-activity-new",
    Lucid.makeAttribute "hx-trigger" "every 10s",
    Lucid.makeAttribute "hx-vals" "js:{since: this.dataset.newestTs}",
    Lucid.makeAttribute "hx-target" "#activity-list",
    Lucid.makeAttribute "hx-swap" "afterbegin"
  ]
  <| do
    Lucid.div_ [Lucid.id_ "activity-list", Lucid.class_ "list-group"]
      <| traverse_ renderListGroupItem recentTasks
    when hasMoreRecent
      <| Lucid.button_
        [ Lucid.id_ "activity-load-more",
          Lucid.class_ "btn btn-secondary load-more-btn",
          Lucid.makeAttribute "hx-get" "/partials/recent-activity-more?offset=5",
          Lucid.makeAttribute "hx-target" "#activity-list",
          Lucid.makeAttribute "hx-swap" "beforeend"
        ]
        "Load More"

Where newestTimestamp is round (utcTimeToPOSIXSeconds (taskUpdatedAt (head recentTasks))).

New Types

data RecentActivityMorePartial = RecentActivityMorePartial [TaskCore.Task] Int Bool
data RecentActivityNewPartial = RecentActivityNewPartial [TaskCore.Task] (Maybe Int)

API Routes

Replace existing recent-activity route with:

    :<|> "partials" :> "recent-activity-new" :> QueryParam "since" Int :> Get '[Lucid.HTML] RecentActivityNewPartial
    :<|> "partials" :> "recent-activity-more" :> QueryParam "offset" Int :> Get '[Lucid.HTML] RecentActivityMorePartial

Endpoint 1: GET /partials/recent-activity-new?since=<unix-ts>

Purpose: Prepend new items on auto-refresh (every 10s)

recentActivityNewHandler :: Maybe Int -> Servant.Handler RecentActivityNewPartial
recentActivityNewHandler maybeSince = do
  allTasks <- liftIO TaskCore.loadTasks
  let sinceTime = maybe (posixSecondsToUTCTime 0) (posixSecondsToUTCTime . fromIntegral) maybeSince
      sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
      newTasks = filter (\t -> TaskCore.taskUpdatedAt t > sinceTime) sortedTasks
      newestTs = if null newTasks then maybeSince else Just (taskToUnixTs (head newTasks))
  pure (RecentActivityNewPartial newTasks newestTs)

Render returns raw list items + OOB attribute update:

instance Lucid.ToHtml RecentActivityNewPartial where
  toHtmlRaw = Lucid.toHtml
  toHtml (RecentActivityNewPartial tasks maybeNewestTs) = do
    traverse_ renderListGroupItem tasks
    case maybeNewestTs of
      Nothing -> pure ()
      Just ts ->
        Lucid.div_
          [ Lucid.id_ "recent-activity",
            Lucid.makeAttribute "data-newest-ts" (tshow ts),
            Lucid.makeAttribute "hx-swap-oob" "attributes:#recent-activity data-newest-ts"
          ]
          ""

Endpoint 2: GET /partials/recent-activity-more?offset=<n>

Purpose: Load older items on "Load More" click

recentActivityMoreHandler :: Maybe Int -> Servant.Handler RecentActivityMorePartial
recentActivityMoreHandler maybeOffset = do
  allTasks <- liftIO TaskCore.loadTasks
  let offset = fromMaybe 0 maybeOffset
      pageSize = 5
      sortedTasks = List.sortBy (flip compare `on` TaskCore.taskUpdatedAt) allTasks
      pageTasks = take pageSize <| drop offset sortedTasks
      hasMore = length sortedTasks > offset + pageSize
      nextOffset = offset + pageSize
  pure (RecentActivityMorePartial pageTasks nextOffset hasMore)

Render returns items + OOB button replacement:

instance Lucid.ToHtml RecentActivityMorePartial where
  toHtmlRaw = Lucid.toHtml
  toHtml (RecentActivityMorePartial tasks nextOffset hasMore) = do
    traverse_ renderListGroupItem tasks
    if hasMore
      then
        Lucid.button_
          [ Lucid.id_ "activity-load-more",
            Lucid.class_ "btn btn-secondary load-more-btn",
            Lucid.makeAttribute "hx-get" ("/partials/recent-activity-more?offset=" <> tshow nextOffset),
            Lucid.makeAttribute "hx-target" "#activity-list",
            Lucid.makeAttribute "hx-swap" "beforeend",
            Lucid.makeAttribute "hx-swap-oob" "true"
          ]
          "Load More"
      else
        Lucid.span_ [Lucid.id_ "activity-load-more", Lucid.makeAttribute "hx-swap-oob" "true"] ""

Helper Function

taskToUnixTs :: TaskCore.Task -> Int
taskToUnixTs t = round (utcTimeToPOSIXSeconds (TaskCore.taskUpdatedAt t))

Add import: Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds, posixSecondsToUTCTime)

Files to Modify

Omni/Jr/Web.hs:

  • Add new partial types (RecentActivityNewPartial, RecentActivityMorePartial)
  • Remove old RecentActivityPartial type
  • Update API type (replace old route with two new ones)
  • Add handlers (recentActivityNewHandler, recentActivityMoreHandler)
  • Update HomePage render (add IDs, data-newest-ts, update HTMX attrs)
  • Add taskToUnixTs helper
  • Add import for Data.Time.Clock.POSIX

Testing

1. Load dashboard, verify initial 5 items shown 2. Click "Load More", verify items 6-10 appear, single button with offset=10 3. Click "Load More" again, verify items 11-15 appear, button updates 4. Wait 10 seconds, verify no items lost, any new activity prepends 5. Create a new task in another terminal, verify it appears at top within 10s 6. Click "Load More" until no more items, verify button disappears

Git Commits

7f3b9cb7Fix Recent Activity pagination and refresh bugs
Ben Sima5 months ago1 files

Timeline (0)

No activity yet.