i changed my mind, fetch logic does belong in <List>

This commit is contained in:
Henry Jameson 2026-06-08 14:24:18 +03:00
commit 4a0be19607
9 changed files with 77 additions and 137 deletions

View file

@ -1,3 +1,5 @@
import { isEmpty } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
const List = { const List = {
@ -6,21 +8,13 @@ const List = {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
items: {
type: Array,
default: [],
},
fetchFunction: { fetchFunction: {
type: Function, type: Function,
default: null, default: null,
}, },
itemsFunction: {
type: Function,
default: null,
},
getKey: { getKey: {
type: Function, type: Function,
default: (item) => item.id, default: (item) => item,
}, },
getClass: { getClass: {
type: Function, type: Function,
@ -38,16 +32,8 @@ const List = {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
loading: { externalItems: {
type: Boolean, type: Array,
default: true,
},
bottomedOut: {
type: Boolean,
default: false,
},
error: {
type: String,
default: null, default: null,
}, },
}, },
@ -57,25 +43,34 @@ const List = {
}, },
data() { data() {
return { return {
selected: [], items: [],
selected: new Set([]),
loading: false,
bottomedOut: true,
error: null,
page: 1,
total: null,
} }
}, },
computed: { computed: {
allKeys() { allKeys() {
return this.items.map(this.getKey) return new Set(this.finalItems.map(this.getKey))
}, },
filteredSelected() { filteredSelected() {
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1) return [ ...this.allKeys.values().filter((key) => this.selected.has(key))]
}, },
allSelected() { allSelected() {
return this.filteredSelected.length === this.items.length return this.selected.size === this.finalItems.length
}, },
noneSelected() { noneSelected() {
return this.filteredSelected.length === 0 return this.selected.size === 0
}, },
someSelected() { someSelected() {
return !this.allSelected && !this.noneSelected return !this.allSelected && !this.noneSelected
}, },
finalItems() {
return this.externalItems || this.items
}
}, },
created() { created() {
window.addEventListener('scroll', this.scrollLoad) window.addEventListener('scroll', this.scrollLoad)
@ -89,7 +84,32 @@ const List = {
}, },
methods: { methods: {
fetchEntries() { fetchEntries() {
this.$emit('fetchRequested') if (this.loading) return
this.loading = true
this.error = null
this.fetchFunction(this.page)
.then((result) => {
this.loading = false
this.bottomedOut = isEmpty(result.items)
if (this.externalItems) return
this.page += 1
this.total = result.count
this.items.push(...result.items)
})
.catch((error) => {
this.loading = false
this.error = error
})
},
reset() {
this.items = []
this.page = 1
this.total = null
this.error = null
this.loading = false
this.fetchEntries()
}, },
scrollLoad(e) { scrollLoad(e) {
if (this.fetchFunction) { if (this.fetchFunction) {
@ -111,18 +131,18 @@ const List = {
const oldChecked = this.isSelected(key) const oldChecked = this.isSelected(key)
if (checked !== oldChecked) { if (checked !== oldChecked) {
if (checked) { if (checked) {
this.selected.push(key) this.selected.add(key)
} else { } else {
this.selected.splice(this.selected.indexOf(key), 1) this.selected.delete(key)
} }
} }
this.$emit('selected', this.selected) this.$emit('selected', this.selected)
}, },
toggleAll(value) { toggleAll(value) {
if (value) { if (value) {
this.selected = this.allKeys.slice(0) this.selected = new Set([...this.allKeys])
} else { } else {
this.selected = [] this.selected = new Set([])
} }
this.$emit('selected', this.selected) this.$emit('selected', this.selected)
}, },

View file

@ -28,7 +28,7 @@
role="list" role="list"
> >
<div <div
v-for="item in items" v-for="item in finalItems"
:key="getKey(item)" :key="getKey(item)"
class="list-item" class="list-item"
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']" :class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
@ -50,7 +50,7 @@
/> />
</div> </div>
<div <div
v-if="items.length === 0 && !!$slots.empty" v-if="finalItems.length === 0 && !!$slots.empty"
class="list-empty-content faint" class="list-empty-content faint"
> >
<slot name="empty" /> <slot name="empty" />

View file

@ -2,7 +2,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue' import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
import TextConfirm from 'src/components/confirm_modal/text_confirm.vue' import TextConfirm from 'src/components/confirm_modal/text_confirm.vue'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import PageList from 'src/components/page_list/page_list.vue' import List from 'src/components/list/list.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import AdminStatusCard from 'src/components/settings_modal/admin_tabs/admin_status_card.vue' import AdminStatusCard from 'src/components/settings_modal/admin_tabs/admin_status_card.vue'
@ -132,7 +132,7 @@ const AdminCard = {
components: { components: {
BasicUserCard, BasicUserCard,
Checkbox, Checkbox,
PageList, List,
AdminStatusCard, AdminStatusCard,
Modal, Modal,
Popover, Popover,

View file

@ -28,12 +28,6 @@ const UsersTab = {
filtersName: '', filtersName: '',
filtersEmail: '', filtersEmail: '',
expandedUser: null, expandedUser: null,
loading: false,
error: null,
bottomedOut: false,
users: [],
page: 1,
total: null,
} }
}, },
computed: { computed: {
@ -118,29 +112,14 @@ const UsersTab = {
GenericConfirm, GenericConfirm,
}, },
methods: { methods: {
fetchPage() { fetchUsers(page) {
if (this.loading) return console.log(page)
return this.$store
this.loading = true
this.error = null
this.$store
.dispatch('fetchAdminUsers', { .dispatch('fetchAdminUsers', {
...this.fetchOptions, ...this.fetchOptions,
page: this.page, page,
})
.then((result) => {
console.log('RESULT', result)
this.loading = false
this.bottomedOut = isEmpty(result.users)
this.page += 1
this.total = result.count
this.users.push(...result.users)
})
.catch((error) => {
this.loading = false
this.error = error
}) })
.then(({ count, users }) => ({ count, items: users }))
}, },
/** /**
* show the confirmation box for bulk actions. * show the confirmation box for bulk actions.
@ -170,9 +149,7 @@ const UsersTab = {
}, },
watch: { watch: {
fetchOptions () { fetchOptions () {
this.page = 1 this.$refs.usersList.reset()
this.users = []
this.fetchPage()
} }
} }
} }

View file

@ -121,12 +121,8 @@
</div> </div>
</div> </div>
<List <List
:get-key="i => i" ref="usersList"
:items="users" :fetch-function="fetchUsers"
:loading="loading"
:error="error"
:bottomed-out="bottomedOut"
@fetch-requested="fetchPage"
selectable selectable
scrollable scrollable
> >

View file

@ -7,7 +7,6 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import MuteCard from 'src/components/mute_card/mute_card.vue' import MuteCard from 'src/components/mute_card/mute_card.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
@ -61,24 +60,7 @@ const MutesAndBlocks = {
}, },
methods: { methods: {
fetchItems(group) { fetchItems(group) {
if (this[group + 'Loading']) return return () => this.$store.dispatch('fetch' + group, this.userId)
const capGroup = group[0].toUpperCase() + group.slice(1)
this[group + 'Loading'] = true
this[group + 'Error'] = null
this.$store
.dispatch('fetch' + capGroup, this.userId)
.then((newEntries) => {
this[group + 'Loading'] = false
this[group + 'BottomedOut'] = isEmpty(newEntries)
return newEntries
})
.catch((error) => {
this[group + 'Loading'] = false
this[group + 'Error'] = error
})
}, },
importFollows(file) { importFollows(file) {
return this.$store.state.api.backendInteractor return this.$store.state.api.backendInteractor

View file

@ -22,11 +22,8 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items="blocks" :external-items="blocks"
:loading="blocksLoading" :fetch-function="fetchItems('Blocks')"
:error="blocksError"
:bottomed-out="blocksBottomedOut"
@fetch-requested="fetchItems('blocks')"
scrollable scrollable
selectable selectable
> >
@ -79,11 +76,8 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items="mutes" :external-items="mutes"
:loading="mutesLoading" :fetch-function="fetchItems('Mutes')"
:error="mutesError"
:bottomed-out="mutesBottomedOut"
@fetch-requested="fetchItems('mutes')"
scrollable scrollable
selectable selectable
> >
@ -136,11 +130,8 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items="domains" :external-items="domains"
:loading="domainsLoading" :fetch-function="fetchItems('DomainMutes')"
:error="domainsError"
:bottomed-out="domainsBottomedOut"
@fetch-requested="fetchItems('domainMutes')"
scrollable scrollable
selectable selectable
> >

View file

@ -1,4 +1,4 @@
import { get, isEmpty } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia' import { mapState } from 'pinia'
import Conversation from 'src/components/conversation/conversation.vue' import Conversation from 'src/components/conversation/conversation.vue'
@ -27,12 +27,6 @@ const UserProfile = {
userId: null, userId: null,
tab: defaultTabKey, tab: defaultTabKey,
footerRef: null, footerRef: null,
friendsLoading: false,
friendsError: null,
friendsBottomedOut: false,
followersLoading: false,
followersError: null,
followersBottomedOut: false,
} }
}, },
created() { created() {
@ -109,24 +103,10 @@ const UserProfile = {
this.footerRef = el this.footerRef = el
}, },
fetchUsers(group) { fetchUsers(group) {
if (this[group + 'Loading']) return return () => this
.$store
const capGroup = group[0].toUpperCase() + group.slice(1) .dispatch('fetch' + group, this.userId)
.then((result) => ({ items: result }))
this[group + 'Loading'] = true
this[group + 'Error'] = null
this.$store
.dispatch('fetch' + capGroup, this.userId)
.then((newEntries) => {
this[group + 'Loading'] = false
this[group + 'BottomedOut'] = isEmpty(newEntries)
return newEntries
})
.catch((error) => {
this[group + 'Loading'] = false
this[group + 'Error'] = error
})
}, },
load(userNameOrId) { load(userNameOrId) {
const startFetchingTimeline = (timeline, userId) => { const startFetchingTimeline = (timeline, userId) => {

View file

@ -40,11 +40,8 @@
:disabled="!user.friends_count" :disabled="!user.friends_count"
> >
<List <List
:items="friends" :fetch-function="fetchUsers('Friends')"
:loading="friendsLoading" :external-items="friends"
:error="friendsError"
:bottomed-out="friendsBottomedOut"
@fetch-requested="fetchUsers('friends')"
> >
<template #item="{item}"> <template #item="{item}">
<FollowCard :user="item" /> <FollowCard :user="item" />
@ -59,11 +56,8 @@
:disabled="!user.followers_count" :disabled="!user.followers_count"
> >
<List <List
:items="followers" :fetch-function="fetchUsers('Followers')"
:loading="followersLoading" :external-items="followers"
:error="followersError"
:bottomed-out="followersBottomedOut"
@fetch-requested="fetchUsers('followers')"
> >
<template #item="{item}"> <template #item="{item}">
<FollowCard <FollowCard