page list instead of hoc withloadmore

This commit is contained in:
luce 2025-07-19 14:11:12 +02:00
commit 4a00d89494
9 changed files with 327 additions and 72 deletions

View file

@ -0,0 +1,50 @@
//import Checkbox from 'src/components/checkbox/checkbox.vue'
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
const PageList = {
components: {
SelectableList
},
props: {
boxOnly: {
type: Boolean,
default: false
},
pageSize: {
type: Number,
default: 50
},
fetchPage: {
type: Function,
default: async () => []
}
},
data () {
return {
pageIndex: 1,
items: [],
selected: [],
canLoadMore: true
}
},
methods: {
reset () {
this.canLoadMore = true
this.pageIndex = 1
this.items = []
this.loadMore() // load one page
},
loadMore () {
this.fetchPage(this.$store, {
page: this.pageIndex++,
pageSize: this.pageSize
}).then((items) => this.items = [...this.items, ...items])
// fetch page, add to items
//this.$forceUpdate()
}
},
mounted () {
this.reset()
}
}
export default PageList

View file

@ -0,0 +1,58 @@
<template>
<div class="page-list">
<SelectableList
:box-only="true"
:get-key="i => i"
:items="items"
>
<template v-slot:item="slotProps">
<slot name="item" v-bind="slotProps"/>
</template>
</SelectableList>
<button
v-if="canLoadMore"
class="button button-default btn"
type="button"
@click="loadMore"
>
{{ $t('page_list.load_more') }}
</button>
<p> prev first next </p>
</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

@ -8,6 +8,9 @@ const AdminCard = {
} }
}, },
computed: { computed: {
isLoaded () {
return typeof(this.user) !== 'undefined'
},
user () { user () {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
}, },

View file

@ -1,4 +1,12 @@
<template> <template>
<div
v-if="!isLoaded"
>
loading user...
</div>
<div
v-else
>
<BasicUserCard :user="user"> <BasicUserCard :user="user">
<div class="admin-card-content-container"> <div class="admin-card-content-container">
<!--<button <!--<button
@ -29,6 +37,7 @@
</button>--> </button>-->
</div> </div>
</BasicUserCard> </BasicUserCard>
</div>
</template> </template>
<script src="./admin_card.js"></script> <script src="./admin_card.js"></script>

View file

@ -2,34 +2,11 @@
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
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 withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue' import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
//import { ref } from 'vue' import PageList from 'src/components/page_list/page_list.vue'
const UserList = withLoadMore({
fetch: (props, $store) => {
console.log('fetch', props)
return $store.dispatch('fetchAdminUsers')
},
select: (props, $store) => {
console.log('select', props)
const filterMethod = typeof props.filterMethod === 'function' ? props.filterMethod : () => true
const users = $store.state.users.users.filter(user => user.id !== $store.state.users.currentUser.id && filterMethod(user))
console.log('users', users)
return users
},
destroy: () => {},
childPropName: 'items'
})(SelectableList)
const UserSortStrategy = Object.freeze({
NONE: 0,
NICKNAME: 1,
DISPLAYNAME: 2,
JOINED: 3
})
const UsersTab = { const UsersTab = {
provide () { provide () {
@ -40,13 +17,17 @@ const UsersTab = {
}, },
data() { data() {
return { return {
filterTerms: [], filters: {
users: [], local: false,
sortStrategy: UserSortStrategy.NONE, external: false,
sortAscending: true, active: false,
need_approval: false,
unconfirmed: false,
deactivated: false,
is_admin: false,
is_moderator: false
},
expandedUser: null, expandedUser: null,
filterActive: 'active_only',
filterLocal: 'local_only',
loading: false loading: false
} }
}, },
@ -54,47 +35,39 @@ const UsersTab = {
Checkbox, Checkbox,
Select, Select,
BasicUserCard, BasicUserCard,
UserList, PageList,
ProgressButton, ProgressButton,
AdminCard, AdminCard,
}, },
computed: { computed: {
knownDomains () {
return this.$store.state.instance.knownDomains
},
user () {
return this.$store.state.users.currentUser
}
}, },
methods: { methods: {
delete_selection () {
console.log('delete selection')
},
delete_user () {},
fetch_page (store, opts) {
opts.query = ""
console.log('current filters:', this.filters)
opts.filters = this.filters
opts.name = ""
opts.email = ""
const users = store.dispatch('fetchAdminUsers', opts)
console.log('users', users)
return users
},
reset () {
this.$refs.userList.reset()
},
toggleLocal () {
console.log('toggle local')
this.filters.local = !this.filters.local
this.reset()
}
}, },
mounted() { mounted() {
console.log("mounted") console.log("mounted")
/*const store = this.$store;
const moduleTree = buildModuleTree(store._modules.root);
console.log(JSON.stringify(moduleTree, null, 2));*/
} }
} }
/*function buildModuleTree(module, path = []) {
const fullPath = path.join('/') || 'root';
const moduleInfo = {
path: fullPath,
namespaced: module.namespaced,
state: Object.keys(module.state),
actions: module._rawModule.actions ? Object.keys(module._rawModule.actions) : [],
mutations: module._rawModule.mutations ? Object.keys(module._rawModule.mutations) : [],
getters: module._rawModule.getters ? Object.keys(module._rawModule.getters) : [],
modules: []
};
if (module._children) {
for (const key in module._children) {
const child = module._children[key];
moduleInfo.modules.push(buildModuleTree(child, path.concat(key)));
}
}
return moduleInfo;
}*/
export default UsersTab export default UsersTab

View file

@ -1,14 +1,101 @@
<template> <template>
<div :label="$t('admin_dash.tabs.instance')"> <div :label="$t('admin_dash.tabs.users')">
<UserList <Checkbox
:model-value="filters[local]"
@update:model-value="v => {filters.local = v; reset();}"
>
only local
</Checkbox><br>
<Checkbox
:model-value="filters[external]"
@update:model-value="v => {filters.external = v; reset();}"
>
only external
</Checkbox><br>
<Checkbox
:model-value="filters[active]"
@update:model-value="v => {filters.active = v; reset();}"
>
only active
</Checkbox><br>
<Checkbox
:model-value="filters[need_approval]"
@update:model-value="v => {filters.need_approval = v; reset();}"
>
only unconfirmed
</Checkbox><br>
<Checkbox
:model-value="filters[deactivated]"
@update:model-value="v => {filters.deactivated = v; reset();}"
>
only deactivated
</Checkbox><br>
<Checkbox
:model-value="filters[is_admin]"
@update:model-value="v => {filters.is_admin = v; reset();}"
>
only if admin
</Checkbox><br>
<Checkbox
:model-value="filters[is_moderator]"
@update:model-value="v => {filters.is_moderator = v; reset();}"
>
only if moderator
</Checkbox><br>
<button
class="button button-default btn"
type="button"
@click="delete_selection"
>
{{ $t('admin_dash.users.activate') }}
</button>
<button
class="button button-default btn"
type="button"
@click="activate_selection"
>
{{ $t('admin_dash.users.deactivate') }}
</button>
<button
class="button button-default btn"
type="button"
@click="deactivate_selection"
>
{{ $t('admin_dash.users.delete') }}
</button>
<PageList
ref="userList"
:refresh="true" :refresh="true"
:get-key="i => i" :get-key="i => i"
:boxOnly="true" :box-only="true"
:page-size="50"
:fetch-page="(store, opts) => this.fetch_page(store, opts)"
> >
<template #item="{item}"> <template #item="{item}">
<AdminCard :userId="item.id" /> <AdminCard :user-id="item.id" />
<button
class="button button-default btn"
type="button"
@click="delete_user"
>
{{ $t('admin_dash.users.activate') }}
</button>
<button
class="button button-default btn"
type="button"
@click="activate_user"
>
{{ $t('admin_dash.users.deactivate') }}
</button>
<button
class="button button-default btn"
type="button"
@click="deactivate_user"
>
{{ $t('admin_dash.users.delete') }}
</button>
</template> </template>
</UserList> </PageList>
</div> </div>
</template> </template>
<script src="./users_tab.js"></script> <script src="./users_tab.js"></script>

View file

@ -376,6 +376,9 @@
"selectable_list": { "selectable_list": {
"select_all": "Select all" "select_all": "Select all"
}, },
"page_list": {
"load_more": "Load more"
},
"settings": { "settings": {
"add_language": "Add fallback language", "add_language": "Add fallback language",
"remove_language": "Remove", "remove_language": "Remove",
@ -1138,7 +1141,10 @@
"local_only": "local only", "local_only": "local only",
"external_only": "external only" "external_only": "external only"
}, },
"loading": "Loading..." "loading": "Loading...",
"deactivate": "deactivate",
"activate": "activate",
"delete": "delete"
}, },
"limits": { "limits": {
"arbitrary_limits": "Arbitrary limits", "arbitrary_limits": "Arbitrary limits",

View file

@ -60,8 +60,12 @@ const adminSettingsStorage = {
} }
}, },
actions: { actions: {
fetchAdminUsers (store) { async fetchAdminUsers (store, opts) {
store.rootState.api.backendInteractor.adminListUsers().then((users) => users.forEach(user => store.dispatch('fetchUserIfMissing', user.id))) const users = await store.rootState.api.backendInteractor.adminListUsers({opts})
//await Promise.all(
users.map(user => store.dispatch('fetchUserIfMissing', user.id))
//)
return users
}, },
loadFrontendsStuff ({ rootState, commit }) { loadFrontendsStuff ({ rootState, commit }) {
rootState.api.backendInteractor.fetchAvailableFrontends() rootState.api.backendInteractor.fetchAvailableFrontends()

View file

@ -115,7 +115,30 @@ const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends' const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install' const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
const PLEROMA_ADMIN_USERS_URL = '/api/v1/pleroma/admin/users' const PLEROMA_ADMIN_USERS_URL = ({page, pageSize, filters = {}, query = '', name = '', email = ''}) => {
const {
local = false,
external = false,
active = false,
need_approval = false,
unconfirmed = false,
deactivated = false,
is_admin = true,
is_moderator = true,
} = filters
const filters_str = (local ? 'local,' : '')
+ (external ? 'external,' : '')
+ (active ? 'active,' : '')
+ (need_approval ? 'need_approval,' : '')
+ (unconfirmed ? 'unconfirmed,' : '')
+ (deactivated ? 'deactivated,' : '')
+ (is_admin ? 'is_admin,' : '')
+ (is_moderator ? 'is_moderator,' : '')
return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
}
const PLEROMA_ADMIN_DELETE_USERS_URL = '/api/v1/pleroma/admin/users'
const PLEROMA_ADMIN_ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
const PLEROMA_ADMIN_DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
@ -1441,10 +1464,49 @@ const dismissAnnouncement = ({ id, credentials }) => {
}) })
} }
const adminListUsers = ({ credentials }) => { const adminListUsers = ({ opts, credentials }) => {
// the reported list is hardly useful because standards are for dating i guess, // the reported list is hardly useful because standards are for dating i guess,
// so make sure to fetchIfMissing right afterward using this call // so make sure to fetchIfMissing right afterward using this call
return promisedRequest({ url: PLEROMA_ADMIN_USERS_URL, credentials }).then((data) => data.users.map(parseUser)) const url = PLEROMA_ADMIN_USERS_URL(opts)
console.log('admin api: ', url)
return promisedRequest({ url: url,
credentials,
method: 'GET'
}).then((data) => data.users.map(parseUser))
}
const adminDeleteUser = ({ nicknames, credentials }) => {
return promisedRequest({
url: PLEROMA_ADMIN_DELETE_USERS_URL,
credentials,
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'nicknames': nicknames})
})
}
const adminActivateUser = ({ nicknames, credentials }) => {
return promisedRequest({ url: PLEROMA_ADMIN_ACTIVATE_USER_URL,
credentials,
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'nicknames': nicknames})
})
}
const adminDeactivateUser = ({ nicknames, credentials }) => {
return promisedRequest({ url: PLEROMA_ADMIN_DEACTIVATE_USER_URL,
credentials,
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({'nicknames': nicknames})
})
} }
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => { const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
@ -2120,6 +2182,9 @@ const apiService = {
updateBookmarkFolder, updateBookmarkFolder,
deleteBookmarkFolder, deleteBookmarkFolder,
adminListUsers, adminListUsers,
adminDeleteUser,
adminActivateUser,
adminDeactivateUser,
} }
export default apiService export default apiService