simplify <List> API and move logic outside of it
This commit is contained in:
parent
503309890f
commit
a0d5decc49
15 changed files with 209 additions and 510 deletions
|
|
@ -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)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue