408fcd488a
Operator-flagged (again) on the tag-merge picker: Enter on the dropdown re-opens it instead of accepting the selection. I'd already patched this twice (fandom picker + fandom set dialog) with copy-pasted capture-phase handlers, so DRY it. New composable useAcceptOnEnter(accept): tracks the menu state and, on a capture-phase Enter, lets Vuetify pick when the menu is open but calls accept() (and blocks the re-open) when it's closed. Applied to every confirm-style picker: - TagsView merge-into picker (the reported one) - AliasPickerDialog - PostSeriesMenu add-to-existing - FandomPicker + FandomSetDialog (refactored off their bespoke handlers) One behavior, one place to change it.
133 lines
4.2 KiB
Vue
133 lines
4.2 KiB
Vue
<template>
|
|
<span class="fc-post-series">
|
|
<v-btn
|
|
size="x-small" variant="text" prepend-icon="mdi-book-plus-outline"
|
|
append-icon="mdi-menu-down"
|
|
:aria-label="`Add post ${post.id} to a series`"
|
|
@click.stop="menuOpen = !menuOpen"
|
|
>Series</v-btn>
|
|
<v-menu v-model="menuOpen" activator="parent" :open-on-click="false">
|
|
<v-list density="compact">
|
|
<v-list-item :disabled="busy" @click="onPromote">
|
|
<template #prepend>
|
|
<v-icon size="small">mdi-book-plus</v-icon>
|
|
</template>
|
|
<v-list-item-title>New series from this post</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item @click="openPicker">
|
|
<template #prepend>
|
|
<v-icon size="small">mdi-playlist-plus</v-icon>
|
|
</template>
|
|
<v-list-item-title>Add to existing series…</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<v-dialog v-model="pickerOpen" max-width="460">
|
|
<v-card>
|
|
<v-card-title>Add to series</v-card-title>
|
|
<v-card-text>
|
|
<p class="text-caption mb-2">
|
|
Appends this post as the next chapter of the chosen series.
|
|
</p>
|
|
<v-autocomplete
|
|
v-model="picked"
|
|
v-model:menu="pickerMenuOpen"
|
|
:items="hits"
|
|
:item-title="(s) => s.name"
|
|
:item-value="(s) => s.id"
|
|
:loading="searching"
|
|
label="Series" density="compact" autofocus clearable
|
|
@keydown.enter.capture="onEnter"
|
|
/>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn @click="pickerOpen = false">Cancel</v-btn>
|
|
<v-btn
|
|
color="primary" rounded="pill"
|
|
:disabled="picked == null || busy" @click="onAddExisting"
|
|
>Add</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</span>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useApi } from '../../composables/useApi.js'
|
|
import { useAcceptOnEnter } from '../../composables/useAcceptOnEnter.js'
|
|
import { toast } from '../../utils/toast.js'
|
|
|
|
const props = defineProps({ post: { type: Object, required: true } })
|
|
|
|
const api = useApi()
|
|
const router = useRouter()
|
|
|
|
const menuOpen = ref(false)
|
|
const busy = ref(false)
|
|
const pickerOpen = ref(false)
|
|
const picked = ref(null)
|
|
const hits = ref([])
|
|
const searching = ref(false)
|
|
|
|
// Enter on the closed dropdown adds the chosen series instead of re-opening it.
|
|
// (aliased — `menuOpen` above is the outer "Series ▾" v-menu's state.)
|
|
const { menuOpen: pickerMenuOpen, onEnter } = useAcceptOnEnter(() => {
|
|
if (picked.value != null && !busy.value) onAddExisting()
|
|
})
|
|
|
|
async function onPromote() {
|
|
menuOpen.value = false
|
|
busy.value = true
|
|
try {
|
|
const out = await api.post('/api/series/from-post', {
|
|
body: { post_id: props.post.id }
|
|
})
|
|
toast({ text: `Series created: ${out.name}`, type: 'success' })
|
|
router.push({ name: 'series-manage', params: { tagId: out.series_tag_id } })
|
|
} catch (e) {
|
|
toast({ text: `Could not create series: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
async function openPicker() {
|
|
menuOpen.value = false
|
|
picked.value = null
|
|
hits.value = []
|
|
pickerOpen.value = true
|
|
// Load the full series list so the picker is browsable (the tag autocomplete
|
|
// returns nothing for an empty query — the #712 trap). v-autocomplete filters
|
|
// client-side by name as the operator types.
|
|
searching.value = true
|
|
try {
|
|
const body = await api.get('/api/series', { params: { sort: 'name' } })
|
|
hits.value = body.series || []
|
|
} catch (e) {
|
|
toast({ text: `Could not load series: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
searching.value = false
|
|
}
|
|
}
|
|
|
|
async function onAddExisting() {
|
|
if (picked.value == null) return
|
|
busy.value = true
|
|
try {
|
|
await api.post(`/api/series/${picked.value}/add-post`, {
|
|
body: { post_id: props.post.id }
|
|
})
|
|
pickerOpen.value = false
|
|
toast({ text: 'Added to series', type: 'success' })
|
|
} catch (e) {
|
|
toast({ text: `Could not add to series: ${e.message}`, type: 'error' })
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
</script>
|