Skip to content

Added infinite scroll support to filter options#27606

Open
kevinansfield wants to merge 1 commit into
mainfrom
codex/filter-options-infinite-scroll
Open

Added infinite scroll support to filter options#27606
kevinansfield wants to merge 1 commit into
mainfrom
codex/filter-options-infinite-scroll

Conversation

@kevinansfield
Copy link
Copy Markdown
Member

@kevinansfield kevinansfield commented Apr 28, 2026

ref https://linear.app/ghost/issue/BER-3334/

Summary

  • Added a shared useFilterOptionsInfiniteScroll hook for filter option lists.
  • Wired the filter dropdowns, combobox, and label picker fallback load-more rows into the shared hook.
  • Added focused Shade tests and members E2E coverage for loading additional remote label pages.

Stack

Stacked on #27605.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Review Change Stack

Walkthrough

This PR introduces infinite-scroll capability for paginated filter and select options across the Ghost admin interface. A new useFilterOptionsInfiniteScroll React hook monitors a sentinel element via IntersectionObserver and triggers loadMore() callbacks when it becomes visible, supporting scroll-driven pagination. The hook is integrated into three key components: the filter UI (SelectOptionsList), the multi-select combobox, and the label picker dropdown. Additionally, the createCombinedValueSource control-flow logic is fixed to route loadMore calls to only one source per invocation when multiple sources are combined. Comprehensive unit tests validate the combined-source behavior, and e2e infrastructure has been added to support testing the paginated flow end-to-end.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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
Title check ✅ Passed The pull request title clearly and accurately summarizes the main change: adding infinite scroll support to filter options, which is the core objective across multiple components.
Description check ✅ Passed The description is directly related to the changeset, providing a summary of the shared hook implementation, wiring into components, and test coverage added.
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.

✏️ 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 codex/filter-options-infinite-scroll

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

@kevinansfield kevinansfield force-pushed the codex/filter-option-source-refactor branch from 0db9884 to 0435e09 Compare April 28, 2026 18:11
@kevinansfield kevinansfield force-pushed the codex/filter-options-infinite-scroll branch 5 times, most recently from fb602c2 to 64e034f Compare April 29, 2026 10:08
@kevinansfield kevinansfield force-pushed the codex/filter-option-source-refactor branch from 0435e09 to 9c5cec2 Compare April 29, 2026 10:17
@kevinansfield kevinansfield force-pushed the codex/filter-options-infinite-scroll branch from 64e034f to 09fef38 Compare April 29, 2026 10:18
Base automatically changed from codex/filter-option-source-refactor to main April 30, 2026 16:46
@kevinansfield kevinansfield force-pushed the codex/filter-options-infinite-scroll branch from 09fef38 to 3b807c1 Compare April 30, 2026 16:51
@kevinansfield kevinansfield marked this pull request as ready for review April 30, 2026 16:56
@kevinansfield kevinansfield requested a review from 9larsons as a code owner April 30, 2026 16:56
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.

Caution

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

⚠️ Outside diff range comments (2)
apps/shade/src/components/ui/filters.tsx (1)

1123-1130: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hide the empty state while more pages are available.

CommandEmpty still renders whenever the current page is empty, even if optionSource.hasMore is true. That produces a misleading “No results found” message alongside a Load more control on paginated sources. Please gate this on !optionSource.hasMore here, and mirror the same check in the combobox renderer.

♻️ Proposed fix
-            ) : (
-                <CommandEmpty>{context.i18n.noResultsFound}</CommandEmpty>
-            )}
+            ) : !optionSource.hasMore ? (
+                <CommandEmpty>{context.i18n.noResultsFound}</CommandEmpty>
+            ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/components/ui/filters.tsx` around lines 1123 - 1130, The
empty-state rendering shows CommandEmpty even when more pages exist; update the
conditional in the list renderer (the block using optionSource.isInitialLoad and
CommandEmpty) to only render <CommandEmpty> when optionSource.hasMore is false
(i.e., !optionSource.hasMore), and apply the same gating in the combobox
renderer where the empty message is displayed so that the “No results found”
message is suppressed while optionSource.hasMore is true; locate references to
optionSource.isInitialLoad, optionSource.hasMore, CommandEmpty and the combobox
renderer to apply the change.
apps/shade/src/components/ui/multi-select-combobox.tsx (1)

327-335: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hide the empty state while more pages remain.

This path still renders CommandEmpty whenever the current page is empty, even if source.hasMore is true. That shows “No results found” alongside a Load more control for paginated sources. Please gate this on !source.hasMore here too.

♻️ Proposed fix
-                    ) : (
-                        <CommandEmpty>{i18n.noResultsFound}</CommandEmpty>
-                    )}
+                    ) : !source.hasMore ? (
+                        <CommandEmpty>{i18n.noResultsFound}</CommandEmpty>
+                    ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shade/src/components/ui/multi-select-combobox.tsx` around lines 327 -
335, The empty-state rendering shows CommandEmpty even when pagination indicates
more pages; update the conditional inside CommandList to render CommandEmpty
only when not initial load AND source.hasMore is false: check
source.isInitialLoad first, otherwise render CommandEmpty only if
!source.hasMore, using the existing symbols (source.isInitialLoad,
source.hasMore, CommandEmpty, i18n.noResultsFound, CommandList) so the "No
results found" message is suppressed while more pages remain.
🧹 Nitpick comments (1)
e2e/tests/admin/members/multiselect-filters.test.ts (1)

170-184: ⚡ Quick win

Exercise the sentinel path here, not only the fallback button.

This test still clicks the “Load more” button, so it would pass even if the new infinite-scroll wiring stopped requesting page 2 automatically. Please trigger the scroll sentinel and assert the next page arrives without a manual click.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/admin/members/multiselect-filters.test.ts` around lines 170 - 184,
Replace the manual "Load more" click with exercising the infinite-scroll
sentinel: after membersPage.openFilterValue('Label'), locate the scroll sentinel
element (e.g. the element used for infinite scroll — look for a test id or class
used by the filter value list, such as a data-testid like
"infinite-scroll-sentinel" or the last list item) and call
scrollIntoView/scrollIntoViewIfNeeded on it so the next page is requested
automatically; then assert requestedLabelPages.has(2) and that
membersPage.getFilterOption(/Infinite Label 105/) is visible. Keep references to
mockPaginatedLabels, seedMembersAndNavigate, membersPage.openFilterField,
membersPage.getFilterOption and membersPage.openFilterValue to find the right
place to modify the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/shade/src/components/ui/filters.tsx`:
- Around line 1123-1130: The empty-state rendering shows CommandEmpty even when
more pages exist; update the conditional in the list renderer (the block using
optionSource.isInitialLoad and CommandEmpty) to only render <CommandEmpty> when
optionSource.hasMore is false (i.e., !optionSource.hasMore), and apply the same
gating in the combobox renderer where the empty message is displayed so that the
“No results found” message is suppressed while optionSource.hasMore is true;
locate references to optionSource.isInitialLoad, optionSource.hasMore,
CommandEmpty and the combobox renderer to apply the change.

In `@apps/shade/src/components/ui/multi-select-combobox.tsx`:
- Around line 327-335: The empty-state rendering shows CommandEmpty even when
pagination indicates more pages; update the conditional inside CommandList to
render CommandEmpty only when not initial load AND source.hasMore is false:
check source.isInitialLoad first, otherwise render CommandEmpty only if
!source.hasMore, using the existing symbols (source.isInitialLoad,
source.hasMore, CommandEmpty, i18n.noResultsFound, CommandList) so the "No
results found" message is suppressed while more pages remain.

---

Nitpick comments:
In `@e2e/tests/admin/members/multiselect-filters.test.ts`:
- Around line 170-184: Replace the manual "Load more" click with exercising the
infinite-scroll sentinel: after membersPage.openFilterValue('Label'), locate the
scroll sentinel element (e.g. the element used for infinite scroll — look for a
test id or class used by the filter value list, such as a data-testid like
"infinite-scroll-sentinel" or the last list item) and call
scrollIntoView/scrollIntoViewIfNeeded on it so the next page is requested
automatically; then assert requestedLabelPages.has(2) and that
membersPage.getFilterOption(/Infinite Label 105/) is visible. Keep references to
mockPaginatedLabels, seedMembersAndNavigate, membersPage.openFilterField,
membersPage.getFilterOption and membersPage.openFilterValue to find the right
place to modify the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 02ef933e-1b81-4edc-be7b-d4265e8cee83

📥 Commits

Reviewing files that changed from the base of the PR and between 97b0454 and 3b807c1.

📒 Files selected for processing (10)
  • apps/posts/src/components/label-picker/label-picker.tsx
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/posts/test/unit/hooks/create-combined-value-source.test.tsx
  • apps/shade/src/components.ts
  • apps/shade/src/components/ui/filters.tsx
  • apps/shade/src/components/ui/multi-select-combobox.tsx
  • apps/shade/src/components/ui/use-filter-options-infinite-scroll.ts
  • apps/shade/test/unit/components/ui/filters.test.tsx
  • e2e/helpers/pages/admin/members/members-list-page.ts
  • e2e/tests/admin/members/multiselect-filters.test.ts

no issue

Large filter option lists now share one guarded load-more trigger, while retaining the manual fallback button and covering remote label pagination in E2E.
@kevinansfield kevinansfield force-pushed the codex/filter-options-infinite-scroll branch from 3b807c1 to ede93a7 Compare May 19, 2026 11:10
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.

Caution

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

⚠️ Outside diff range comments (1)
apps/shade/src/components/patterns/filters.tsx (1)

1332-1339: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Suppress the empty state while more pages are available.

Line 1332 still renders CommandEmpty whenever the initial load is done, so this list can show “No results found” at the same time as the load-more control. That is the same contradictory state the label picker change avoids.

Suggested fix
-            {optionSource.isInitialLoad ? (
+            {optionSource.isInitialLoad ? (
                 <div className="flex items-center justify-center py-6 text-sm text-muted-foreground">
                     <Loader2 className="mr-2 size-4 animate-spin" />
                     {context.i18n.loading}
                 </div>
-            ) : (
+            ) : !optionSource.hasMore ? (
                 <CommandEmpty>{context.i18n.noResultsFound}</CommandEmpty>
-            )}
+            ) : null}
🤖 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/shade/src/components/patterns/filters.tsx` around lines 1332 - 1339, The
empty-state is shown whenever optionSource.isInitialLoad is false, which leads
to a "No results found" message even if more pages can be loaded; update the
conditional that renders CommandEmpty so it only shows when loading is finished
AND there are no additional pages (e.g., check optionSource.hasMore /
optionSource.canLoadMore / optionSource.hasNextPage or similar boolean used by
your load-more control). Replace the current else-branch condition to require
!optionSource.isInitialLoad && !optionSource.<has-more-flag> before rendering
CommandEmpty (keep the initial load branch with Loader2 unchanged).
🤖 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.

Outside diff comments:
In `@apps/shade/src/components/patterns/filters.tsx`:
- Around line 1332-1339: The empty-state is shown whenever
optionSource.isInitialLoad is false, which leads to a "No results found" message
even if more pages can be loaded; update the conditional that renders
CommandEmpty so it only shows when loading is finished AND there are no
additional pages (e.g., check optionSource.hasMore / optionSource.canLoadMore /
optionSource.hasNextPage or similar boolean used by your load-more control).
Replace the current else-branch condition to require !optionSource.isInitialLoad
&& !optionSource.<has-more-flag> before rendering CommandEmpty (keep the initial
load branch with Loader2 unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b26f19a-0f25-4ff3-9374-901793ab2713

📥 Commits

Reviewing files that changed from the base of the PR and between 3b807c1 and ede93a7.

📒 Files selected for processing (6)
  • apps/posts/src/components/label-picker/label-picker.tsx
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/posts/test/unit/hooks/create-combined-value-source.test.tsx
  • apps/shade/src/components.ts
  • apps/shade/src/components/patterns/filters.tsx
  • apps/shade/src/components/ui/multi-select-combobox.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/posts/src/hooks/filter-sources/create-combined-value-source.ts
  • apps/shade/src/components.ts
  • apps/shade/src/components/ui/multi-select-combobox.tsx
  • apps/posts/test/unit/hooks/create-combined-value-source.test.tsx

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