Skip to content

feat: add journey list view toggle#9158

Closed
edmonday wants to merge 1 commit into
mainfrom
edmonday/journey-list-view
Closed

feat: add journey list view toggle#9158
edmonday wants to merge 1 commit into
mainfrom
edmonday/journey-list-view

Conversation

@edmonday
Copy link
Copy Markdown
Contributor

@edmonday edmonday commented May 5, 2026

Summary

  • add a grid/list display toggle to the journeys admin list controls
  • keep the existing grid card view as the default
  • add a responsive list row view using existing journey data, actions, access avatars, and template analytics

Notes

  • no new journey functionality or data fetching was added; this is a presentation toggle over the existing list data

Validation

  • not run per request; local dependency/type-check setup was intentionally skipped for this PR

Summary by CodeRabbit

  • New Features
    • Added a display toggle to switch between grid and list views for journeys.
    • List view: row-oriented layout with status indicators (New/Action Required), badges, language and last-modified info, owner avatars, activity/analytics panels, skeleton image placeholders, and per-row action menus.
    • Grid view behavior preserved; empty-state logic unchanged.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Walkthrough

Adds a grid/list display mode toggle to the journeys admin UI, introduces JourneyListDisplay state, a DisplayControl toggle, a JourneyListRows list renderer, and wires the display value through view modes to JourneyListContent for conditional list or grid rendering.

Changes

Display Mode Selection & List View Implementation

Layer / File(s) Summary
Type Definitions
apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx, apps/journeys-admin/src/components/JourneyList/JourneyListView/index.ts
Adds exported `JourneyListDisplay = 'grid'
View State
apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx
Adds local display state (default 'grid') and forwards display/setDisplay to mode components.
Shared Props / API
apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/shared.ts
Extends SharedControlProps with display and setDisplay; updates SharedModeProps.renderList to accept display.
Display Control UI
apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx
New DisplayControl component: MUI ToggleButtonGroup with grid/list buttons, translation-enabled, calls setDisplay on change.
Mode Integration
apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/SharedWithMeMode/SharedWithMeMode.tsx, .../TeamMode/TeamMode.tsx
Import and render DisplayControl in headers; pass display into renderList calls.
Top-level Wiring
apps/journeys-admin/src/components/JourneyList/JourneyList.tsx
renderList signature updated to accept display and display prop passed into JourneyListContent.
List Row Data Shape
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx
Adds optional display?: JourneyListDisplay prop; memoizes listRows: JourneyListRowItem[] from sortedJourneys, partitioning into actionRequired, new, and active variants when appropriate.
Conditional Rendering
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx
When display === 'list' and listRows exists, renders JourneyListRows; otherwise preserves existing priority-list or grid rendering paths and empty-state logic.
List Row Implementation
apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx, .../JourneyListRow/index.ts
Adds JourneyListRows export and JourneyListRowItem type; implements responsive header, per-row hover/image/load state, badges (New/Action Required/Quick Start/Website), activity column (template analytics vs responses/analytics), owner avatars filtering for actionRequired, and menu/dialog coordination.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant DC as DisplayControl
    participant JLV as JourneyListView
    participant JLC as JourneyListContent
    participant JLR as JourneyListRows
    participant Grid as GridView

    User->>DC: click 'list' toggle
    DC->>JLV: setDisplay('list')
    JLV->>JLV: update display state
    JLV->>JLC: renderList(..., display)
    JLC->>JLC: compute listRows from sortedJourneys
    alt display === 'list'
        JLC->>JLR: render JourneyListRows with rows
        JLR-->>User: show table-like rows with badges and analytics
    else display === 'grid'
        JLC->>Grid: render MUI Grid or ActivePriorityList
        Grid-->>User: show card grid or priority layout
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'feat: add journey list view toggle' directly and clearly summarizes the main change: adding a toggle feature for switching between grid and list views in the journey list display.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch edmonday/journey-list-view

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Fails
🚫 Please assign someone to merge this PR.

Generated by 🚫 dangerJS against 871ba6f

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 5, 2026

View your CI Pipeline Execution ↗ for commit 184230b

Command Status Duration Result
nx run journeys-admin-e2e:e2e ✅ Succeeded 29s View ↗
nx run-many --target=vercel-alias --projects=jo... ✅ Succeeded 2s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 10s View ↗
nx run-many --target=deploy --projects=journeys... ✅ Succeeded 2m 48s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-05 11:00:54 UTC

@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 5, 2026 05:20 Inactive
@blacksmith-sh

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

The latest updates on your projects.

Name Status Preview Updated (UTC)
journeys-admin ✅ Ready journeys-admin preview Tue May 5 22:57:37 NZST 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx (1)

603-625: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty state is missing in list mode for the priority-list scenario.

When display === 'list', usePriorityList === true, and sortedJourneys.length === 0 (e.g., a first-time user on the active journeys tab), JourneyListRows renders with an empty rows={[]} and the bottom isEmpty block doesn't trigger because isEmpty requires !usePriorityList. The grid path covers this case with the dedicated empty Card + AddJourneyButton at lines 633-654, so list-mode users see a blank table with no call-to-action.

🐛 One option: broaden the empty-state predicate so the bottom message renders in list mode too
-  const isEmpty =
-    sortedJourneys != null && sortedJourneys.length === 0 && !usePriorityList
+  const isEmpty =
+    sortedJourneys != null &&
+    sortedJourneys.length === 0 &&
+    (!usePriorityList || display === 'list')

If you want full parity with the grid view (including the AddJourneyButton), consider rendering the same empty Card inside the display === 'list' branch when listRows.length === 0 && usePriorityList.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx`
around lines 603 - 625, The empty-state for priority lists is skipped because
isEmpty excludes usePriorityList; update the render logic so when display ===
'list' and usePriorityList === true and listRows is an empty array you render
the same empty Card + AddJourneyButton used in the grid path instead of
rendering JourneyListRows with rows=[]. Concretely: either broaden the isEmpty
predicate (remove the !usePriorityList condition) or add a conditional in the
list branch (check listRows?.length === 0 && usePriorityList) and render the
existing empty Card + AddJourneyButton block in that branch; reference
JourneyListRows, listRows, usePriorityList, isEmpty and AddJourneyButton to
locate the code to change.
🧹 Nitpick comments (4)
apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx (2)

213-213: ⚡ Quick win

onLoadingComplete is deprecated — replace with onLoad.

onLoadingComplete was deprecated in Next.js 14; the replacement is onLoad, which receives the event object instead. Since this project is on Next.js 15.5.10, using it triggers a console deprecation warning per the Next.js source. Because the argument is unused here, the fix is a one-liner.

♻️ Proposed fix
-                onLoadingComplete={() => setIsImageLoading(false)}
+                onLoad={() => setIsImageLoading(false)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
at line 213, Replace the deprecated Next.js Image prop onLoadingComplete with
onLoad inside the JourneyListRow component: update the Image (or img) prop that
currently calls onLoadingComplete={() => setIsImageLoading(false)} to use onLoad
instead (i.e., onLoad={() => setIsImageLoading(false)}), keeping the existing
setIsImageLoading state updater unchanged so the image-loading state logic stays
the same.

137-141: 💤 Low value

updateHoverState should be prefixed with handle per project conventions.

As per coding guidelines, event-related functions should be named with a handle prefix. Rename to e.g. handleHoverStateUpdate or split into handleMouseEnter/handleMouseLeave.

♻️ Proposed rename
-  function updateHoverState(hovered: boolean): void {
+  function handleHoverStateUpdate(hovered: boolean): void {
     if (!hasOpenDialog) {
       setIsHovered(hovered)
     }
   }
   // ...
-      onMouseEnter={() => updateHoverState(true)}
-      onMouseLeave={() => updateHoverState(false)}
+      onMouseEnter={() => handleHoverStateUpdate(true)}
+      onMouseLeave={() => handleHoverStateUpdate(false)}
   // ...
-          onMenuClose={() => updateHoverState(false)}
+          onMenuClose={() => handleHoverStateUpdate(false)}

As per coding guidelines: "Event handlers must be prefixed with 'handle'."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
around lines 137 - 141, The function updateHoverState should be renamed to
follow the event-handler convention (prefix with "handle"); rename
updateHoverState to a descriptive handler such as handleHoverStateUpdate (or
split into handleMouseEnter and handleMouseLeave) and update all references/call
sites accordingly (e.g., where setIsHovered is used and any JSX event props in
JourneyListRow that call updateHoverState); ensure the parameter and behavior
remain unchanged and adjust any exports or tests that reference the original
function name.
apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx (1)

94-94: ⚡ Quick win

display preference is not persisted — resets to 'grid' on every page load.

Unlike sortOrder (written to / read from sessionStorage), the display mode is only in local component state. Users who prefer list view will need to re-select it after every navigation or refresh.

If persistence is intentional for now, a brief comment would clarify intent. Otherwise, mirror the sortOrder persistence pattern:

♻️ Proposed persistence (optional)
-  const [display, setDisplay] = useState<JourneyListDisplay>('grid')
+  const [display, setDisplay] = useState<JourneyListDisplay>(() => {
+    const stored = sessionStorage.getItem('journeyListDisplay')
+    return stored === 'list' ? 'list' : 'grid'
+  })

And in the setter passed to child modes, persist on change:

// Pass a wrapped setter instead of setDisplay directly
+  const handleSetDisplay = (value: JourneyListDisplay): void => {
+    setDisplay(value)
+    sessionStorage.setItem('journeyListDisplay', value)
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx`
at line 94, display state (const [display, setDisplay] =
useState<JourneyListDisplay>('grid')) isn't persisted and resets on reload;
update it to mirror sortOrder's sessionStorage pattern by reading the initial
value from sessionStorage (falling back to 'grid') and writing the new value to
sessionStorage whenever setDisplay is called or via a wrapper setter passed to
children, or add a comment if persistence is intentional; reference the display,
setDisplay, JourneyListDisplay and sortOrder usage in JourneyListView to
implement the read-on-init and write-on-change behavior.
apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx (1)

431-468: 💤 Low value

Optional: short-circuit listRows when display !== 'list'.

listRows is only consumed in the display === 'list' branch, but the memo still walks sortedJourneys and categorizes by role for grid users on every render (since sortedJourneys is recomputed without memoization upstream, the cached reference rarely sticks). A small early return avoids the work for the common grid path.

♻️ Proposed early return
   const listRows = useMemo(() => {
+    if (display !== 'list') return undefined
     if (sortedJourneys == null) return undefined

     if (!usePriorityList) {
       return sortedJourneys.map((journey) => ({ journey }))
     }
     // ...
-  }, [sortedJourneys, usePriorityList, user?.id])
+  }, [display, sortedJourneys, usePriorityList, user?.id])

Note: also worth considering memoizing sortedJourneys itself (line 422) so this memo's identity actually stabilizes — out of scope for this PR.

Also note that the action-required / new / active categorization here mirrors the logic inside ActivePriorityList. Consider extracting a shared helper in a follow-up to prevent drift if the rules change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx`
around lines 431 - 468, Add an early-return to the listRows useMemo so it does
no work unless the component is rendering the list view: at the top of the memo
check the display prop/value and if display !== 'list' return undefined (or the
current empty/neutral value) immediately; this avoids iterating sortedJourneys
and running the categorization when display is grid, and keeps the existing
logic inside the memo (references: listRows, useMemo, display, sortedJourneys,
usePriorityList, JourneyCardVariant).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`:
- Around line 395-399: TemplateBreakdownAnalyticsDialog is rendered for every
row which triggers its dynamic import even for non-template journeys; wrap the
component render in the same isTemplateCard conditional that renders the
template button (use the existing isTemplateCard boolean) so the dialog is only
mounted when isTemplateCard is true, keeping the same props
(journeyId={journey.id}, open={breakdownDialogOpen}, handleClose={() =>
setBreakdownDialogOpen(false)}) to avoid unnecessary bundle requests for
non-template rows.
- Around line 130-133: The current languageName derivation uses
journey.language.name.find with a predicate that incorrectly depends on
journey.language.name.some which is evaluated outside each element and can
return the first non-primary value; change the logic in the languageName
computation (the variable languageName and its use of
journey.language.name.find) to explicitly pick the element where primary ===
true if any exists, otherwise fall back to the first element (e.g., use find(n
=> n.primary) || journey.language.name[0]); then read its .value safely (e.g.,
via optional chaining) so primary languages are returned correctly.
- Around line 126-141: The hover state can remain true after a dialog closes
because updateHoverState(false) is skipped while hasOpenDialog is true; add a
cleanup effect that watches hasOpenDialog and when it becomes false calls
setIsHovered(false) to reset the row highlight; locate the state and updater
names (isHovered, hasOpenDialog, setIsHovered, updateHoverState) in
JourneyListRow and implement a useEffect that clears isHovered when
hasOpenDialog transitions to false.

---

Outside diff comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx`:
- Around line 603-625: The empty-state for priority lists is skipped because
isEmpty excludes usePriorityList; update the render logic so when display ===
'list' and usePriorityList === true and listRows is an empty array you render
the same empty Card + AddJourneyButton used in the grid path instead of
rendering JourneyListRows with rows=[]. Concretely: either broaden the isEmpty
predicate (remove the !usePriorityList condition) or add a conditional in the
list branch (check listRows?.length === 0 && usePriorityList) and render the
existing empty Card + AddJourneyButton block in that branch; reference
JourneyListRows, listRows, usePriorityList, isEmpty and AddJourneyButton to
locate the code to change.

---

Nitpick comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx`:
- Around line 431-468: Add an early-return to the listRows useMemo so it does no
work unless the component is rendering the list view: at the top of the memo
check the display prop/value and if display !== 'list' return undefined (or the
current empty/neutral value) immediately; this avoids iterating sortedJourneys
and running the categorization when display is grid, and keeps the existing
logic inside the memo (references: listRows, useMemo, display, sortedJourneys,
usePriorityList, JourneyCardVariant).

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`:
- Line 213: Replace the deprecated Next.js Image prop onLoadingComplete with
onLoad inside the JourneyListRow component: update the Image (or img) prop that
currently calls onLoadingComplete={() => setIsImageLoading(false)} to use onLoad
instead (i.e., onLoad={() => setIsImageLoading(false)}), keeping the existing
setIsImageLoading state updater unchanged so the image-loading state logic stays
the same.
- Around line 137-141: The function updateHoverState should be renamed to follow
the event-handler convention (prefix with "handle"); rename updateHoverState to
a descriptive handler such as handleHoverStateUpdate (or split into
handleMouseEnter and handleMouseLeave) and update all references/call sites
accordingly (e.g., where setIsHovered is used and any JSX event props in
JourneyListRow that call updateHoverState); ensure the parameter and behavior
remain unchanged and adjust any exports or tests that reference the original
function name.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx`:
- Line 94: display state (const [display, setDisplay] =
useState<JourneyListDisplay>('grid')) isn't persisted and resets on reload;
update it to mirror sortOrder's sessionStorage pattern by reading the initial
value from sessionStorage (falling back to 'grid') and writing the new value to
sessionStorage whenever setDisplay is called or via a wrapper setter passed to
children, or add a comment if persistence is intentional; reference the display,
setDisplay, JourneyListDisplay and sortOrder usage in JourneyListView to
implement the read-on-init and write-on-change behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4451b7e1-2ee8-47d2-b075-e1c9a6160f6b

📥 Commits

Reviewing files that changed from the base of the PR and between dbbc1e3 and 9a1c251.

📒 Files selected for processing (10)
  • apps/journeys-admin/src/components/JourneyList/JourneyList.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListRow/index.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/SharedWithMeMode/SharedWithMeMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/TeamMode/TeamMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/shared.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/index.ts

Comment on lines +126 to +141
const [isHovered, setIsHovered] = useState(false)
const [isImageLoading, setIsImageLoading] = useState(true)
const [breakdownDialogOpen, setBreakdownDialogOpen] = useState(false)
const [hasOpenDialog, setHasOpenDialog] = useState(false)
const languageName = journey.language.name.find(
({ primary }) =>
primary || journey.language.name.some(({ primary }) => !primary)
)?.value
const isTemplateCard =
journey.template === true && journey.team?.id !== 'jfp-team'

function updateHoverState(hovered: boolean): void {
if (!hasOpenDialog) {
setIsHovered(hovered)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hover highlight stays on indefinitely if the mouse leaves while a dialog is open.

When hasOpenDialog is true, updateHoverState(false) from onMouseLeave is skipped. After the dialog closes and hasOpenDialog returns to false, isHovered is never reset, leaving the row permanently highlighted until the cursor re-enters.

🐛 Proposed fix — add a cleanup effect
  const [hasOpenDialog, setHasOpenDialog] = useState(false)
+ const isMouseOverRef = useRef(false)

  function updateHoverState(hovered: boolean): void {
    if (!hasOpenDialog) {
      setIsHovered(hovered)
    }
+   isMouseOverRef.current = hovered
  }

+ useEffect(() => {
+   if (!hasOpenDialog) {
+     setIsHovered(isMouseOverRef.current)
+   }
+ }, [hasOpenDialog])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isHovered, setIsHovered] = useState(false)
const [isImageLoading, setIsImageLoading] = useState(true)
const [breakdownDialogOpen, setBreakdownDialogOpen] = useState(false)
const [hasOpenDialog, setHasOpenDialog] = useState(false)
const languageName = journey.language.name.find(
({ primary }) =>
primary || journey.language.name.some(({ primary }) => !primary)
)?.value
const isTemplateCard =
journey.template === true && journey.team?.id !== 'jfp-team'
function updateHoverState(hovered: boolean): void {
if (!hasOpenDialog) {
setIsHovered(hovered)
}
}
const [isHovered, setIsHovered] = useState(false)
const [isImageLoading, setIsImageLoading] = useState(true)
const [breakdownDialogOpen, setBreakdownDialogOpen] = useState(false)
const [hasOpenDialog, setHasOpenDialog] = useState(false)
const isMouseOverRef = useRef(false)
const languageName = journey.language.name.find(
({ primary }) =>
primary || journey.language.name.some(({ primary }) => !primary)
)?.value
const isTemplateCard =
journey.template === true && journey.team?.id !== 'jfp-team'
function updateHoverState(hovered: boolean): void {
if (!hasOpenDialog) {
setIsHovered(hovered)
}
isMouseOverRef.current = hovered
}
useEffect(() => {
if (!hasOpenDialog) {
setIsHovered(isMouseOverRef.current)
}
}, [hasOpenDialog])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
around lines 126 - 141, The hover state can remain true after a dialog closes
because updateHoverState(false) is skipped while hasOpenDialog is true; add a
cleanup effect that watches hasOpenDialog and when it becomes false calls
setIsHovered(false) to reset the row highlight; locate the state and updater
names (isHovered, hasOpenDialog, setIsHovered, updateHoverState) in
JourneyListRow and implement a useEffect that clears isHovered when
hasOpenDialog transitions to false.

Comment on lines +130 to +133
const languageName = journey.language.name.find(
({ primary }) =>
primary || journey.language.name.some(({ primary }) => !primary)
)?.value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Logic bug in languageName derivation — may show wrong language.

journey.language.name.some(({ primary }) => !primary) is evaluated once and produces the same boolean for all iterations. When any non-primary name exists (common for multilingual journeys), the predicate is always true, so find returns the first array element — not necessarily the primary one.

Minimal counter-example:

name = [{value:'Spanish', primary:false}, {value:'English', primary:true}]
→ some(not primary) = true → returns 'Spanish'  ✗
🐛 Proposed fix
-  const languageName = journey.language.name.find(
-    ({ primary }) =>
-      primary || journey.language.name.some(({ primary }) => !primary)
-  )?.value
+  const languageName = (
+    journey.language.name.find(({ primary }) => primary) ??
+    journey.language.name[0]
+  )?.value
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const languageName = journey.language.name.find(
({ primary }) =>
primary || journey.language.name.some(({ primary }) => !primary)
)?.value
const languageName = (
journey.language.name.find(({ primary }) => primary) ??
journey.language.name[0]
)?.value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
around lines 130 - 133, The current languageName derivation uses
journey.language.name.find with a predicate that incorrectly depends on
journey.language.name.some which is evaluated outside each element and can
return the first non-primary value; change the logic in the languageName
computation (the variable languageName and its use of
journey.language.name.find) to explicitly pick the element where primary ===
true if any exists, otherwise fall back to the first element (e.g., use find(n
=> n.primary) || journey.language.name[0]); then read its .value safely (e.g.,
via optional chaining) so primary languages are returned correctly.

Comment on lines +395 to +399
<TemplateBreakdownAnalyticsDialog
journeyId={journey.id}
open={breakdownDialogOpen}
handleClose={() => setBreakdownDialogOpen(false)}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

TemplateBreakdownAnalyticsDialog is mounted for every row, including non-template journeys.

It lives outside the isTemplateCard check, so the dynamic import is triggered for all rows. Since it's { ssr: false } via next/dynamic, this also creates an unnecessary bundle chunk request per non-template row. Wrapping it in {isTemplateCard && ...} aligns it with the button that opens it.

🐛 Proposed fix
-      <TemplateBreakdownAnalyticsDialog
-        journeyId={journey.id}
-        open={breakdownDialogOpen}
-        handleClose={() => setBreakdownDialogOpen(false)}
-      />
+      {isTemplateCard && (
+        <TemplateBreakdownAnalyticsDialog
+          journeyId={journey.id}
+          open={breakdownDialogOpen}
+          handleClose={() => setBreakdownDialogOpen(false)}
+        />
+      )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
around lines 395 - 399, TemplateBreakdownAnalyticsDialog is rendered for every
row which triggers its dynamic import even for non-template journeys; wrap the
component render in the same isTemplateCard conditional that renders the
template button (use the existing isTemplateCard boolean) so the dialog is only
mounted when isTemplateCard is true, keeping the same props
(journeyId={journey.id}, open={breakdownDialogOpen}, handleClose={() =>
setBreakdownDialogOpen(false)}) to avoid unnecessary bundle requests for
non-template rows.

@edmonday edmonday force-pushed the edmonday/journey-list-view branch from 9a1c251 to f132b71 Compare May 5, 2026 10:01
@github-actions github-actions Bot had a problem deploying to Preview - journeys-admin May 5, 2026 10:03 Failure
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx (1)

397-402: ⚡ Quick win

Replace inline props object types with named {ComponentName}Props interfaces.

ActivityCell, OwnerCell, and ListBadge currently use inline object types. Please switch to ActivityCellProps, OwnerCellProps, and ListBadgeProps interfaces for consistency and maintainability.

As per coding guidelines: “Props interfaces must follow naming convention {ComponentName}Props” and “Define a type if possible.”

Also applies to: 462-465, 494-498

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`
around lines 397 - 402, The components ActivityCell, OwnerCell, and ListBadge
are using inline object prop types; create named interfaces ActivityCellProps,
OwnerCellProps, and ListBadgeProps and replace the inline types with these
interfaces for each component (e.g., change the inline parameter type in
ActivityCell(...), OwnerCell(...), and ListBadge(...) to use ActivityCellProps,
OwnerCellProps, and ListBadgeProps respectively). Ensure each interface lists
the same properties currently declared inline (preserve names and types such as
journey: Journey, variant: JourneyCardVariant, isTemplateCard: boolean,
onOpenBreakdown: () => void where applicable) and export or scope them
consistently with the component file so the components compile and follow the
{ComponentName}Props naming convention.
apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx (2)

66-68: 💤 Low value

Extract inline onChange as a named handleDisplayChange handler.

The coding guideline requires event handler functions to carry the handle prefix. The current anonymous arrow in onChange bypasses that convention.

♻️ Proposed refactor
 export const DisplayControl = ({
   display,
   setDisplay
 }: Pick<SharedControlProps, 'display' | 'setDisplay'>): ReactElement => {
   const { t } = useTranslation('apps-journeys-admin')
+
+  const handleDisplayChange = (
+    _event: React.MouseEvent<HTMLElement>,
+    value: string | null
+  ): void => {
+    if (value != null) setDisplay(value as JourneyListDisplay)
+  }

   return (
     <ToggleButtonGroup
       ...
-      onChange={(_event, value) => {
-        if (value != null) setDisplay(value)
-      }}
+      onChange={handleDisplayChange}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx`
around lines 66 - 68, The inline anonymous onChange handler should be extracted
to a named handler called handleDisplayChange to follow the "handle" prefix
convention; create a function handleDisplayChange(event, value) (or with the
same params used by the component) that checks value != null and calls
setDisplay(value), then replace the inline onChange prop with
onChange={handleDisplayChange} in the Controls component so references to
setDisplay and the onChange logic are centralized and testable.

6-6: 💤 Low value

Consider extracting the onChange handler with a "handle" prefix.

The onChange callback on line 69 uses an inline arrow function. Per coding guidelines, event handlers should be named with a "handle" prefix (e.g., handleDisplayChange). Extract it as:

const handleDisplayChange = (value: any) => {
  if (value != null) setDisplay(value)
}

Then pass onChange={handleDisplayChange} to ToggleButtonGroup.

The useTranslation import from 'next-i18next/pages' is correct and follows the established convention across the journeys-admin codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx`
at line 6, Extract the inline onChange arrow into a named handler called
handleDisplayChange and pass it to ToggleButtonGroup: implement const
handleDisplayChange = (value: any) => { if (value != null) setDisplay(value) }
and replace the inline onChange prop with onChange={handleDisplayChange}; ensure
you reference the existing setDisplay state updater and ToggleButtonGroup
component when making the change (keep the existing useTranslation import
as-is).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx`:
- Around line 397-402: The components ActivityCell, OwnerCell, and ListBadge are
using inline object prop types; create named interfaces ActivityCellProps,
OwnerCellProps, and ListBadgeProps and replace the inline types with these
interfaces for each component (e.g., change the inline parameter type in
ActivityCell(...), OwnerCell(...), and ListBadge(...) to use ActivityCellProps,
OwnerCellProps, and ListBadgeProps respectively). Ensure each interface lists
the same properties currently declared inline (preserve names and types such as
journey: Journey, variant: JourneyCardVariant, isTemplateCard: boolean,
onOpenBreakdown: () => void where applicable) and export or scope them
consistently with the component file so the components compile and follow the
{ComponentName}Props naming convention.

In
`@apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx`:
- Around line 66-68: The inline anonymous onChange handler should be extracted
to a named handler called handleDisplayChange to follow the "handle" prefix
convention; create a function handleDisplayChange(event, value) (or with the
same params used by the component) that checks value != null and calls
setDisplay(value), then replace the inline onChange prop with
onChange={handleDisplayChange} in the Controls component so references to
setDisplay and the onChange logic are centralized and testable.
- Line 6: Extract the inline onChange arrow into a named handler called
handleDisplayChange and pass it to ToggleButtonGroup: implement const
handleDisplayChange = (value: any) => { if (value != null) setDisplay(value) }
and replace the inline onChange prop with onChange={handleDisplayChange}; ensure
you reference the existing setDisplay state updater and ToggleButtonGroup
component when making the change (keep the existing useTranslation import
as-is).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 51b606e9-40de-40a3-b60e-6a0099d0faf1

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1c251 and f132b71.

📒 Files selected for processing (10)
  • apps/journeys-admin/src/components/JourneyList/JourneyList.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListRow/JourneyListRow.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListRow/index.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/Controls.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/SharedWithMeMode/SharedWithMeMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/TeamMode/TeamMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/shared.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/index.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/journeys-admin/src/components/JourneyList/JourneyListRow/index.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/TeamMode/TeamMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyList.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/index.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/SharedWithMeMode/SharedWithMeMode.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/DisplayModes/shared.ts
  • apps/journeys-admin/src/components/JourneyList/JourneyListView/JourneyListView.tsx
  • apps/journeys-admin/src/components/JourneyList/JourneyListContent/JourneyListContent.tsx

@edmonday edmonday force-pushed the edmonday/journey-list-view branch from f132b71 to cf719d9 Compare May 5, 2026 10:11
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 5, 2026 10:13 Inactive
@edmonday edmonday force-pushed the edmonday/journey-list-view branch from cf719d9 to 96b706c Compare May 5, 2026 10:22
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 5, 2026 10:24 Inactive
@edmonday edmonday force-pushed the edmonday/journey-list-view branch from 96b706c to 184230b Compare May 5, 2026 10:37
@github-actions github-actions Bot had a problem deploying to Preview - journeys-admin May 5, 2026 10:39 Failure
@edmonday edmonday force-pushed the edmonday/journey-list-view branch from 184230b to 84fd911 Compare May 5, 2026 10:42
@github-actions github-actions Bot had a problem deploying to Preview - journeys-admin May 5, 2026 10:44 Failure
@edmonday edmonday force-pushed the edmonday/journey-list-view branch from 84fd911 to 871ba6f Compare May 5, 2026 10:51
@edmonday edmonday changed the title Add journey list view toggle feat: add journey list view toggle May 5, 2026
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 5, 2026 10:53 Inactive
@edmonday
Copy link
Copy Markdown
Contributor Author

edmonday commented May 5, 2026

Superseded by #9159 using a CI-compliant branch name.

@edmonday edmonday closed this May 5, 2026
@github-actions github-actions Bot temporarily deployed to Preview - journeys-admin May 5, 2026 10:57 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant