Merge feature/search-2: Herkunft filter dropdown on Tiere list + live-search UX confirmed [god-QA: 55/47/e2e 60]
This commit is contained in:
@@ -15,6 +15,22 @@ test('Liste lädt und die Namenssuche filtert', async ({ page }) => {
|
|||||||
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden()
|
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Herkunft-Filter zeigt nur Tiere des gewählten Kontakts (SEARCH-2)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
await page.goto('/rennmaeuse')
|
||||||
|
await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible()
|
||||||
|
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeVisible()
|
||||||
|
|
||||||
|
// Herkunft (originContactId) auf den Seed-Kontakt 'Zoohandlung Meier' (nur Krümel).
|
||||||
|
await page
|
||||||
|
.locator('label.field', { has: page.locator(`span:text-is("${t.fields.origin}")`) })
|
||||||
|
.locator('select')
|
||||||
|
.selectOption({ label: 'Zoohandlung Meier' })
|
||||||
|
|
||||||
|
await expect(page.getByRole('link', { name: /Krümel/ })).toBeVisible()
|
||||||
|
await expect(page.getByRole('link', { name: /Fridolin/ })).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
test('Neue Rennmaus anlegen — Validierung + Erfolg', async ({ page }) => {
|
test('Neue Rennmaus anlegen — Validierung + Erfolg', async ({ page }) => {
|
||||||
await gotoSection(page, de.nav.gerbils)
|
await gotoSection(page, de.nav.gerbils)
|
||||||
await page.getByRole('link', { name: t.newButton }).click()
|
await page.getByRole('link', { name: t.newButton }).click()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { listGerbils, updateGerbil } from '../api/gerbils'
|
import { listGerbils, updateGerbil } from '../api/gerbils'
|
||||||
import { listColorVarieties } from '../api/lookups'
|
import { listColorVarieties, listContacts } from '../api/lookups'
|
||||||
import { andFilter, condition, type GridifyQuery } from '../api/gridify'
|
import { andFilter, condition, type GridifyQuery } from '../api/gridify'
|
||||||
import { GENDERS, GERBIL_STATUSES, type Gender, type GerbilStatus } from '../api/types'
|
import { GENDERS, GERBIL_STATUSES, type Gender, type GerbilStatus } from '../api/types'
|
||||||
import { useApi, useMutation } from '../hooks/useApi'
|
import { useApi, useMutation } from '../hooks/useApi'
|
||||||
@@ -11,6 +11,11 @@ import './gerbils.css'
|
|||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
|
// SEARCH-2: name search field. Switches to Pam's separator-insensitive
|
||||||
|
// normalized field once SEARCH-1 lands (see conversation SEARCH-1); 'name'
|
||||||
|
// works today via the contains hotfix.
|
||||||
|
const NAME_SEARCH_FIELD = 'name'
|
||||||
|
|
||||||
type SortKey = 'nameAsc' | 'nameDesc' | 'birthDesc' | 'birthAsc'
|
type SortKey = 'nameAsc' | 'nameDesc' | 'birthDesc' | 'birthAsc'
|
||||||
const SORT_ORDER_BY: Record<SortKey, string> = {
|
const SORT_ORDER_BY: Record<SortKey, string> = {
|
||||||
nameAsc: 'name',
|
nameAsc: 'name',
|
||||||
@@ -25,6 +30,7 @@ export default function GerbilsPage() {
|
|||||||
const [status, setStatus] = useState<GerbilStatus | ''>('Active')
|
const [status, setStatus] = useState<GerbilStatus | ''>('Active')
|
||||||
const [gender, setGender] = useState<Gender | ''>('')
|
const [gender, setGender] = useState<Gender | ''>('')
|
||||||
const [colorVarietyId, setColorVarietyId] = useState('')
|
const [colorVarietyId, setColorVarietyId] = useState('')
|
||||||
|
const [originContactId, setOriginContactId] = useState('')
|
||||||
const [sort, setSort] = useState<SortKey>('nameAsc')
|
const [sort, setSort] = useState<SortKey>('nameAsc')
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
@@ -34,12 +40,14 @@ export default function GerbilsPage() {
|
|||||||
for (const cv of colorVarieties.data ?? []) map.set(cv.id, cv.name)
|
for (const cv of colorVarieties.data ?? []) map.set(cv.id, cv.name)
|
||||||
return map
|
return map
|
||||||
}, [colorVarieties.data])
|
}, [colorVarieties.data])
|
||||||
|
const contacts = useApi(() => listContacts(), [])
|
||||||
|
|
||||||
const filter = andFilter(
|
const filter = andFilter(
|
||||||
search.trim() && condition({ field: 'name', op: 'contains', value: search.trim() }),
|
search.trim() && condition({ field: NAME_SEARCH_FIELD, op: 'contains', value: search.trim() }),
|
||||||
status && condition({ field: 'status', op: '==', value: status }),
|
status && condition({ field: 'status', op: '==', value: status }),
|
||||||
gender && condition({ field: 'gender', op: '==', value: gender }),
|
gender && condition({ field: 'gender', op: '==', value: gender }),
|
||||||
colorVarietyId && condition({ field: 'colorVarietyId', op: '==', value: colorVarietyId }),
|
colorVarietyId && condition({ field: 'colorVarietyId', op: '==', value: colorVarietyId }),
|
||||||
|
originContactId && condition({ field: 'originContactId', op: '==', value: originContactId }),
|
||||||
)
|
)
|
||||||
const orderBy = SORT_ORDER_BY[sort]
|
const orderBy = SORT_ORDER_BY[sort]
|
||||||
const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE }
|
const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE }
|
||||||
@@ -51,6 +59,7 @@ export default function GerbilsPage() {
|
|||||||
setStatus('Active')
|
setStatus('Active')
|
||||||
setGender('')
|
setGender('')
|
||||||
setColorVarietyId('')
|
setColorVarietyId('')
|
||||||
|
setOriginContactId('')
|
||||||
setPage(1)
|
setPage(1)
|
||||||
setSort('nameAsc')
|
setSort('nameAsc')
|
||||||
}
|
}
|
||||||
@@ -152,6 +161,20 @@ export default function GerbilsPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="field">
|
||||||
|
<span>{t.fields.origin}</span>
|
||||||
|
<select
|
||||||
|
value={originContactId}
|
||||||
|
onChange={(e) => onFilterChange(setOriginContactId)(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{t.filters.all}</option>
|
||||||
|
{(contacts.data ?? []).map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t.filters.sortBy}</span>
|
<span>{t.filters.sortBy}</span>
|
||||||
<select value={sort} onChange={(e) => setSort(e.target.value as SortKey)}>
|
<select value={sort} onChange={(e) => setSort(e.target.value as SortKey)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user