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:
Kasper Seweryn 2023-06-11 22:17:47 +02:00
parent 8100d83bcf
commit a26b29d434
3 changed files with 60 additions and 63 deletions

View File

@ -0,0 +1 @@
Fixed Fomantic UI dropdown messing with Vue internals in radio builder (#2142)

View File

@ -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>

View File

@ -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>