Migrate rest of ordering/pagination mixins to composables
This commit is contained in:
parent
21e5d8ddf0
commit
a8fd0e3f28
|
@ -42,7 +42,7 @@ const logger = useLogger()
|
||||||
const sharedLabels = useSharedLabels()
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
const updateQueryString = () => router.replace({
|
const updateQueryString = () => router.replace({
|
||||||
query: {
|
query: {
|
||||||
|
|
|
@ -47,7 +47,7 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
const sharedLabels = useSharedLabels()
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const updateQueryString = () => router.replace({
|
const updateQueryString = () => router.replace({
|
||||||
|
|
|
@ -47,7 +47,7 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
const sharedLabels = useSharedLabels()
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const updateQueryString = () => router.replace({
|
const updateQueryString = () => router.replace({
|
||||||
|
|
|
@ -50,7 +50,7 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
const sharedLabels = useSharedLabels()
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const updateQueryString = () => router.replace({
|
const updateQueryString = () => router.replace({
|
||||||
|
|
|
@ -42,7 +42,7 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
const sharedLabels = useSharedLabels()
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const updateQueryString = () => router.replace({
|
const updateQueryString = () => router.replace({
|
||||||
|
|
|
@ -34,7 +34,8 @@ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
const actions = []
|
// TODO (wvffle): Find correct type
|
||||||
|
const actions: unknown[] = []
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
@ -65,11 +66,7 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(() => {
|
onSearch(() => (page.value = 1))
|
||||||
page.value = 1
|
|
||||||
fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(page, fetchData)
|
watch(page, fetchData)
|
||||||
onOrderingUpdate(fetchData)
|
onOrderingUpdate(fetchData)
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
|
@ -76,11 +76,7 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(() => {
|
onSearch(() => (page.value = 1))
|
||||||
page.value = 1
|
|
||||||
fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(page, fetchData)
|
watch(page, fetchData)
|
||||||
onOrderingUpdate(fetchData)
|
onOrderingUpdate(fetchData)
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
|
@ -74,11 +74,7 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(() => {
|
onSearch(() => (page.value = 1))
|
||||||
page.value = 1
|
|
||||||
fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(page, fetchData)
|
watch(page, fetchData)
|
||||||
onOrderingUpdate(fetchData)
|
onOrderingUpdate(fetchData)
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
|
@ -81,7 +81,8 @@ const fetchTargets = async () => {
|
||||||
targets[key as keyof typeof targets][payload.id] = {
|
targets[key as keyof typeof targets][payload.id] = {
|
||||||
payload,
|
payload,
|
||||||
currentState: configs[key as keyof typeof targets].fields.reduce((state, field) => {
|
currentState: configs[key as keyof typeof targets].fields.reduce((state, field) => {
|
||||||
state[field.id] = { value: field.getValue(payload) }
|
// TODO (wvffle): This cast may result in an `undefined` key added, make sure to test that.
|
||||||
|
state[field.id as EditObjectType] = { value: field.getValue(payload) }
|
||||||
return state
|
return state
|
||||||
}, {} as Record<EditObjectType, { value: unknown }>)
|
}, {} as Record<EditObjectType, { value: unknown }>)
|
||||||
}
|
}
|
||||||
|
@ -119,17 +120,13 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(() => {
|
onSearch(() => (page.value = 1))
|
||||||
page.value = 1
|
|
||||||
fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(page, fetchData)
|
watch(page, fetchData)
|
||||||
onOrderingUpdate(fetchData)
|
onOrderingUpdate(fetchData)
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
const { $pgettext } = useGettext()
|
const { $pgettext } = useGettext()
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
const labels = computed(() => ({
|
const labels = computed(() => ({
|
||||||
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…')
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…')
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -76,11 +76,7 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(() => {
|
onSearch(() => (page.value = 1))
|
||||||
page.value = 1
|
|
||||||
fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(page, fetchData)
|
watch(page, fetchData)
|
||||||
onOrderingUpdate(fetchData)
|
onOrderingUpdate(fetchData)
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
|
@ -1,16 +1,112 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
||||||
|
import { truncate } from '~/utils/filters'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['name', 'name'],
|
||||||
|
['length', 'length'],
|
||||||
|
['items_count', 'items_count']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Delete'),
|
||||||
|
confirmationMessage: $pgettext('Popup/*/Paragraph', 'The selected tag will be removed and unlinked with existing content, if any. This action is irreversible.'),
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: false,
|
||||||
|
confirmColor: 'danger'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/tags/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by name')
|
||||||
|
}))
|
||||||
|
|
||||||
|
// TODO (wvffle): Check if we can remove the prop
|
||||||
|
const detailedUpload = ref({})
|
||||||
|
const showUploadDetailModal = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui six wide field">
|
<div class="ui six wide field">
|
||||||
<label for="tags-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="tags-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="tags-search"
|
id="tags-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -125,11 +221,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -143,125 +238,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
|
||||||
import { truncate } from '~/utils/filters'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable,
|
|
||||||
ImportStatusModal
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: () => { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels, truncate }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
detailedUpload: {},
|
|
||||||
showUploadDetailModal: false,
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['name', 'name'],
|
|
||||||
['length', 'length'],
|
|
||||||
['items_count', 'items_count']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search.query
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
|
||||||
const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tag will be removed and unlinked with existing content, if any. This action is irreversible.')
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
label: deleteLabel,
|
|
||||||
confirmationMessage: confirmationMessage,
|
|
||||||
isDangerous: true,
|
|
||||||
allowAll: false,
|
|
||||||
confirmColor: 'danger'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/tags/', { params: params }).then((response) => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.result = response.data
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,16 +1,103 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Delete'),
|
||||||
|
confirmationMessage: $pgettext('Popup/*/Paragraph', 'The selected tracks will be removed, as well as associated uploads, favorites and listening history. This action is irreversible.'),
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: false,
|
||||||
|
confirmColor: 'danger'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/library/tracks/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by domain, title, artist, album, MusicBrainz ID…')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui six wide field">
|
<div class="ui six wide field">
|
||||||
<label for="tracks-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="tracks-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="tracks-search"
|
id="tracks-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -177,11 +264,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -195,117 +281,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: () => { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, title, artist, album, MusicBrainz ID…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search.query
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
|
||||||
const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected tracks will be removed, as well as associated uploads, favorites and listening history. This action is irreversible.')
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
label: deleteLabel,
|
|
||||||
confirmationMessage: confirmationMessage,
|
|
||||||
isDangerous: true,
|
|
||||||
allowAll: false,
|
|
||||||
confirmColor: 'danger'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/library/tracks/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,16 +1,118 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
||||||
|
import { humanSize, truncate } from '~/utils/filters'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
import { Upload } from '~/types'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['modification_date', 'modification_date'],
|
||||||
|
['accessed_date', 'accessed_date'],
|
||||||
|
['size', 'size'],
|
||||||
|
['bitrate', 'bitrate'],
|
||||||
|
['duration', 'duration']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Delete'),
|
||||||
|
confirmationMessage: $pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.'),
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: false,
|
||||||
|
confirmColor: 'danger'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/library/uploads/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, reference, source…')
|
||||||
|
}))
|
||||||
|
|
||||||
|
const displayName = (upload: Upload): string => {
|
||||||
|
return upload.filename ?? upload.source ?? upload.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailedUpload = ref({})
|
||||||
|
const showUploadDetailModal = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui six wide field">
|
<div class="ui six wide field">
|
||||||
<label for="uploads-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="uploads-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="uploads-search"
|
id="uploads-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -21,7 +123,7 @@
|
||||||
id="uploads-visibility"
|
id="uploads-visibility"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
:value="getTokenValue('privacy_level', '')"
|
:value="getTokenValue('privacy_level', '')"
|
||||||
@change="addSearchToken('privacy_level', $event.target.value)"
|
@change="addSearchToken('privacy_level', ($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
<translate translate-context="Content/*/Dropdown">
|
<translate translate-context="Content/*/Dropdown">
|
||||||
|
@ -45,7 +147,7 @@
|
||||||
id="uploads-status"
|
id="uploads-status"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
:value="getTokenValue('status', '')"
|
:value="getTokenValue('status', '')"
|
||||||
@change="addSearchToken('status', $event.target.value)"
|
@change="addSearchToken('status', ($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
<translate translate-context="Content/*/Dropdown">
|
<translate translate-context="Content/*/Dropdown">
|
||||||
|
@ -111,6 +213,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- TODO (wvffle): Check if :upload shouldn't be v-modl:upload -->
|
||||||
<import-status-modal
|
<import-status-modal
|
||||||
v-model:show="showUploadDetailModal"
|
v-model:show="showUploadDetailModal"
|
||||||
:upload="detailedUpload"
|
:upload="detailedUpload"
|
||||||
|
@ -286,11 +389,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -304,136 +406,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
|
||||||
import { humanSize, truncate } from '~/utils/filters'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable,
|
|
||||||
ImportStatusModal
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels, humanSize, truncate }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
detailedUpload: {},
|
|
||||||
showUploadDetailModal: false,
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['modification_date', 'modification_date'],
|
|
||||||
['accessed_date', 'accessed_date'],
|
|
||||||
['size', 'size'],
|
|
||||||
['bitrate', 'bitrate'],
|
|
||||||
['duration', 'duration']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, actor, name, reference, source…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search.query
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
|
||||||
const confirmationMessage = this.$pgettext('Popup/*/Paragraph', 'The selected upload will be removed. This action is irreversible.')
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
label: deleteLabel,
|
|
||||||
confirmationMessage: confirmationMessage,
|
|
||||||
isDangerous: true,
|
|
||||||
allowAll: false,
|
|
||||||
confirmColor: 'danger'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/library/uploads/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
},
|
|
||||||
displayName (upload) {
|
|
||||||
if (upload.filename) {
|
|
||||||
return upload.filename
|
|
||||||
}
|
|
||||||
if (upload.source) {
|
|
||||||
return upload.source
|
|
||||||
}
|
|
||||||
return upload.uuid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,16 +1,104 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'first_seen'],
|
||||||
|
['last_fetch_date', 'last_seen'],
|
||||||
|
['preferred_username', 'username'],
|
||||||
|
['domain', 'domain'],
|
||||||
|
['uploads_count', 'uploads']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'purge',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Purge'),
|
||||||
|
isDangerous: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/accounts/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by domain, username, bio…')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui six wide field">
|
<div class="ui six wide field">
|
||||||
<label for="accounts-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="accounts-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="accounts-search"
|
id="accounts-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -150,11 +238,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -168,116 +255,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'first_seen'],
|
|
||||||
['last_fetch_date', 'last_seen'],
|
|
||||||
['preferred_username', 'username'],
|
|
||||||
['domain', 'domain'],
|
|
||||||
['uploads_count', 'uploads']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by domain, username, bio…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search.query
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'purge',
|
|
||||||
label: this.$pgettext('*/*/*/Verb', 'Purge'),
|
|
||||||
isDangerous: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/accounts/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,112 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
|
interface Props extends OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
allowListEnabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
filters: () => ({}),
|
||||||
|
allowListEnabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['name', 'name'],
|
||||||
|
['creation_date', 'first_seen'],
|
||||||
|
['actors_count', 'users'],
|
||||||
|
['outbox_activities_count', 'received_messages']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const query = ref('')
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'purge',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Purge'),
|
||||||
|
isDangerous: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'allow_list_add',
|
||||||
|
label: $pgettext('Content/Moderation/Action/Verb', 'Add to allow-list'),
|
||||||
|
filterCheckable: (obj: { allowed: boolean }) => {
|
||||||
|
return !obj.allowed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'allow_list_remove',
|
||||||
|
label: $pgettext('Content/Moderation/Action/Verb', 'Remove from allow-list'),
|
||||||
|
filterCheckable: (obj: { allowed: boolean }) => {
|
||||||
|
return obj.allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const allowed = ref(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
allowed: allowed.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.allowed === null) {
|
||||||
|
delete params.allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/federation/domains/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(query, () => (page.value = 1), { debounce: 300 })
|
||||||
|
watch(page, fetchData)
|
||||||
|
watch(allowed, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by name…'),
|
||||||
|
allowListTitle: $pgettext('Content/Moderation/Popup', 'This domain is present in your allow-list')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
|
@ -6,7 +115,7 @@
|
||||||
<label for="domains-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="domains-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<input
|
<input
|
||||||
id="domains-search"
|
id="domains-search"
|
||||||
v-model="search"
|
v-model="query"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
|
@ -161,11 +270,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -179,135 +287,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } },
|
|
||||||
allowListEnabled: { type: Boolean, default: false }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: '',
|
|
||||||
allowed: null,
|
|
||||||
orderingOptions: [
|
|
||||||
['name', 'name'],
|
|
||||||
['creation_date', 'first_seen'],
|
|
||||||
['actors_count', 'users'],
|
|
||||||
['outbox_activities_count', 'received_messages']
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by name…'),
|
|
||||||
allowListTitle: this.$pgettext('Content/Moderation/Popup', 'This domain is present in your allow-list')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'purge',
|
|
||||||
label: this.$pgettext('*/*/*/Verb', 'Purge'),
|
|
||||||
isDangerous: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow_list_add',
|
|
||||||
label: this.$pgettext('Content/Moderation/Action/Verb', 'Add to allow-list'),
|
|
||||||
filterCheckable: (obj) => {
|
|
||||||
return !obj.allowed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow_list_remove',
|
|
||||||
label: this.$pgettext('Content/Moderation/Action/Verb', 'Remove from allow-list'),
|
|
||||||
filterCheckable: (obj) => {
|
|
||||||
return obj.allowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
allowed () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const baseFilters = {
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}
|
|
||||||
if (this.allowed !== null) {
|
|
||||||
baseFilters.allowed = this.allowed
|
|
||||||
}
|
|
||||||
const params = merge(baseFilters, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/federation/domains/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,92 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import moment from 'moment'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['expiration_date', 'expiration_date'],
|
||||||
|
['creation_date', 'creation_date']
|
||||||
|
]
|
||||||
|
|
||||||
|
const query = ref('')
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Delete'),
|
||||||
|
filterCheckable: (obj: { users: unknown[], expiration_date: Date }) => {
|
||||||
|
return obj.users.length === 0 && moment().isBefore(obj.expiration_date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
is_open: isOpen.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/users/invitations/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(query, () => (page.value = 1), { debounce: 300 })
|
||||||
|
watch(isOpen, () => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Admin/Input.Placeholder/Verb', 'Search by username, e-mail address, code…')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
|
@ -6,7 +95,7 @@
|
||||||
<label for="invitations-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="invitations-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<input
|
<input
|
||||||
id="invitations-search"
|
id="invitations-search"
|
||||||
v-model="search"
|
v-model="query"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
|
@ -133,11 +222,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -153,120 +241,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import moment from 'moment'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
moment,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: '',
|
|
||||||
isOpen: null,
|
|
||||||
orderingOptions: [
|
|
||||||
['expiration_date', 'expiration_date'],
|
|
||||||
['creation_date', 'creation_date']
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Admin/Input.Placeholder/Verb', 'Search by username, e-mail address, code…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
const deleteLabel = this.$pgettext('*/*/*/Verb', 'Delete')
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
label: deleteLabel,
|
|
||||||
filterCheckable: (obj) => {
|
|
||||||
return obj.users.length === 0 && moment().isBefore(obj.expiration_date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
isOpen () {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search,
|
|
||||||
is_open: this.isOpen,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/users/invitations/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,97 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { watchDebounced } from '@vueuse/core'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface Props extends OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
filters: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
const query = ref('')
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['date_joined', 'date_joined'],
|
||||||
|
['last_activity', 'last_activity'],
|
||||||
|
['username', 'username']
|
||||||
|
]
|
||||||
|
|
||||||
|
const permissions = computed(() => [
|
||||||
|
{
|
||||||
|
code: 'library',
|
||||||
|
label: $pgettext('*/*/*/Noun', 'Library')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'moderation',
|
||||||
|
label: $pgettext('*/Moderation/*', 'Moderation')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'settings',
|
||||||
|
label: $pgettext('*/*/*/Noun', 'Settings')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
// TODO (wvffle): Find correct type
|
||||||
|
const actions: unknown[] = []
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/manage/users/users/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDebounced(query, () => (page.value = 1), { debounce: 300 })
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by username, e-mail address, name…')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
|
@ -6,7 +100,7 @@
|
||||||
<label for="users-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="users-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<input
|
<input
|
||||||
id="users-search"
|
id="users-search"
|
||||||
v-model="search"
|
v-model="query"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
|
@ -176,11 +270,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -196,129 +289,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: '',
|
|
||||||
orderingOptions: [
|
|
||||||
['date_joined', 'date_joined'],
|
|
||||||
['last_activity', 'last_activity'],
|
|
||||||
['username', 'username']
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by username, e-mail address, name…')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
privacyLevels () {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
permissions () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
code: 'library',
|
|
||||||
label: this.$pgettext('*/*/*/Noun', 'Library')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'moderation',
|
|
||||||
label: this.$pgettext('*/Moderation/*', 'Moderation')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: 'settings',
|
|
||||||
label: this.$pgettext('*/*/*/Noun', 'Settings')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
return [
|
|
||||||
// {
|
|
||||||
// name: 'delete',
|
|
||||||
// label: this.$pgettext('Content/Admin/Button.Label/Verb', 'Delete'),
|
|
||||||
// isDangerous: true
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/manage/users/users/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
defaultPage: { type: Number, required: false, default: 1 },
|
|
||||||
defaultPaginateBy: { type: Number, required: false, default: 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -70,6 +70,7 @@ export default () => ({
|
||||||
release_date: $pgettext('Content/*/*/Noun', 'Release date'),
|
release_date: $pgettext('Content/*/*/Noun', 'Release date'),
|
||||||
accessed_date: $pgettext('Content/*/*/Noun', 'Accessed date'),
|
accessed_date: $pgettext('Content/*/*/Noun', 'Accessed date'),
|
||||||
applied_date: $pgettext('Content/*/*/Noun', 'Applied date'),
|
applied_date: $pgettext('Content/*/*/Noun', 'Applied date'),
|
||||||
|
handled_date: $pgettext('Content/*/*/Noun', 'Handled date'),
|
||||||
first_seen: $pgettext('Content/Moderation/Dropdown/Noun', 'First seen date'),
|
first_seen: $pgettext('Content/Moderation/Dropdown/Noun', 'First seen date'),
|
||||||
last_seen: $pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
|
last_seen: $pgettext('Content/Moderation/Dropdown/Noun', 'Last seen date'),
|
||||||
modification_date: $pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
|
modification_date: $pgettext('Content/Playlist/Dropdown/Noun', 'Modification date'),
|
||||||
|
|
|
@ -11,13 +11,13 @@ interface ConfigField {
|
||||||
getValueRepr?: (obj: any) => string
|
getValueRepr?: (obj: any) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditableConfigField extends ConfigField {
|
export interface EditableConfigField extends ConfigField {
|
||||||
id: EditObjectType
|
id: EditObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditObject = Artist | Album | Track
|
export type EditObject = Artist | Album | Track
|
||||||
export type EditObjectType = 'artist' | 'album' | 'track'
|
export type EditObjectType = 'artist' | 'album' | 'track'
|
||||||
type Configs = Record<EditObjectType, { fields: EditableConfigField[] }>
|
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>
|
||||||
|
|
||||||
const { $pgettext } = gettext
|
const { $pgettext } = gettext
|
||||||
const getContentValueRepr = (val: Content) => val.text
|
const getContentValueRepr = (val: Content) => val.text
|
||||||
|
|
|
@ -8,7 +8,7 @@ export interface OrderingProps {
|
||||||
orderingConfigName: RouteWithPreferences | null
|
orderingConfigName: RouteWithPreferences | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (orderingConfigName: MaybeRef<RouteWithPreferences | null>) => {
|
export default (orderingConfigName: MaybeRef<RouteWithPreferences | null>, defaultPaginateBy?: MaybeRef<number>) => {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ export default (orderingConfigName: MaybeRef<RouteWithPreferences | null>) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { paginateBy, ordering, orderingDirection } = toRefs(config)
|
const { paginateBy, ordering, orderingDirection } = toRefs(config)
|
||||||
|
if (defaultPaginateBy !== undefined) {
|
||||||
|
paginateBy.value = unref(defaultPaginateBy)
|
||||||
|
}
|
||||||
|
|
||||||
const orderingString = computed(() => {
|
const orderingString = computed(() => {
|
||||||
if (orderingDirection.value === '-') return `-${ordering.value}`
|
if (orderingDirection.value === '-') return `-${ordering.value}`
|
||||||
|
|
|
@ -19,7 +19,10 @@ export type WebSocketEventName = 'inbox.item_added' | 'import.status_updated' |
|
||||||
| 'report.created' | 'user_request.created' | 'Listen'
|
| 'report.created' | 'user_request.created' | 'Listen'
|
||||||
|
|
||||||
export type OrderingField = 'creation_date' | 'title' | 'album__title' | 'artist__name' | 'release_date' | 'name'
|
export type OrderingField = 'creation_date' | 'title' | 'album__title' | 'artist__name' | 'release_date' | 'name'
|
||||||
| 'applied_date' | 'followers_count' | 'uploads_count'
|
| 'applied_date' | 'followers_count' | 'uploads_count' | 'length' | 'items_count' | 'modification_date' | 'size'
|
||||||
|
| 'accessed_date' | 'bitrate' | 'duration' | 'last_fetch_date' | 'preferred_username' | 'domain' | 'handled_date'
|
||||||
|
| 'album_title' | 'artist_name' | 'actors_count' | 'outbox_activities_count' | 'expiration_date' | 'date_joined'
|
||||||
|
| 'last_activity' | 'username'
|
||||||
|
|
||||||
export type OrderingDirection = '-' | '+'
|
export type OrderingDirection = '-' | '+'
|
||||||
interface RoutePreferences {
|
interface RoutePreferences {
|
||||||
|
|
|
@ -160,6 +160,13 @@ export interface Form {
|
||||||
help_text: Content
|
help_text: Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload stuff
|
||||||
|
export interface Upload {
|
||||||
|
filename?: string
|
||||||
|
source?: string
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
// Yet uncategorized stuff
|
// Yet uncategorized stuff
|
||||||
export interface Actor {
|
export interface Actor {
|
||||||
preferred_username: string
|
preferred_username: string
|
||||||
|
|
|
@ -1,3 +1,89 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ReportCard from '~/components/manage/moderation/ReportCard.vue'
|
||||||
|
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find more types
|
||||||
|
mode?: 'card'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
mode: 'card'
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['applied_date', 'applied_date']
|
||||||
|
]
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('manage/moderation/reports/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
if (query.value === 'resolved:no') {
|
||||||
|
console.log('Refreshing sidebar notifications')
|
||||||
|
store.commit('ui/incrementNotifications', {
|
||||||
|
type: 'pendingReviewReports',
|
||||||
|
value: response.data.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…'),
|
||||||
|
reports: $pgettext('*/Moderation/*/Noun', 'Reports')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main v-title="labels.reports">
|
<main v-title="labels.reports">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
|
@ -11,13 +97,13 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="reports-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="reports-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="reports-search"
|
id="reports-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -28,7 +114,7 @@
|
||||||
id="reports-status"
|
id="reports-status"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
:value="getTokenValue('resolved', '')"
|
:value="getTokenValue('resolved', '')"
|
||||||
@change="addSearchToken('resolved', $event.target.value)"
|
@change="addSearchToken('resolved', ($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
<translate translate-context="Content/*/Dropdown">
|
<translate translate-context="Content/*/Dropdown">
|
||||||
|
@ -114,135 +200,11 @@
|
||||||
<div class="ui center aligned basic segment">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
:current="page"
|
v-model:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import ReportCard from '~/components/manage/moderation/ReportCard.vue'
|
|
||||||
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ReportCard,
|
|
||||||
ReportCategoryDropdown
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
mode: { type: String, default: 'card' }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['applied_date', 'applied_date']
|
|
||||||
],
|
|
||||||
targets: {
|
|
||||||
track: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by account, summary, domain…'),
|
|
||||||
reports: this.$pgettext('*/Moderation/*/Noun', 'Reports')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
this.result = null
|
|
||||||
axios.get('manage/moderation/reports/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
if (self.search.query === 'resolved:no') {
|
|
||||||
console.log('Refreshing sidebar notifications')
|
|
||||||
self.$store.commit('ui/incrementNotifications', { type: 'pendingReviewReports', value: response.data.count })
|
|
||||||
}
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
},
|
|
||||||
handle (type, id, value) {
|
|
||||||
if (type === 'delete') {
|
|
||||||
this.exclude.push(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.result.results.forEach((e) => {
|
|
||||||
if (e.uuid === id) {
|
|
||||||
e.is_approved = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getCurrentState (target) {
|
|
||||||
if (!target) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
if (this.targets[target.type] && this.targets[target.type][String(target.id)]) {
|
|
||||||
return this.targets[target.type][String(target.id)].currentState
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,84 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['handled_date', 'handled_date']
|
||||||
|
]
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('manage/moderation/requests/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
|
||||||
|
if (query.value === 'status:pending') {
|
||||||
|
store.commit('ui/incrementNotifications', {
|
||||||
|
type: 'pendingReviewRequests',
|
||||||
|
value: response.data.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
onOrderingUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Search/Input.Placeholder', 'Search by username…'),
|
||||||
|
reports: $pgettext('*/Moderation/*/Noun', 'User Requests')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main v-title="labels.reports">
|
<main v-title="labels.reports">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
|
@ -11,13 +92,13 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<label for="requests-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
<label for="requests-search"><translate translate-context="Content/Search/Input.Label/Noun">Search</translate></label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="requests-search"
|
id="requests-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -28,7 +109,7 @@
|
||||||
id="requests-status"
|
id="requests-status"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
:value="getTokenValue('status', '')"
|
:value="getTokenValue('status', '')"
|
||||||
@change="addSearchToken('status', $event.target.value)"
|
@change="addSearchToken('status', ($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
<translate translate-context="Content/*/Dropdown">
|
<translate translate-context="Content/*/Dropdown">
|
||||||
|
@ -111,110 +192,12 @@
|
||||||
<div class="ui center aligned basic segment">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result.count > paginateBy"
|
v-if="result.count > paginateBy"
|
||||||
:current="page"
|
v-model:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
UserRequestCard
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
time,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['handled_date', 'handled_date']
|
|
||||||
],
|
|
||||||
targets: {
|
|
||||||
track: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext('Content/Search/Input.Placeholder', 'Search by username…'),
|
|
||||||
reports: this.$pgettext('*/Moderation/*/Noun', 'User Requests')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
orderingDirection () {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
const params = merge({
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.search.query,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}, this.filters)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
this.result = null
|
|
||||||
axios.get('manage/moderation/requests/', { params: params }).then((response) => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
if (self.search.query === 'status:pending') {
|
|
||||||
self.$store.commit('ui/incrementNotifications', { type: 'pendingReviewRequests', value: response.data.count })
|
|
||||||
}
|
|
||||||
}, error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,121 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import time from '~/utils/time'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
||||||
|
import { humanSize, truncate } from '~/utils/filters'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import useSmartSearch, { SmartSearchProps } from '~/composables/useSmartSearch'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends SmartSearchProps, OrderingProps {
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
filters?: object
|
||||||
|
needsRefresh?: boolean
|
||||||
|
// TODO (wvffle): find object type
|
||||||
|
customObjects?: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultQuery: '',
|
||||||
|
updateUrl: false,
|
||||||
|
filters: () => ({}),
|
||||||
|
needsRefresh: false,
|
||||||
|
customObjects: () => []
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(1)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
|
||||||
|
const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props.defaultQuery, props.updateUrl)
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['title', 'track_title'],
|
||||||
|
['size', 'size'],
|
||||||
|
['duration', 'duration'],
|
||||||
|
['bitrate', 'bitrate'],
|
||||||
|
['album_title', 'album_title'],
|
||||||
|
['artist_name', 'artist_name']
|
||||||
|
]
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const actionFilters = computed(() => ({ q: query.value, ...props.filters }))
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
label: $pgettext('*/*/*/Verb', 'Delete'),
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: true,
|
||||||
|
confirmColor: 'danger'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relaunch_import',
|
||||||
|
label: $pgettext('Content/Library/Dropdown/Verb', 'Restart import'),
|
||||||
|
isDangerous: true,
|
||||||
|
allowAll: true,
|
||||||
|
// TODO (wvffle): Find correct type
|
||||||
|
filterCheckable: (filter: { import_status: string }) => {
|
||||||
|
return filter.import_status !== 'finished'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const emit = defineEmits(['fetch-start'])
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
emit('fetch-start')
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
include_channels: 'true',
|
||||||
|
...props.filters
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/uploads/', {
|
||||||
|
params
|
||||||
|
// TODO (wvffle): Check if params should be serialized. In other similar components (Podcasts, Artists) they are
|
||||||
|
// paramsSerializer: function (params) {
|
||||||
|
// return qs.stringify(params, { indices: false })
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(() => (page.value = 1))
|
||||||
|
onOrderingUpdate(() => (page.value = 1))
|
||||||
|
watch(page, fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
searchPlaceholder: $pgettext('Content/Library/Input.Placeholder', 'Search by title, artist, album…'),
|
||||||
|
showStatus: $pgettext('Content/Library/Button.Label/Verb', 'Show information about the upload status for this track')
|
||||||
|
}))
|
||||||
|
|
||||||
|
const detailedUpload = ref({})
|
||||||
|
const showUploadDetailModal = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="ui inline form">
|
<div class="ui inline form">
|
||||||
|
@ -6,13 +124,13 @@
|
||||||
<label for="files-search">
|
<label for="files-search">
|
||||||
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
<translate translate-context="Content/Search/Input.Label/Noun">Search</translate>
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="search.query = $refs.search.value">
|
<form @submit.prevent="query = $refs.search.value">
|
||||||
<input
|
<input
|
||||||
id="files-search"
|
id="files-search"
|
||||||
ref="search"
|
ref="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
:value="search.query"
|
:value="query"
|
||||||
:placeholder="labels.searchPlaceholder"
|
:placeholder="labels.searchPlaceholder"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
|
@ -25,7 +143,7 @@
|
||||||
id="import-status"
|
id="import-status"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
:value="getTokenValue('status', '')"
|
:value="getTokenValue('status', '')"
|
||||||
@change="addSearchToken('status', $event.target.value)"
|
@change="addSearchToken('status', ($event.target as HTMLSelectElement).value)"
|
||||||
>
|
>
|
||||||
<option value>
|
<option value>
|
||||||
<translate translate-context="Content/*/Dropdown">
|
<translate translate-context="Content/*/Dropdown">
|
||||||
|
@ -112,7 +230,7 @@
|
||||||
<div class="ui loader" />
|
<div class="ui loader" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="!result && result.results.length === 0 && !needsRefresh"
|
v-else-if="!result || result?.results.length === 0 && !needsRefresh"
|
||||||
class="ui placeholder segment"
|
class="ui placeholder segment"
|
||||||
>
|
>
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
|
@ -247,11 +365,10 @@
|
||||||
<div>
|
<div>
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.count > paginateBy"
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:current="page"
|
||||||
:compact="true"
|
:compact="true"
|
||||||
:current="page"
|
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="page = $event; fetchData()"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
<span v-if="result && result.results.length > 0">
|
||||||
|
@ -263,158 +380,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import { merge } from 'lodash-es'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
import { normalizeQuery, parseTokens } from '~/utils/search'
|
|
||||||
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
|
|
||||||
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
|
||||||
import { humanSize, truncate } from '~/utils/filters'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Pagination,
|
|
||||||
ActionTable,
|
|
||||||
ImportStatusModal
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, SmartSearchMixin],
|
|
||||||
props: {
|
|
||||||
filters: { type: Object, required: false, default: function () { return {} } },
|
|
||||||
needsRefresh: { type: Boolean, required: false, default: false },
|
|
||||||
customObjects: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels, humanSize, time, truncate }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
detailedUpload: {},
|
|
||||||
showUploadDetailModal: false,
|
|
||||||
isLoading: false,
|
|
||||||
result: null,
|
|
||||||
page: 1,
|
|
||||||
search: {
|
|
||||||
query: this.defaultQuery,
|
|
||||||
tokens: parseTokens(normalizeQuery(this.defaultQuery))
|
|
||||||
},
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['title', 'track_title'],
|
|
||||||
['size', 'size'],
|
|
||||||
['duration', 'duration'],
|
|
||||||
['bitrate', 'bitrate'],
|
|
||||||
['album_title', 'album_title'],
|
|
||||||
['artist_name', 'artist_name']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
searchPlaceholder: this.$pgettext(
|
|
||||||
'Content/Library/Input.Placeholder',
|
|
||||||
'Search by title, artist, album…'
|
|
||||||
),
|
|
||||||
showStatus: this.$pgettext('Content/Library/Button.Label/Verb', 'Show information about the upload status for this track')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actionFilters () {
|
|
||||||
const currentFilters = {
|
|
||||||
q: this.search.query,
|
|
||||||
include_channels: 'true'
|
|
||||||
}
|
|
||||||
if (this.filters) {
|
|
||||||
return merge(currentFilters, this.filters)
|
|
||||||
} else {
|
|
||||||
return currentFilters
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions () {
|
|
||||||
const deleteMsg = this.$pgettext('*/*/*/Verb', 'Delete')
|
|
||||||
const relaunchMsg = this.$pgettext(
|
|
||||||
'Content/Library/Dropdown/Verb',
|
|
||||||
'Restart import'
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
label: deleteMsg,
|
|
||||||
isDangerous: true,
|
|
||||||
allowAll: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'relaunch_import',
|
|
||||||
label: relaunchMsg,
|
|
||||||
isDangerous: true,
|
|
||||||
allowAll: true,
|
|
||||||
filterCheckable: f => {
|
|
||||||
return f.import_status !== 'finished'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
orderingDirection: function () {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
page: function () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
ordering: function () {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
search (newValue) {
|
|
||||||
this.page = 1
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData () {
|
|
||||||
this.$emit('fetch-start')
|
|
||||||
const params = merge(
|
|
||||||
{
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
ordering: this.getOrderingAsString(),
|
|
||||||
q: this.search.query,
|
|
||||||
include_channels: 'true'
|
|
||||||
},
|
|
||||||
this.filters || {}
|
|
||||||
)
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.checked = []
|
|
||||||
axios.get('/uploads/', { params: params }).then(
|
|
||||||
response => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,105 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import $ from 'jquery'
|
||||||
|
import qs from 'qs'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
import PlaylistCardList from '~/components/playlists/CardList.vue'
|
||||||
|
import Pagination from '~/components/vui/Pagination.vue'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import useOrdering, { OrderingProps } from '~/composables/useOrdering'
|
||||||
|
import { OrderingField } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props extends OrderingProps {
|
||||||
|
defaultPage?: number
|
||||||
|
defaultPaginateBy?: number
|
||||||
|
defaultQuery?: string
|
||||||
|
scope?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultPage: 1,
|
||||||
|
defaultPaginateBy: 1,
|
||||||
|
defaultQuery: '',
|
||||||
|
scope: 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(+props.defaultPage)
|
||||||
|
type ResponseType = { count: number, results: any[] }
|
||||||
|
const result = ref<null | ResponseType>(null)
|
||||||
|
const query = ref(props.defaultQuery)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['modification_date', 'modification_date'],
|
||||||
|
['name', 'name']
|
||||||
|
]
|
||||||
|
|
||||||
|
const logger = useLogger()
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName, props.defaultPaginateBy)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const updateQueryString = () => router.replace({
|
||||||
|
query: {
|
||||||
|
query: query.value,
|
||||||
|
page: page.value,
|
||||||
|
paginateBy: paginateBy.value,
|
||||||
|
ordering: orderingString.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(page, updateQueryString)
|
||||||
|
onOrderingUpdate(updateQueryString)
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchData = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
const params = {
|
||||||
|
scope: props.scope,
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
q: query.value,
|
||||||
|
ordering: orderingString.value,
|
||||||
|
playable: true
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.time('Fetching albums')
|
||||||
|
try {
|
||||||
|
const response = await axios.get('playlists/', {
|
||||||
|
params,
|
||||||
|
paramsSerializer: function (params) {
|
||||||
|
return qs.stringify(params, { indices: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
result.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
result.value = null
|
||||||
|
} finally {
|
||||||
|
logger.timeEnd('Fetching albums')
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBeforeRouteUpdate(fetchData)
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
// @ts-expect-error semantic ui
|
||||||
|
onMounted(() => $('.ui.dropdown').dropdown())
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
playlists: $pgettext('*/*/*', 'Playlists'),
|
||||||
|
searchPlaceholder: $pgettext('Content/Playlist/Placeholder/Call to action', 'Enter playlist name…')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main v-title="labels.playlists">
|
<main v-title="labels.playlists">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
|
@ -126,111 +228,11 @@
|
||||||
<div class="ui center aligned basic segment">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="result && result.results.length > 0"
|
v-if="result && result.results.length > 0"
|
||||||
:current="page"
|
v-model:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="result.count"
|
:total="result.count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import $ from 'jquery'
|
|
||||||
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import PaginationMixin from '~/components/mixins/Pagination.vue'
|
|
||||||
import PlaylistCardList from '~/components/playlists/CardList.vue'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
const FETCH_URL = 'playlists/'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
PlaylistCardList,
|
|
||||||
Pagination
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, PaginationMixin],
|
|
||||||
props: {
|
|
||||||
defaultQuery: { type: String, required: false, default: '' },
|
|
||||||
scope: { type: String, required: false, default: 'all' }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isLoading: true,
|
|
||||||
result: null,
|
|
||||||
page: parseInt(this.defaultPage),
|
|
||||||
query: this.defaultQuery,
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['modification_date', 'modification_date'],
|
|
||||||
['name', 'name']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
const playlists = this.$pgettext('*/*/*', 'Playlists')
|
|
||||||
const searchPlaceholder = this.$pgettext('Content/Playlist/Placeholder/Call to action', 'Enter playlist name…')
|
|
||||||
return {
|
|
||||||
playlists,
|
|
||||||
searchPlaceholder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
page () {
|
|
||||||
this.updateQueryString()
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
$('.ui.dropdown').dropdown()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateQueryString: function () {
|
|
||||||
history.pushState(
|
|
||||||
{},
|
|
||||||
null,
|
|
||||||
this.$route.path + '?' + new URLSearchParams(
|
|
||||||
{
|
|
||||||
query: this.query,
|
|
||||||
page: this.page,
|
|
||||||
paginateBy: this.paginateBy,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}).toString()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
fetchData: function () {
|
|
||||||
const self = this
|
|
||||||
this.isLoading = true
|
|
||||||
const url = FETCH_URL
|
|
||||||
const params = {
|
|
||||||
scope: this.scope,
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
q: this.query,
|
|
||||||
ordering: this.getOrderingAsString(),
|
|
||||||
playable: true
|
|
||||||
}
|
|
||||||
axios.get(url, { params: params }).then(response => {
|
|
||||||
self.result = response.data
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
Loading…
Reference in New Issue