fix(radio-builder): render Fomantic UI's dropdown content once
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2459>
This commit is contained in:
parent
8100d83bcf
commit
a26b29d434
|
@ -0,0 +1 @@
|
||||||
|
Fixed Fomantic UI dropdown messing with Vue internals in radio builder (#2142)
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, reactive, watch, watchEffect, onMounted } from 'vue'
|
import { computed, ref, reactive, watch, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
@ -84,7 +84,11 @@ const fetchCandidates = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(filters, fetchCandidates)
|
// NOTE: Whenever we modify filters array, we refetch the candidates automatically
|
||||||
|
watch(filters, fetchCandidates, {
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
const checkErrors = computed(() => checkResult.value?.errors ?? [])
|
const checkErrors = computed(() => checkResult.value?.errors ?? [])
|
||||||
|
|
||||||
const isPublic = ref(true)
|
const isPublic = ref(true)
|
||||||
|
@ -107,6 +111,7 @@ const fetchFilters = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filterId = Number.MIN_SAFE_INTEGER
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
@ -114,10 +119,10 @@ const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`radios/radios/${props.id}/`)
|
const response = await axios.get(`radios/radios/${props.id}/`)
|
||||||
filters.length = 0
|
filters.length = 0
|
||||||
filters.push(...response.data.config.map((filter: FilterConfig) => ({
|
filters.push(...response.data.config.map((config: FilterConfig) => ({
|
||||||
config: filter,
|
config,
|
||||||
filter: availableFilters.find(available => available.type === filter.type),
|
filter: availableFilters.find(available => available.type === config.type),
|
||||||
hash: +new Date()
|
hash: filterId++
|
||||||
})))
|
})))
|
||||||
|
|
||||||
radioName.value = response.data.name
|
radioName.value = response.data.name
|
||||||
|
@ -130,10 +135,10 @@ const fetchData = async () => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFilters().then(() => watchEffect(fetchData))
|
fetchFilters().then(() => fetchData())
|
||||||
|
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
if (currentFilter.value) {
|
if (!currentFilter.value) return
|
||||||
filters.push({
|
filters.push({
|
||||||
config: {} as FilterConfig,
|
config: {} as FilterConfig,
|
||||||
filter: currentFilter.value,
|
filter: currentFilter.value,
|
||||||
|
@ -141,17 +146,8 @@ const add = async () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchCandidates()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfig = async (index: number, field: keyof FilterConfig, value: unknown) => {
|
|
||||||
filters[index].config[field] = value
|
|
||||||
return fetchCandidates()
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteFilter = async (index: number) => {
|
const deleteFilter = async (index: number) => {
|
||||||
filters.splice(index, 1)
|
filters.splice(index, 1)
|
||||||
return fetchCandidates()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = ref(false)
|
const success = ref(false)
|
||||||
|
@ -325,11 +321,8 @@ onMounted(() => {
|
||||||
<builder-filter
|
<builder-filter
|
||||||
v-for="(f, index) in filters"
|
v-for="(f, index) in filters"
|
||||||
:key="f.hash"
|
:key="f.hash"
|
||||||
:index="index"
|
v-model:data="filters[index]"
|
||||||
:config="f.config"
|
@delete="deleteFilter(index)"
|
||||||
:filter="f.filter"
|
|
||||||
@update-config="updateConfig"
|
|
||||||
@delete="deleteFilter"
|
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -6,8 +6,8 @@ import type { Track } from '~/types'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
|
|
||||||
import { useCurrentElement } from '@vueuse/core'
|
import { useCurrentElement, useVModel } from '@vueuse/core'
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
import { clone } from 'lodash-es'
|
import { clone } from 'lodash-es'
|
||||||
|
|
||||||
|
@ -20,35 +20,33 @@ type Filter = { candidates: { count: number, sample: Track[] } }
|
||||||
type ResponseType = { filters: Array<Filter> }
|
type ResponseType = { filters: Array<Filter> }
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'update-config', index: number, name: string, value: number[] | boolean): void
|
(e: 'update:data', name: string, value: number[] | boolean): void
|
||||||
(e: 'delete', index: number): void
|
(e: 'delete'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number
|
data: {
|
||||||
|
|
||||||
filter: BuilderFilter
|
filter: BuilderFilter
|
||||||
config: FilterConfig
|
config: FilterConfig
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits<Events>()
|
const emit = defineEmits<Events>()
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
const data = useVModel(props, 'data', emit)
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const checkResult = ref<Filter | null>(null)
|
const checkResult = ref<Filter | null>(null)
|
||||||
const showCandidadesModal = ref(false)
|
const showCandidadesModal = ref(false)
|
||||||
const exclude = ref(props.config.not)
|
const exclude = computed({
|
||||||
|
get: () => data.value.config.not,
|
||||||
|
set: (value: boolean) => (data.value.config.not = value)
|
||||||
|
})
|
||||||
|
|
||||||
const el = useCurrentElement()
|
const el = useCurrentElement()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
for (const field of props.filter.fields) {
|
for (const field of data.value.filter.fields) {
|
||||||
const selector = ['.dropdown']
|
|
||||||
|
|
||||||
if (field.type === 'list') {
|
|
||||||
selector.push('.multiple')
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings: SemanticUI.DropdownSettings = {
|
const settings: SemanticUI.DropdownSettings = {
|
||||||
onChange (value) {
|
onChange (value) {
|
||||||
value = $(this).dropdown('get value').split(',')
|
value = $(this).dropdown('get value').split(',')
|
||||||
|
@ -57,15 +55,19 @@ onMounted(() => {
|
||||||
value = value.map((number: string) => parseInt(number))
|
value = value.map((number: string) => parseInt(number))
|
||||||
}
|
}
|
||||||
|
|
||||||
value.value = value
|
data.value.config[field.name] = value
|
||||||
emit('update-config', props.index, field.name, value)
|
|
||||||
fetchCandidates()
|
fetchCandidates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selector = field.type === 'list'
|
||||||
|
? '.dropdown.multiple'
|
||||||
|
: '.dropdown'
|
||||||
|
|
||||||
if (field.autocomplete) {
|
if (field.autocomplete) {
|
||||||
selector.push('.autocomplete')
|
selector += '.autocomplete'
|
||||||
// @ts-expect-error custom field?
|
|
||||||
|
// @ts-expect-error Semantic UI types are incomplete
|
||||||
settings.fields = field.autocomplete_fields
|
settings.fields = field.autocomplete_fields
|
||||||
settings.minCharacters = 1
|
settings.minCharacters = 1
|
||||||
settings.apiSettings = {
|
settings.apiSettings = {
|
||||||
|
@ -85,15 +87,15 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(el.value).find(selector.join('')).dropdown(settings)
|
$(el.value).find(selector).dropdown(settings)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchCandidates = async () => {
|
const fetchCandidates = async () => {
|
||||||
const params = {
|
const params = {
|
||||||
filters: [{
|
filters: [{
|
||||||
...clone(props.config),
|
...clone(data.value.config),
|
||||||
type: props.filter.type
|
type: data.value.filter.type
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,11 +108,12 @@ const fetchCandidates = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(exclude, fetchCandidates)
|
watch(exclude, fetchCandidates)
|
||||||
|
fetchCandidates()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ filter.label }}</td>
|
<td>{{ data.filter.label }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="ui toggle checkbox">
|
<div class="ui toggle checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -118,7 +121,6 @@ watch(exclude, fetchCandidates)
|
||||||
v-model="exclude"
|
v-model="exclude"
|
||||||
name="public"
|
name="public"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@change="$emit('update-config', index, 'not', exclude)"
|
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
for="exclude-filter"
|
for="exclude-filter"
|
||||||
|
@ -130,33 +132,34 @@ watch(exclude, fetchCandidates)
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
v-for="f in filter.fields"
|
v-for="f in data.filter.fields"
|
||||||
:key="f.name"
|
:key="f.name"
|
||||||
class="ui field"
|
class="ui field"
|
||||||
>
|
>
|
||||||
<div :class="['ui', 'search', 'selection', 'dropdown', {'autocomplete': f.autocomplete}, {'multiple': f.type === 'list'}]">
|
<div :class="['ui', 'search', 'selection', 'dropdown', { autocomplete: f.autocomplete }, { multiple: f.type === 'list' }]">
|
||||||
<i class="dropdown icon" />
|
<i class="dropdown icon" />
|
||||||
<div class="default text">
|
<div class="default text">
|
||||||
{{ f.placeholder }}
|
{{ f.placeholder }}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="f.type === 'list' && config[f.name as keyof FilterConfig]"
|
v-if="f.type === 'list' && data.config[f.name as keyof FilterConfig]"
|
||||||
:id="f.name"
|
:id="f.name"
|
||||||
:value="(config[f.name as keyof FilterConfig] as string[]).join(',')"
|
:value="(data.config[f.name as keyof FilterConfig] as string[]).join(',')"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="typeof config[f.name as keyof FilterConfig] === 'object'"
|
v-if="typeof data.config[f.name as keyof FilterConfig] === 'object'"
|
||||||
class="ui menu"
|
class="ui menu"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(v, i) in config[f.name as keyof FilterConfig] as object"
|
v-for="(v, i) in data.config[f.name as keyof FilterConfig] as object"
|
||||||
:key="i"
|
v-once
|
||||||
|
:key="data.config.ids?.[i] ?? v"
|
||||||
class="ui item"
|
class="ui item"
|
||||||
:data-value="v"
|
:data-value="v"
|
||||||
>
|
>
|
||||||
<template v-if="config.names">
|
<template v-if="data.config.names">
|
||||||
{{ config.names[i] }}
|
{{ data.config.names[i] }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ v }}
|
{{ v }}
|
||||||
|
@ -170,7 +173,7 @@ watch(exclude, fetchCandidates)
|
||||||
<a
|
<a
|
||||||
v-if="checkResult"
|
v-if="checkResult"
|
||||||
href=""
|
href=""
|
||||||
:class="['ui', {'success': checkResult.candidates.count > 10}, 'label']"
|
:class="['ui', { success: checkResult.candidates.count > 10 }, 'label']"
|
||||||
@click.prevent="showCandidadesModal = !showCandidadesModal"
|
@click.prevent="showCandidadesModal = !showCandidadesModal"
|
||||||
>
|
>
|
||||||
{{ $t('components.library.radios.Filter.matchingTracks', checkResult.candidates.count) }}
|
{{ $t('components.library.radios.Filter.matchingTracks', checkResult.candidates.count) }}
|
||||||
|
@ -200,7 +203,7 @@ watch(exclude, fetchCandidates)
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
class="ui danger button"
|
class="ui danger button"
|
||||||
@click="$emit('delete', index)"
|
@click="emit('delete')"
|
||||||
>
|
>
|
||||||
{{ $t('components.library.radios.Filter.removeButton') }}
|
{{ $t('components.library.radios.Filter.removeButton') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
Loading…
Reference in New Issue