simplify <List> API and move logic outside of it

This commit is contained in:
Henry Jameson 2026-06-08 05:27:47 +03:00
commit a0d5decc49
15 changed files with 209 additions and 510 deletions

View file

@ -1,5 +1,3 @@
import { isEmpty } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
const List = { const List = {
@ -10,7 +8,7 @@ const List = {
}, },
items: { items: {
type: Array, type: Array,
default: () => [], default: [],
}, },
fetchFunction: { fetchFunction: {
type: Function, type: Function,
@ -40,28 +38,37 @@ const List = {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
loading: {
type: Boolean,
default: true,
},
bottomedOut: {
type: Boolean,
default: false,
},
error: {
type: String,
default: null,
},
}, },
emits: ['fetchRequested'],
components: { components: {
Checkbox, Checkbox,
}, },
data() { data() {
return { return {
loading: false,
bottomedOut: false,
error: false,
dynamicItems: this.itemsFunction ? [] : null,
selected: [], selected: [],
} }
}, },
computed: { computed: {
allKeys() { allKeys() {
return this.actualItems.map(this.getKey) return this.items.map(this.getKey)
}, },
filteredSelected() { filteredSelected() {
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1) return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
}, },
allSelected() { allSelected() {
return this.filteredSelected.length === this.actualItems.length return this.filteredSelected.length === this.items.length
}, },
noneSelected() { noneSelected() {
return this.filteredSelected.length === 0 return this.filteredSelected.length === 0
@ -69,54 +76,26 @@ const List = {
someSelected() { someSelected() {
return !this.allSelected && !this.noneSelected return !this.allSelected && !this.noneSelected
}, },
actualItems() {
return this.dynamicItems || this.actualItems
},
}, },
created() { created() {
if (this.fetchFunction) { window.addEventListener('scroll', this.scrollLoad)
window.addEventListener('scroll', this.scrollLoad)
if (this.dynamicItems.length === 0) { if (this.items.length === 0) {
this.fetchEntries() this.fetchEntries()
}
} }
}, },
unmounted() { unmounted() {
window.removeEventListener('scroll', this.scrollLoad) window.removeEventListener('scroll', this.scrollLoad)
}, },
methods: { methods: {
// Entries is not a computed because computed can't track the dynamic
// selector for changes and won't trigger after fetch.
updateEntries(newEntries) {
this.dynamicItems = this.itemsFunction(newEntries)
},
fetchEntries() { fetchEntries() {
if (!this.loading) { this.$emit('fetchRequested')
this.loading = true
this.error = false
this.fetchFunction()
.then((newEntries) => {
this.loading = false
this.bottomedOut = isEmpty(newEntries)
return newEntries
})
.catch((error) => {
this.loading = false
this.error = error
})
.finally((newEntries) => {
this.updateEntries(newEntries)
})
}
}, },
scrollLoad(e) { scrollLoad(e) {
if (this.fetchFunction) { if (this.fetchFunction) {
const bodyBRect = document.body.getBoundingClientRect() const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y) const height = Math.max(bodyBRect.height, -bodyBRect.y)
if ( if (
this.loading === false &&
this.bottomedOut === false &&
this.$el.offsetHeight > 0 && this.$el.offsetHeight > 0 &&
window.innerHeight + window.pageYOffset >= height - 750 window.innerHeight + window.pageYOffset >= height - 750
) { ) {
@ -128,7 +107,6 @@ const List = {
return this.filteredSelected.indexOf(this.getKey(item)) !== -1 return this.filteredSelected.indexOf(this.getKey(item)) !== -1
}, },
toggle(checked, item) { toggle(checked, item) {
console.log('TOGGLE', checked, item)
const key = this.getKey(item) const key = this.getKey(item)
const oldChecked = this.isSelected(key) const oldChecked = this.isSelected(key)
if (checked !== oldChecked) { if (checked !== oldChecked) {
@ -138,6 +116,7 @@ const List = {
this.selected.splice(this.selected.indexOf(key), 1) this.selected.splice(this.selected.indexOf(key), 1)
} }
} }
this.$emit('selected', this.selected)
}, },
toggleAll(value) { toggleAll(value) {
if (value) { if (value) {
@ -145,6 +124,7 @@ const List = {
} else { } else {
this.selected = [] this.selected = []
} }
this.$emit('selected', this.selected)
}, },
}, },
} }

View file

@ -28,7 +28,7 @@
role="list" role="list"
> >
<div <div
v-for="item in actualItems" v-for="item in items"
: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="actualItems.length === 0 && !!$slots.empty" v-if="items.length === 0 && !!$slots.empty"
class="list-empty-content faint" class="list-empty-content faint"
> >
<slot name="empty" /> <slot name="empty" />

View file

@ -1,87 +0,0 @@
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
const PageList = {
components: {
SelectableList,
},
props: {
/**
* only make the checkbox clickable to toggle, not the whole area
*/
boxOnly: {
type: Boolean,
default: false,
},
/**
* how many entries to fetch at once
*/
pageSize: {
type: Number,
default: 50,
},
/**
* the function/callback used to fetch new entries (one page)
*/
fetchPage: {
type: Function,
default: async () => [],
},
/**
* wether or not this is a single page list (so it won't allow fetching more pages)
*/
singlePage: {
type: Boolean,
default: false,
},
},
data() {
return {
pageIndex: 1,
items: [],
canLoadMore: true,
isLoading: false,
}
},
methods: {
/**
* reset and load first page
*/
reset() {
this.canLoadMore = true
this.pageIndex = 1
this.items = []
this.isLoading = false
this.loadMore() // load one page
},
/**
* load another page
*/
loadMore() {
if (!this.isLoading && this.canLoadMore) {
this.isLoading = true
console.log('is loading = true')
this.fetchPage(this.$store, {
page: this.pageIndex++,
pageSize: this.pageSize,
}).then((items) => {
this.items = [...this.items, ...items]
this.isLoading = false
})
}
},
/**
* get currently selected elements
* @returns {Array}
*/
getSelected() {
return this.$refs.list.selected
},
},
/**
* auto-load first page when mounted
*/
mounted() {
this.loadMore()
},
}
export default PageList

View file

@ -1,83 +0,0 @@
<template>
<div class="page-list">
<SelectableList
ref="list"
:box-only="true"
:get-key="i => i"
:items="items"
>
<template #header="slotProps">
<slot
name="header"
v-bind="slotProps"
/>
</template>
<template #item="slotProps">
<slot
name="item"
v-bind="slotProps"
/>
</template>
<template #load="slotProps">
<slot
v-if="isLoading"
name="load"
v-bind="slotProps"
/>
</template>
<template #empty="slotProps">
<slot
v-if="items.length == 0 && !isLoading"
name="empty"
v-bind="slotProps"
/>
</template>
</SelectableList>
<div v-if="!singlePage">
<button
v-if="canLoadMore"
class="button button-default btn"
type="button"
@click="loadMore"
>
{{ $t('page_list.load_more') }}
</button>
</div>
</div>
</template>
<script src="./page_list.js"></script>
<style lang="scss">
.page-list {
--__line-height: 1.5em;
--__horizontal-gap: 0.75em;
--__vertical-gap: 0.5em;
&-item-inner {
display: flex;
align-items: center;
> * {
min-width: 0;
}
}
&-header {
display: flex;
align-items: center;
padding: var(--__vertical-gap) var(--__horizontal-gap);
border-bottom: 1px solid;
border-bottom-color: var(--border);
&-actions {
flex: 1;
}
}
&-checkbox-wrapper {
padding-right: var(--__horizontal-gap);
flex: none;
}
}
</style>

View file

@ -1,70 +0,0 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import List from 'src/components/list/list.vue'
const SelectableList = {
components: {
List,
Checkbox,
},
props: {
boxOnly: {
type: Boolean,
default: false,
},
items: {
type: Array,
default: () => [],
},
getKey: {
type: Function,
default: (item) => item.id,
},
},
data() {
return {
selected: [],
}
},
computed: {
allKeys() {
return this.items.map(this.getKey)
},
filteredSelected() {
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
},
allSelected() {
return this.filteredSelected.length === this.items.length
},
noneSelected() {
return this.filteredSelected.length === 0
},
someSelected() {
return !this.allSelected && !this.noneSelected
},
},
methods: {
isSelected(item) {
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
},
toggle(checked, item) {
const key = this.getKey(item)
const oldChecked = this.isSelected(key)
if (checked !== oldChecked) {
if (checked) {
this.selected.push(key)
} else {
this.selected.splice(this.selected.indexOf(key), 1)
}
}
},
toggleAll(value) {
if (value) {
this.selected = this.allKeys.slice(0)
} else {
this.selected = []
}
},
},
}
export default SelectableList

View file

@ -1,119 +0,0 @@
<template>
<div class="selectable-list">
<div
v-if="items.length > 0"
class="selectable-list-header"
>
<div class="selectable-list-checkbox-wrapper">
<Checkbox
:model-value="allSelected"
:indeterminate="someSelected"
@update:model-value="toggleAll"
>
{{ $t('selectable_list.select_all') }}
</Checkbox>
</div>
<div class="selectable-list-header-actions">
<slot
name="header"
:selected="filteredSelected"
/>
</div>
</div>
<List
:items="items"
:get-key="getKey"
:get-class="item => isSelected(item) ? '-active' : ''"
>
<template #item="{item}">
<div
v-if="!boxOnly"
class="selectable-list-item-inner"
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
@click.stop="toggle(!isSelected(item), item)"
>
<div class="selectable-list-checkbox-wrapper">
<Checkbox
:model-value="isSelected(item)"
@update:model-value="checked => toggle(checked, item)"
@click.stop
/>
</div>
<slot
name="item"
:item="item"
/>
</div>
<div
v-else
class="selectable-list-item-inner"
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
>
<div
class="selectable-list-checkbox-wrapper"
@click.stop="toggle(!isSelected(item), item)"
>
<Checkbox
:model-value="isSelected(item)"
@update:model-value="checked => toggle(checked, item)"
@click.stop
/>
</div>
<slot
name="item"
:item="item"
/>
</div>
</template>
<template #empty>
<slot name="empty" />
</template>
<template #load>
<slot name="load" />
</template>
</List>
</div>
</template>
<script src="./selectable_list.js"></script>
<style lang="scss">
.selectable-list {
--__line-height: 1.5em;
--__horizontal-gap: 0.75em;
--__vertical-gap: 0.5em;
display: flex;
flex-direction: column;
.list {
flex: 1;
}
&-item-inner {
display: flex;
align-items: center;
> * {
min-width: 0;
}
}
&-header {
display: flex;
align-items: center;
padding: var(--__vertical-gap) var(--__horizontal-gap);
border-bottom: 1px solid;
border-bottom-color: var(--border);
&-actions {
flex: 1;
}
}
&-checkbox-wrapper {
padding-right: var(--__horizontal-gap);
flex: none;
}
}
</style>

View file

@ -1,7 +1,9 @@
import { isEmpty } from 'lodash'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' 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 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 ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
@ -17,7 +19,6 @@ const UsersTab = {
}, },
data() { data() {
return { return {
init: false,
filtersOrigin: 'local', filtersOrigin: 'local',
filtersActivity: 'all', filtersActivity: 'all',
filtersPrivileges: 'all', filtersPrivileges: 'all',
@ -28,6 +29,11 @@ const UsersTab = {
filtersEmail: '', filtersEmail: '',
expandedUser: null, expandedUser: null,
loading: false, loading: false,
error: null,
bottomedOut: false,
users: [],
page: 1,
total: null,
} }
}, },
computed: { computed: {
@ -79,26 +85,7 @@ const UsersTab = {
filtersExternal() { filtersExternal() {
return this.filtersOrigin === 'external' return this.filtersOrigin === 'external'
}, },
}, fetchOptions() {
components: {
Checkbox,
Select,
BasicUserCard,
PageList,
ProgressButton,
AdminCard,
TabSwitcher,
Popover,
GenericConfirm,
},
methods: {
/**
* fetch a new page of users via admin-api
* @param {object} store
* @param {object} opts
*/
fetchPage(store, opts) {
if (!this.init) return new Promise(() => [])
const filters = { const filters = {
isAdmin: this.filtersIsAdmin, isAdmin: this.filtersIsAdmin,
isModerator: this.filtersIsModerator, isModerator: this.filtersIsModerator,
@ -109,22 +96,51 @@ const UsersTab = {
needApproval: this.filtersNeedApproval, needApproval: this.filtersNeedApproval,
unconfirmed: this.filtersUnconfirmeUnconfirmed, unconfirmed: this.filtersUnconfirmeUnconfirmed,
} }
const nopts = {
...opts, return {
...{ query: this.filtersQuery,
query: this.filtersQuery, name: this.filtersName,
filters, email: this.filtersEmail,
name: this.filtersName, pageSize: 50,
email: this.filtersEmail, filters,
},
} }
return store.dispatch('fetchAdminUsers', nopts)
}, },
/** },
* reset the userlist explicitly components: {
*/ Checkbox,
reset() { Select,
this.$refs.userList.reset() BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
Popover,
GenericConfirm,
},
methods: {
fetchPage() {
if (this.loading) return
this.loading = true
this.error = null
this.$store
.dispatch('fetchAdminUsers', {
...this.fetchOptions,
page: this.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
})
}, },
/** /**
* show the confirmation box for bulk actions. * show the confirmation box for bulk actions.
@ -147,20 +163,18 @@ const UsersTab = {
u.id !== this.$store.state.users.currentUser.id u.id !== this.$store.state.users.currentUser.id
) { ) {
const uf = this.$store.getters.findUser(u.id) const uf = this.$store.getters.findUser(u.id)
console.log('user: ', uf)
this.$store.dispatch(action, this.$store.getters.findUser(u.id)) this.$store.dispatch(action, this.$store.getters.findUser(u.id))
} }
}) })
this.reset()
}, },
}, },
/** watch: {
* mark as initialized and reset user list fetchOptions () {
*/ this.page = 1
mounted() { this.users = []
this.init = true this.fetchPage()
this.reset() }
}, }
} }
export default UsersTab export default UsersTab

View file

@ -1,6 +1,6 @@
.UsersTab { .UsersTab {
max-height: 100%; max-height: 100%;
display: grid; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: hidden; overflow-y: hidden;

View file

@ -14,7 +14,6 @@
<input <input
v-model="filtersQuery" v-model="filtersQuery"
class="input string-input filter-input" class="input string-input filter-input"
@input="reset()"
> >
</label> </label>
<label class="filter"> <label class="filter">
@ -24,7 +23,6 @@
<input <input
v-model="filtersName" v-model="filtersName"
class="input string-input filter-input" class="input string-input filter-input"
@input="reset()"
> >
</label> </label>
<label class="filter"> <label class="filter">
@ -34,7 +32,6 @@
<input <input
v-model="filtersEmail" v-model="filtersEmail"
class="input string-input filter-input" class="input string-input filter-input"
@input="reset()"
> >
</label> </label>
<div class="filter"> <div class="filter">
@ -43,7 +40,6 @@
</div> </div>
<Select <Select
v-model="filtersOrigin" v-model="filtersOrigin"
@update:model-value="reset"
> >
<option <option
value="all" value="all"
@ -68,7 +64,6 @@
</div> </div>
<Select <Select
v-model="filtersActivity" v-model="filtersActivity"
@update:model-value="reset"
> >
<option <option
value="all" value="all"
@ -91,10 +86,7 @@
<div class="query-label"> <div class="query-label">
{{ $t('admin_dash.users.labels.privileges') }} {{ $t('admin_dash.users.labels.privileges') }}
</div> </div>
<Select <Select v-model="filtersPrivileges">
v-model="filtersPrivileges"
@update:model-value="reset"
>
<option <option
value="all" value="all"
> >
@ -118,27 +110,25 @@
</Select> </Select>
</div> </div>
<div class="filter"> <div class="filter">
<Checkbox <Checkbox v-model="filtersNeedApproval">
@update:model-value="v => {filtersNeedApproval = v; reset();}"
>
{{ $t('admin_dash.users.options.only_unapproved') }} {{ $t('admin_dash.users.options.only_unapproved') }}
</Checkbox> </Checkbox>
</div> </div>
<div class="filter"> <div class="filter">
<Checkbox <Checkbox v-model="filtersUncomfirmed">
@update:model-value="v => {filtersUncomfirmed = v; reset();}"
>
{{ $t('admin_dash.users.options.only_unconfirmed') }} {{ $t('admin_dash.users.options.only_unconfirmed') }}
</Checkbox> </Checkbox>
</div> </div>
</div> </div>
<PageList <List
ref="userList"
:refresh="true"
:get-key="i => i" :get-key="i => i"
:box-only="true" :items="users"
:page-size="20" :loading="loading"
:fetch-page="(store, opts) => fetchPage(store, opts)" :error="error"
:bottomed-out="bottomedOut"
@fetch-requested="fetchPage"
selectable
scrollable
> >
<template #header> <template #header>
<Popover <Popover
@ -264,7 +254,7 @@
<template #empty> <template #empty>
<span> no users </span> <span> no users </span>
</template> </template>
</PageList> </List>
<GenericConfirm <GenericConfirm
ref="confirmActivate" ref="confirmActivate"
:title="$t('admin_dash.users.actions.confirm_multi.title')" :title="$t('admin_dash.users.actions.confirm_multi.title')"

View file

@ -1,6 +1,5 @@
import { get, map, reject } from 'lodash' import { get, map, reject, isEmpty } from 'lodash'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import BlockCard from 'src/components/block_card/block_card.vue' import BlockCard from 'src/components/block_card/block_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
@ -18,6 +17,15 @@ const MutesAndBlocks = {
data() { data() {
return { return {
activeTab: 'profile', activeTab: 'profile',
mutesLoading: false,
mutesError: null,
mutesBottomedOut: false,
blocksLoading: false,
blocksError: null,
blocksBottomedOut: false,
domainsLoading: false,
domainsError: null,
domainsBottomedOut: false,
} }
}, },
created() { created() {
@ -41,8 +49,37 @@ const MutesAndBlocks = {
user() { user() {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
blocks() {
return get(this.$store.state.users.currentUser, 'blockIds', [])
},
mutes() {
return get(this.$store.state.users.currentUser, 'muteIds', [])
},
domains() {
return get(this.$store.state.users.currentUser, 'domainMutes', [])
},
}, },
methods: { methods: {
fetchItems(group) {
if (this[group + 'Loading']) return
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
.importFollows({ file }) .importFollows({ file })
@ -74,15 +111,6 @@ const MutesAndBlocks = {
}) })
.join('\n') .join('\n')
}, },
getBlocks() {
return get(this.$store.state.users.currentUser, 'blockIds', [])
},
getMutes() {
return get(this.$store.state.users.currentUser, 'muteIds', [])
},
getDomainMutes() {
return get(this.$store.state.users.currentUser, 'domainMutes', [])
},
activateTab(tabName) { activateTab(tabName) {
this.activeTab = tabName this.activeTab = tabName
}, },

View file

@ -22,8 +22,11 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items-function="getBlocks" :items="blocks"
:fetch-function="() => $store.dispatch('fetchBlocks')" :loading="blocksLoading"
:error="blocksError"
:bottomed-out="blocksBottomedOut"
@fetch-requested="fetchItems('blocks')"
scrollable scrollable
selectable selectable
> >
@ -76,8 +79,11 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items-function="getMutes" :items="mutes"
:fetch-function="() => $store.dispatch('fetchMutes')" :loading="mutesLoading"
:error="mutesError"
:bottomed-out="mutesBottomedOut"
@fetch-requested="fetchItems('mutes')"
scrollable scrollable
selectable selectable
> >
@ -130,8 +136,11 @@
</div> </div>
<List <List
:get-key="i => i" :get-key="i => i"
:items-function="getDomainMutes" :items="domains"
:fetch-function="() => $store.dispatch('fetchDomainMutes')" :loading="domainsLoading"
:error="domainsError"
:bottomed-out="domainsBottomedOut"
@fetch-requested="fetchItems('domainMutes')"
scrollable scrollable
selectable selectable
> >

View file

@ -1,4 +1,4 @@
import { get } from 'lodash' import { get, isEmpty } 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,6 +27,12 @@ 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() {
@ -83,22 +89,44 @@ const UserProfile = {
compactProfiles() { compactProfiles() {
return useMergedConfigStore().mergedConfig.compactProfiles return useMergedConfigStore().mergedConfig.compactProfiles
}, },
friends() {
return get(
this.$store.getters.findUser(this.userId),
'friendIds',
[],
).map((id) => this.$store.getters.findUser(id))
},
followers() {
return get(
this.$store.getters.findUser(this.userId),
'followerIds',
[],
).map((id) => this.$store.getters.findUser(id))
},
}, },
methods: { methods: {
setFooterRef(el) { setFooterRef(el) {
this.footerRef = el this.footerRef = el
}, },
getFriends() { fetchUsers(group) {
return get( if (this[group + 'Loading']) return
this.$store.getters.findUser(this.userId),
'friendIds', [] const capGroup = group[0].toUpperCase() + group.slice(1)
).map((id) => this.$store.getters.findUser(id))
}, this[group + 'Loading'] = true
getFollowers() { this[group + 'Error'] = null
return get(
this.$store.getters.findUser(this.userId), this.$store
'followerIds', [] .dispatch('fetch' + capGroup, this.userId)
).map((id) => this.$store.getters.findUser(id)) .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,9 +40,11 @@
:disabled="!user.friends_count" :disabled="!user.friends_count"
> >
<List <List
:user-id="userId" :items="friends"
:items-function="getFriends" :loading="friendsLoading"
:fetch-function="() => $store.dispatch('fetchFriends', userId)" :error="friendsError"
:bottomed-out="friendsBottomedOut"
@fetch-requested="fetchUsers('friends')"
> >
<template #item="{item}"> <template #item="{item}">
<FollowCard :user="item" /> <FollowCard :user="item" />
@ -57,9 +59,11 @@
:disabled="!user.followers_count" :disabled="!user.followers_count"
> >
<List <List
:user-id="userId" :items="followers"
:items-function="getFollowers" :loading="followersLoading"
:fetch-function="() => $store.dispatch('fetchFollowers', userId)" :error="followersError"
:bottomed-out="followersBottomedOut"
@fetch-requested="fetchUsers('followers')"
> >
<template #item="{item}"> <template #item="{item}">
<FollowCard <FollowCard

View file

@ -61,11 +61,13 @@ const adminSettingsStorage = {
}, },
actions: { actions: {
async fetchAdminUsers(store, opts) { async fetchAdminUsers(store, opts) {
const users = await store.rootState.api.backendInteractor.adminListUsers({ const data = await store.rootState.api.backendInteractor.adminListUsers({
opts, opts,
}) })
users.forEach((user) => store.dispatch('fetchUserIfMissing', user.id)) data.users.forEach((user) =>
return users store.dispatch('fetchUserIfMissing', user.id),
)
return data
}, },
adminAddUserToAdminGroup(store, user) { adminAddUserToAdminGroup(store, user) {
store.rootState.api.backendInteractor store.rootState.api.backendInteractor

View file

@ -1673,7 +1673,10 @@ const adminListUsers = ({ opts, credentials }) => {
url: url, url: url,
credentials, credentials,
method: 'GET', method: 'GET',
}).then((data) => data.users.map(parseUser)) }).then((data) => ({
...data,
users: data.users.map(parseUser),
}))
} }
const adminAddUserToAdminGroup = ({ user, credentials }) => { const adminAddUserToAdminGroup = ({ user, credentials }) => {