fix: the Latest feed's filter dropdowns opened empty
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 1m2s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m54s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m20s

Operator: "the filters in the latest feed, drop down but don't have values". Two separate causes:

- Platform: PostsFilterBar built its items from `platformsStore.platforms`. The platforms store has never had that property; it exposes `list` and `byKey`. The read returned undefined, `|| []` turned that into an empty list, and nothing failed. ArtistsView had copied the same read, so the Browse → Artists platform filter was empty too. Both now read `list` and show platform names rather than raw keys.

- Artist: the autocomplete searched the server only after something was typed (autocomplete returns [] for an empty query by design, which its tests pin). Opening the dropdown therefore showed an empty menu. PostsFilterBar now loads every artist once from a new lightweight `GET /api/artists/names` (id, name, slug; alphabetical; no joins) and filters client-side, so the list is there on open. A deep-linked artist_id now also shows the artist's real name instead of "Artist #id".

Guard: frontend/test/storeUsage.spec.js scans src for `platformsStore.<name>` and fails on any name the store doesn't define, since the frontend CI has no type-checker to catch this. A positive control shows the shipped `platformsStore.platforms` read is flagged, and a vacuity check confirms the scan really walks the tree.

tests/test_api_artists_create.py covers /names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
This commit is contained in:
2026-09-13 22:38:27 -04:00
co-authored by Claude Opus 5
parent 7ff8915147
commit 3fe9d0a612
6 changed files with 140 additions and 53 deletions
@@ -1,21 +1,22 @@
<template>
<div class="fc-posts-filters">
<!-- The full artist list, filtered client-side as you type. This used to
search the server only once something was typed, so opening the
dropdown showed an empty menu. -->
<v-autocomplete
v-model="artistModel"
:items="artistOptions"
:loading="artistLoading"
:search="artistQuery"
item-title="name"
item-value="id"
label="Artist"
density="compact"
hide-details
clearable
no-filter
return-object
no-data-text="No matching artists"
class="fc-posts-filters__artist"
@update:search="onArtistSearch"
@update:model-value="onArtistPicked"
@update:model-value="emitFilters"
/>
<v-select
@@ -39,7 +40,8 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useSourcesStore } from '../../stores/sources.js'
import { useApi } from '../../composables/useApi.js'
import { usePlatformsStore } from '../../stores/platforms.js'
const props = defineProps({
@@ -48,18 +50,19 @@ const props = defineProps({
})
const emit = defineEmits(['update:filters'])
const sourcesStore = useSourcesStore()
const api = useApi()
const platformsStore = usePlatformsStore()
// Artist autocomplete state
const artistQuery = ref('')
const artistOptions = ref([])
const artistLoading = ref(false)
const artistModel = ref(null)
// Platform v-select state
// `list`, not `platforms`: the store has never had a `platforms` property, so
// this read undefined and the dropdown opened empty.
// test/storeUsage.spec.js now fails on any read of a property the store does
// not define.
const platformItems = computed(() =>
(platformsStore.platforms || []).map(p => ({ title: p.key, value: p.key }))
platformsStore.list.map(p => ({ title: p.name || p.key, value: p.key }))
)
const platformModel = ref(props.platform)
@@ -67,29 +70,6 @@ const hasFilters = computed(
() => artistModel.value != null || platformModel.value != null
)
let _searchTimer = null
function onArtistSearch(q) {
artistQuery.value = q
clearTimeout(_searchTimer)
if (!q || !q.trim()) {
artistOptions.value = artistModel.value ? [artistModel.value] : []
return
}
_searchTimer = setTimeout(async () => {
artistLoading.value = true
try {
const results = await sourcesStore.autocompleteArtist(q, 20)
artistOptions.value = results
} finally {
artistLoading.value = false
}
}, 200)
}
function onArtistPicked() {
emitFilters()
}
function emitFilters() {
emit('update:filters', {
artist_id: artistModel.value?.id ?? null,
@@ -100,30 +80,38 @@ function emitFilters() {
function clearAll() {
artistModel.value = null
platformModel.value = null
artistOptions.value = []
artistQuery.value = ''
emitFilters()
}
onMounted(async () => {
await platformsStore.loadAll()
// If a deep-link arrives with an artist_id, hydrate the selected display.
if (props.artistId != null) {
const seed = { id: props.artistId, name: `Artist #${props.artistId}` }
artistModel.value = seed
artistOptions.value = [seed]
// A deep link carries only the id. Once the list is loaded, show the real name
// rather than a placeholder.
function selectArtistById(id) {
if (id == null) {
artistModel.value = null
return
}
if (artistModel.value?.id === id) return
artistModel.value = artistOptions.value.find(a => a.id === id)
|| { id, name: `Artist #${id}` }
}
async function loadArtists() {
artistLoading.value = true
try {
artistOptions.value = await api.get('/api/artists/names')
} catch {
artistOptions.value = []
} finally {
artistLoading.value = false
}
}
onMounted(async () => {
await Promise.all([platformsStore.loadAll(), loadArtists()])
selectArtistById(props.artistId)
})
watch(() => props.artistId, (val) => {
if (val == null) {
artistModel.value = null
} else if (artistModel.value?.id !== val) {
const seed = { id: val, name: `Artist #${val}` }
artistModel.value = seed
artistOptions.value = [seed]
}
})
watch(() => props.artistId, (val) => selectArtistById(val))
watch(() => props.platform, (val) => {
if (platformModel.value !== val) platformModel.value = val || null
+3 -2
View File
@@ -58,8 +58,9 @@ useInfiniteScroll(sentinelEl, () => {
onMounted(async () => {
await platformsStore.loadAll()
platformItems.value = (platformsStore.platforms || []).map(p => ({
title: p.key, value: p.key,
// `list` — the store has no `platforms` property (test/storeUsage.spec.js).
platformItems.value = platformsStore.list.map(p => ({
title: p.name || p.key, value: p.key,
}))
// Apply any deep-linked q (and load). setQuery resets + fetches.
store.setQuery(searchTerm.value || '')