From cbbd38051b3657af0a0ef6e9a3bba3bc4857488a Mon Sep 17 00:00:00 2001 From: edwh Date: Thu, 1 Oct 2026 16:59:03 +0100 Subject: [PATCH] Keep scrolling through your groups as they load (#928) The Your groups tab fetches each group separately. If the list's infinite scroller checked before most had arrived, it decided there were no more rows and never asked again, so only the first page ever showed. Restart the scroller whenever more groups arrive, or the groups in view change. Co-Authored-By: Claude Opus 5.5 (1M context) --- resources/js/components/GroupsTable.test.js | 44 +++++++++++++++++++++ resources/js/components/GroupsTable.vue | 14 ++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/resources/js/components/GroupsTable.test.js b/resources/js/components/GroupsTable.test.js index 8f8a29305..b21cc4bf3 100644 --- a/resources/js/components/GroupsTable.test.js +++ b/resources/js/components/GroupsTable.test.js @@ -204,8 +204,52 @@ describe('GroupsTable paging', () => { expect(wrapper.vm.show).toBe(before + wrapper.vm.pageSize) }) + + // "Your groups" fetches each group separately, so the scroller can run out + // of rows and give up before most of them have arrived. The groups that + // turn up afterwards must still be reachable by scrolling (#928). + test('keeps scrolling through groups that arrive after it ran out', async () => { + const store = new Vuex.Store({ + modules: { + groups: { + namespaced: true, + state: { list: {} }, + getters: { list: state => Object.values(state.list), get: state => id => state.list[id] }, + mutations: { add: (state, g) => Vue.set(state.list, g.id, g) }, + actions: { fetch: () => Promise.resolve(), hydrate: () => Promise.resolve() }, + }, + }, + }) + const wrapper = mount(GroupsTable, { + localVue, + store, + propsData: { groupids: many.map(g => g.id) }, + stubs: { GroupsTableFilters: true, GroupArchivedBadge: true, InfiniteLoading: true, 'b-img-lazy': true }, + }) + + // Only the first few have loaded when the scroller first checks. + many.slice(0, 5).forEach(g => store.commit('groups/add', g)) + await wrapper.vm.$nextTick() + let completed = false + wrapper.vm.loadMore({ loaded: () => {}, complete: () => { completed = true } }) + expect(completed).toBe(true) + + const before = wrapper.findComponent(InfiniteLoadingStub()).attributes('identifier') + many.forEach(g => store.commit('groups/add', g)) + await wrapper.vm.$nextTick() + await wrapper.vm.$nextTick() + + // The scroller is restarted, and the next scroll brings in more rows. + expect(wrapper.findComponent(InfiniteLoadingStub()).attributes('identifier')).not.toBe(before) + wrapper.vm.loadMore({ loaded: () => {}, complete: () => {} }) + expect(wrapper.vm.itemsToShow.length).toBe(50) + }) }) +function InfiniteLoadingStub() { + return { name: 'infinite-loading' } +} + describe('GroupsTable distance sort', () => { // Someone looking at a map wants the groups they can see, nearest first. // Alphabetical order tells them nothing about where anything is. diff --git a/resources/js/components/GroupsTable.vue b/resources/js/components/GroupsTable.vue index a5e80ca2e..7b8e1ea94 100644 --- a/resources/js/components/GroupsTable.vue +++ b/resources/js/components/GroupsTable.vue @@ -90,7 +90,7 @@ - + @@ -180,7 +180,8 @@ export default { searchName: null, searchTags: null, searchShow: false, - show: PAGE_SIZE + show: PAGE_SIZE, + infiniteId: 0 } }, computed: { @@ -281,6 +282,15 @@ export default { if (!same) { this.show = PAGE_SIZE + this.infiniteId++ + } + }, + 'items.length'(newVal, oldVal) { + // Groups can arrive after the scroller has decided there are no more - + // "Your groups" fetches each one separately - and once it has finished + // it never asks again. Restart it so the late arrivals can be scrolled to. + if (newVal > oldVal) { + this.infiniteId++ } }, activeFilters: {