feat(tags): fandom-edit UI in tags directory + image modal
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 13s
CI / frontend-build (push) Successful in 26s
CI / intimp (push) Successful in 3m47s
CI / intapi (push) Successful in 7m52s
CI / intcore (push) Successful in 8m58s

Adds the missing UI to change a character tag's fandom, in both places:

- FandomSetDialog (shared): pick an existing fandom, create a new one, or
  clear it; on a name collision in the target fandom it surfaces a merge
  confirmation and resolves via setFandom(merge:true). Reuses the tags
  store's fandom cache.
- TagCard kebab gains "Set fandom…" for character tags (→ TagsView opens
  the dialog, reloads on success).
- TagPanel chip kebab gains "Set fandom…" for character tags (→ reloads the
  modal's tag list on success).
- tags store: setFandom(tagId, fandomId, {merge}) action + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:21:25 -04:00
parent d9ab6e15c6
commit e678d1dfdf
6 changed files with 258 additions and 2 deletions
+61
View File
@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useTagStore } from '../src/stores/tags.js'
function stubFetch(handler) {
globalThis.fetch = vi.fn(async (url, init) => {
const { status, body } = handler(url, init)
return {
ok: status >= 200 && status < 300,
status,
statusText: String(status),
text: async () => (body == null ? '' : JSON.stringify(body)),
}
})
}
describe('tags store: setFandom', () => {
beforeEach(() => setActivePinia(createPinia()))
afterEach(() => vi.restoreAllMocks())
it('PATCHes fandom_id, and null to clear', async () => {
const s = useTagStore()
const calls = []
stubFetch((url, init) => {
calls.push({ url, init })
return { status: 200, body: { id: 5, name: 'Ichigo', kind: 'character', fandom_id: 9 } }
})
const body = await s.setFandom(5, 9)
expect(body.fandom_id).toBe(9)
const last = calls.at(-1)
expect(last.url).toContain('/api/tags/5')
expect(last.init.method).toBe('PATCH')
expect(JSON.parse(last.init.body)).toEqual({ fandom_id: 9 })
await s.setFandom(5, null)
expect(JSON.parse(calls.at(-1).init.body)).toEqual({ fandom_id: null })
})
it('sends merge: true when requested', async () => {
const s = useTagStore()
const calls = []
stubFetch((url, init) => {
calls.push({ url, init })
return { status: 200, body: { id: 2 } }
})
await s.setFandom(7, 3, { merge: true })
expect(JSON.parse(calls.at(-1).init.body)).toEqual({ fandom_id: 3, merge: true })
})
it('throws ApiError carrying the 409 collision body', async () => {
const s = useTagStore()
stubFetch(() => ({
status: 409,
body: { error: 'exists', target: { id: 42, name: 'Renji' } },
}))
await expect(s.setFandom(1, 2)).rejects.toMatchObject({
status: 409,
body: { target: { id: 42 } },
})
})
})