diff --git a/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx b/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx new file mode 100644 index 000000000..3a4ff2207 --- /dev/null +++ b/packages/react/test-app/Pages/InfiniteScroll/Filtering.tsx @@ -0,0 +1,67 @@ +import { InfiniteScroll, Link, useForm } from '@inertiajs/react' +import { debounce } from 'lodash-es' +import { useCallback, useEffect } from 'react' +import UserCard, { User } from './UserCard' + +interface Props { + users: { data: User[] } + filter?: string + search?: string +} + +export default ({ users, filter, search }: Props) => { + const { data, setData, get } = useForm({ + filter: undefined, + page: undefined, + search: search, + }) + + const debouncedSearch = useCallback( + debounce(() => { + get('', { replace: true }) + }, 250), + [get], + ) + + useEffect(() => { + if (data.search !== search) { + debouncedSearch() + } + }, [data.search, search, debouncedSearch]) + + const handleSearchChange = (e: React.ChangeEvent) => { + setData('search', e.target.value) + } + + return ( +
+
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+ +
Loading...
} + > + {users.data.map((user) => ( + + ))} +
+ +
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+
+ ) +} diff --git a/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte b/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte new file mode 100644 index 000000000..cdc36b396 --- /dev/null +++ b/packages/svelte/test-app/Pages/InfiniteScroll/Filtering.svelte @@ -0,0 +1,64 @@ + + +
+
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+ + +
Loading...
+ + {#each users.data as user (user.id)} + + {/each} +
+ +
+ No Filter + A-M + N-Z +
Current filter: {filter || 'none'}
+
Current search: {search || 'none'}
+ +
+
diff --git a/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue new file mode 100644 index 000000000..05ef41cd5 --- /dev/null +++ b/packages/vue3/test-app/Pages/InfiniteScroll/Filtering.vue @@ -0,0 +1,53 @@ + + + diff --git a/tests/app/eloquent.js b/tests/app/eloquent.js index b6e12f0e0..d3ad547e7 100644 --- a/tests/app/eloquent.js +++ b/tests/app/eloquent.js @@ -5,6 +5,102 @@ function makeUser(id) { } } +export function getUserNames() { + return [ + 'Adelle Crona DVM', + 'Alison Walter PhD', + 'Aliza Langosh II', + 'Amara DuBuque', + 'Amaya Lang', + 'Angelica Rodriguez', + 'Anjali Windler', + 'Ansley Gusikowski', + 'Asha Welch II', + 'Bailee Zulauf', + 'Barrett Heathcote', + 'Beryl Morar', + 'Bethany Grant', + 'Braden Mayert II', + 'Breana Herzog', + 'Camylle Metz Sr.', + 'Carmen Kerluke', + 'Casimer McClure', + 'Cecil Walsh', + 'Chandler McKenzie', + 'Chaya Rempel', + 'Chelsey Mertz Jr.', + 'Coralie Auer', + 'Daniella Hoppe', + 'Daphnee Douglas', + 'Davonte Heathcote', + 'Dejah Parisian', + 'Demarco Medhurst', + 'Dewayne Rau', + 'Diamond Gibson PhD', + 'Diamond Herzog', + 'Dino Predovic I', + 'Domingo Luettgen', + 'Dora Runolfsdottir', + 'Dr. Billy Larkin', + 'Dr. Chase Green', + 'Dr. Curtis Lehner', + 'Einar Crona MD', + 'Eloisa Pollich', + 'Elsie Goldner', + 'Emma Little Sr.', + 'Erika Ziemann DDS', + 'Ethan Beatty', + 'Euna Boehm', + 'Euna Kerluke', + 'Felton Yost', + 'Genesis Hand', + 'Hailie Quitzon', + 'Helga Waelchi', + 'Ibrahim Jakubowski', + 'Jack Halvorson', + 'Jasmin Stoltenberg', + 'Jennie Olson PhD', + 'Jimmy Gusikowski', + 'Joy Schimmel', + 'Kamron Bechtelar DDS', + 'Katarina McLaughlin', + 'Katharina Towne', + 'Kavon Sporer', + 'Keshawn Langosh DDS', + 'Lacy Johnston V', + 'Lauren Thiel', + 'Lelia Haley', + 'Lonny Hermiston', + 'Lupe Jacobs', + 'Magdalena Rowe', + 'Marjolaine Gleason', + 'Mattie Bradtke', + 'Miss Amiya Altenwerth', + 'Miss Haven Kuhic', + 'Miss Janie Bayer', + 'Miss Raegan Doyle IV', + 'Molly Murray', + 'Niko Christiansen Jr.', + 'Paxton Koss', + 'Reilly Bechtelar', + 'Rex Blanda', + 'Riley Legros', + 'River Pfeffer', + 'Rory Lubowitz', + 'Rosamond Mueller II', + 'Rosario Nicolas Sr.', + 'Sandrine Hammes', + 'Tad Thompson', + 'Talon Fahey DVM', + 'Taylor Kuhlman IV', + 'Tyler Zieme', + 'Vella Price', + 'Virginie Beatty', + 'Wiley Donnelly', + 'Woodrow Kuvalis', + ] +} + function getUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { // orderByDesc = false // page = 1 => User 1 ... 15, page = 3 => User 31 ... 40 @@ -35,29 +131,6 @@ function getUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { } } -export function simplePaginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { - const users = getUsers(page, perPage, total, orderByDesc) - const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 - - const paginated = { - current_page: page, - data: users, - from: users[0]?.id || null, - per_page: perPage, - to: users[users.length - 1]?.id || null, - } - - return { - paginated, - scrollProp: { - pageName: 'page', - previousPage: page > 1 ? page - 1 : null, - nextPage: hasMore ? page + 1 : null, - currentPage: page, - }, - } -} - export function paginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { const users = getUsers(page, perPage, total, orderByDesc) const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 @@ -78,23 +151,3 @@ export function paginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = }, } } - -export function cursorPaginateUsers(page = 1, perPage = 15, total = 40, orderByDesc = false) { - const users = getUsers(page, perPage, total, orderByDesc) - const hasMore = getUsers(page + 1, perPage, total, orderByDesc).length > 0 - - return { - paginated: { - data: users, - per_page: perPage, - next_cursor: hasMore ? page + 1 : null, - prev_cursor: page === 1 ? null : page - 1, - }, - scrollProp: { - pageName: 'page', - previousPage: page > 1 ? page - 1 : null, - nextPage: hasMore ? page + 1 : null, - currentPage: page, - }, - } -} diff --git a/tests/app/server.js b/tests/app/server.js index a57bd42fd..fb155069c 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -4,7 +4,7 @@ const inertia = require('./helpers') const bodyParser = require('body-parser') const multer = require('multer') const { showServerStatus } = require('./server-status') -const { paginateUsers } = require('./eloquent') +const { getUserNames, paginateUsers } = require('./eloquent') const app = express() app.use(bodyParser.urlencoded({ extended: true })) @@ -925,6 +925,49 @@ app.get('/infinite-scroll/short-content', (req, res) => renderInfiniteScroll(req, res, 'InfiniteScroll/ShortContent', 100, false, 5), ) +function renderInfiniteScrollWithTag(req, res, component, total = 40, orderByDesc = false, perPage = 15) {} + +app.get('/infinite-scroll/filtering', (req, res) => { + const filter = req.query.filter + const search = req.query.search + + let users = getUserNames() + + if (search) { + users = users.filter((user) => user.toLowerCase().includes(search.toLowerCase())) + } + + if (filter === 'a-m') { + users = users.filter((user) => user.toLowerCase() >= 'a' && user.toLowerCase() <= 'm') + } else if (filter === 'n-z') { + users = users.filter((user) => user.toLowerCase() >= 'n' && user.toLowerCase() <= 'z') + } + + const perPage = 15 + const page = req.query.page ? parseInt(req.query.page) : 1 + + const partialReload = !!req.headers['x-inertia-partial-data'] + const shouldAppend = req.headers['x-inertia-infinite-scroll-merge-intent'] !== 'prepend' + const { paginated, scrollProp } = paginateUsers(page, perPage, users.length) + + if (page > 1) { + users = users.slice((page - 1) * perPage, page * perPage) + } + + paginated.data = paginated.data.map((user, i) => ({ ...user, name: users[i] })) + + setTimeout( + () => + inertia.render(req, res, { + component: 'InfiniteScroll/Filtering', + props: { users: paginated, filter, search }, + [shouldAppend ? 'mergeProps' : 'prependProps']: ['users.data'], + scrollProps: { users: scrollProp }, + }), + partialReload ? 250 : 0, + ) +}) + app.all('*', (req, res) => inertia.render(req, res)) const adapterPorts = { diff --git a/tests/infinite-scroll.spec.ts b/tests/infinite-scroll.spec.ts index ea1e95099..2101665f4 100644 --- a/tests/infinite-scroll.spec.ts +++ b/tests/infinite-scroll.spec.ts @@ -1728,3 +1728,131 @@ Object.entries({ }) }) }) + +test.describe('Query parameter handling', () => { + test('it keeps the existing query parameters intact when updating the page param', async ({ page }) => { + requests.listen(page) + await page.goto('/infinite-scroll/filtering') + + await page.getByRole('link', { name: 'N-Z' }).first().click() + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + + // Scroll to bottom to load page 2 + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + // Assert filter=n-z&page=2 + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await expect(infiniteScrollRequests().length).toBe(1) + }) + + test('it resets the infinite scroll component when navigating to the same page with different query params', async ({ + page, + }) => { + requests.listen(page) + await page.goto('/infinite-scroll/filtering') + + await page.getByRole('link', { name: 'A-M' }).first().click() + + await expect(page.getByText('Adelle Crona DVM')).toBeVisible() + await expect(page.getByText('Breana Herzog')).toBeVisible() + await expect(page.getByText('Current filter: a-m').first()).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Camylle Metz Sr.')).toBeVisible() + + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight - 1000)) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=a-m') + expect(page.url()).toContain('page=2') + + await expect(infiniteScrollRequests().length).toBe(1) + + await page.getByRole('link', { name: 'N-Z' }).first().click() + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + + await expect(page.getByText('Adelle Crona DVM')).toBeHidden() + await expect(page.getByText('Camylle Metz Sr.')).toBeHidden() + + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(infiniteScrollRequests().length).toBe(2) + }) + + test('it reset the page and filter params when searching for a user', async ({ page }) => { + requests.listen(page) + await page.goto('/infinite-scroll/filtering') + await page.getByRole('link', { name: 'N-Z' }).first().click() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') + + // Search for 'adelle' in bottom input box + await page.locator('input').nth(1).fill('adelle') + + await expect(page.getByText('Adelle Crona DVM')).toBeVisible() + + // Assert search=adelle (no page param, no filter param) + expect(page.url()).toContain('search=adelle') + expect(page.url()).not.toContain('page=') + expect(page.url()).not.toContain('filter=') + await expect(page.getByText('Current search: adelle').first()).toBeVisible() + await expect(page.getByText('Current filter: none').first()).toBeVisible() + + // Assert only 'Adelle Crona DVM' is visible + await expect(page.getByText('Niko Christiansen Jr.')).toBeHidden() + await expect(page.getByText('Woodrow Kuvalis')).toBeHidden() + + // Click N-Z again (this should reset search and apply filter) + await page.getByRole('link', { name: 'N-Z' }).first().click() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + + // Assert filter=n-z (no page param, no search param) + expect(page.url()).toContain('filter=n-z') + expect(page.url()).not.toContain('page=') + expect(page.url()).not.toContain('search=') + await expect(page.getByText('Current filter: n-z').first()).toBeVisible() + await expect(page.getByText('Current search: none').first()).toBeVisible() + + // Assert 'Adelle Crona DVM' is hidden, 'Niko Christiansen Jr.' is visible + await expect(page.getByText('Adelle Crona DVM')).toBeHidden() + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + + await scrollToBottom(page) + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await scrollToBottom(page) + await expectQueryString(page, '2') + expect(page.url()).toContain('filter=n-z') + expect(page.url()).toContain('page=2') + + await expect(page.getByText('Niko Christiansen Jr.')).toBeVisible() + await expect(page.getByText('Woodrow Kuvalis')).toBeVisible() + + await expect(infiniteScrollRequests().length).toBe(2) // Initial N-Z page 2 load, final N-Z page 2 load + }) +})