fix(radio-builder): render Fomantic UI's dropdown content once

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2459>
environments/review-front-2157-bqy7y3/deployments/17707
Kasper Seweryn 2023-06-11 22:17:47 +02:00
rodzic 8100d83bcf
commit a26b29d434
3 zmienionych plików z 60 dodań i 63 usunięć

Wyświetl plik

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

Wyświetl plik

@ -1,5 +1,5 @@
<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 { 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 isPublic = ref(true)
@ -107,6 +111,7 @@ const fetchFilters = async () => {
}
}
let filterId = Number.MIN_SAFE_INTEGER
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
@ -114,10 +119,10 @@ const fetchData = async () => {
try {
const response = await axios.get(`radios/radios/${props.id}/`)
filters.length = 0
filters.push(...response.data.config.map((filter: FilterConfig) => ({
config: filter,
filter: availableFilters.find(available => available.type === filter.type),
hash: +new Date()
filters.push(...response.data.config.map((config: FilterConfig) => ({
config,
filter: availableFilters.find(available => available.type === config.type),
hash: filterId++
})))
radioName.value = response.data.name
@ -130,28 +135,19 @@ const fetchData = async () => {
isLoading.value = false
}
fetchFilters().then(() => watchEffect(fetchData))
fetchFilters().then(() => fetchData())
const add = async () => {
if (currentFilter.value) {
filters.push({
config: {} as FilterConfig,
filter: currentFilter.value,
hash: +new Date()
})
}
return fetchCandidates()
}
const updateConfig = async (index: number, field: keyof FilterConfig, value: unknown) => {
filters[index].config[field] = value
return fetchCandidates()
if (!currentFilter.value) return
filters.push({
config: {} as FilterConfig,
filter: currentFilter.value,
hash: +new Date()
})
}
const deleteFilter = async (index: number) => {
filters.splice(index, 1)
return fetchCandidates()
}
const success = ref(false)
@ -325,11 +321,8 @@ onMounted(() => {
<builder-filter
v-for="(f, index) in filters"
:key="f.hash"
:index="index"
:config="f.config"
:filter="f.filter"
@update-config="updateConfig"
@delete="deleteFilter"
v-model:data="filters[index]"
@delete="deleteFilter(index)"
/>
</tbody>
</table>

Wyświetl plik

@ -6,8 +6,8 @@ import type { Track } from '~/types'
import axios from 'axios'
import $ from 'jquery'
import { useCurrentElement } from '@vueuse/core'
import { ref, onMounted, watch } from 'vue'
import { useCurrentElement, useVModel } from '@vueuse/core'
import { ref, onMounted, watch, computed } from 'vue'
import { useStore } from '~/store'
import { clone } from 'lodash-es'
@ -20,35 +20,33 @@ type Filter = { candidates: { count: number, sample: Track[] } }
type ResponseType = { filters: Array<Filter> }
interface Events {
(e: 'update-config', index: number, name: string, value: number[] | boolean): void
(e: 'delete', index: number): void
(e: 'update:data', name: string, value: number[] | boolean): void
(e: 'delete'): void
}
interface Props {
index: number
filter: BuilderFilter
config: FilterConfig
data: {
filter: BuilderFilter
config: FilterConfig
}
}
const emit = defineEmits<Events>()
const props = defineProps<Props>()
const data = useVModel(props, 'data', emit)
const store = useStore()
const checkResult = ref<Filter | null>(null)
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()
onMounted(() => {
for (const field of props.filter.fields) {
const selector = ['.dropdown']
if (field.type === 'list') {
selector.push('.multiple')
}
for (const field of data.value.filter.fields) {
const settings: SemanticUI.DropdownSettings = {
onChange (value) {
value = $(this).dropdown('get value').split(',')
@ -57,15 +55,19 @@ onMounted(() => {
value = value.map((number: string) => parseInt(number))
}
value.value = value
emit('update-config', props.index, field.name, value)
data.value.config[field.name] = value
fetchCandidates()
}
}
let selector = field.type === 'list'
? '.dropdown.multiple'
: '.dropdown'
if (field.autocomplete) {
selector.push('.autocomplete')
// @ts-expect-error custom field?
selector += '.autocomplete'
// @ts-expect-error Semantic UI types are incomplete
settings.fields = field.autocomplete_fields
settings.minCharacters = 1
settings.apiSettings = {
@ -85,15 +87,15 @@ onMounted(() => {
}
}
$(el.value).find(selector.join('')).dropdown(settings)
$(el.value).find(selector).dropdown(settings)
}
})
const fetchCandidates = async () => {
const params = {
filters: [{
...clone(props.config),
type: props.filter.type
...clone(data.value.config),
type: data.value.filter.type
}]
}
@ -106,11 +108,12 @@ const fetchCandidates = async () => {
}
watch(exclude, fetchCandidates)
fetchCandidates()
</script>
<template>
<tr>
<td>{{ filter.label }}</td>
<td>{{ data.filter.label }}</td>
<td>
<div class="ui toggle checkbox">
<input
@ -118,7 +121,6 @@ watch(exclude, fetchCandidates)
v-model="exclude"
name="public"
type="checkbox"
@change="$emit('update-config', index, 'not', exclude)"
>
<label
for="exclude-filter"
@ -130,33 +132,34 @@ watch(exclude, fetchCandidates)
</td>
<td>
<div
v-for="f in filter.fields"
v-for="f in data.filter.fields"
:key="f.name"
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" />
<div class="default text">
{{ f.placeholder }}
</div>
<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"
:value="(config[f.name as keyof FilterConfig] as string[]).join(',')"
:value="(data.config[f.name as keyof FilterConfig] as string[]).join(',')"
type="hidden"
>
<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"
>
<div
v-for="(v, i) in config[f.name as keyof FilterConfig] as object"
:key="i"
v-for="(v, i) in data.config[f.name as keyof FilterConfig] as object"
v-once
:key="data.config.ids?.[i] ?? v"
class="ui item"
:data-value="v"
>
<template v-if="config.names">
{{ config.names[i] }}
<template v-if="data.config.names">
{{ data.config.names[i] }}
</template>
<template v-else>
{{ v }}
@ -170,7 +173,7 @@ watch(exclude, fetchCandidates)
<a
v-if="checkResult"
href=""
:class="['ui', {'success': checkResult.candidates.count > 10}, 'label']"
:class="['ui', { success: checkResult.candidates.count > 10 }, 'label']"
@click.prevent="showCandidadesModal = !showCandidadesModal"
>
{{ $t('components.library.radios.Filter.matchingTracks', checkResult.candidates.count) }}
@ -200,7 +203,7 @@ watch(exclude, fetchCandidates)
<td>
<button
class="ui danger button"
@click="$emit('delete', index)"
@click="emit('delete')"
>
{{ $t('components.library.radios.Filter.removeButton') }}
</button>