mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 23:11:58 +00:00
数据目录 F2b / B13:清单的搜索词、筛选、排序和「只看运行中查询过的表」写进地址的查询参数(q / filter / sort / used),刷新、前进后退都不丢,打开表和回到清单时带着走;点了立刻生效,搜索词停手 200ms 再写地址;补检查脚本的断言
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { ArrowLeft, BookMarked, Database, ListFilter as ListFilterIcon, ScanSearch, Sparkles } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { ApiError, api } from '../../api/client'
|
||||
@@ -16,8 +16,8 @@ import { ProfileSettingsDialog } from './ProfileSettings'
|
||||
import { profileBlockOf, profileSettingsOf, secondsText } from './profile'
|
||||
import { TableDetail } from './TableDetail'
|
||||
import { TableIndex } from './TableIndex'
|
||||
import { filterCounts, progressOf, rowFromDetail, usageSummary, visibleRows } from './model'
|
||||
import type { ListFilter, ListSort } from './model'
|
||||
import { filterCounts, listParamsOf, progressOf, rowFromDetail, usageSummary, visibleRows, writeListParams } from './model'
|
||||
import type { ListFilter, ListParams, ListSort } from './model'
|
||||
|
||||
// ===========================================================================
|
||||
// 数据目录页(/data/catalog/:sourceId,选中一张表时 /data/catalog/:sourceId/:table)。
|
||||
@@ -27,6 +27,9 @@ import type { ListFilter, ListSort } from './model'
|
||||
// 位置;编辑到一半换表、离开页面,走全站统一的离开前确认。
|
||||
//
|
||||
// 宽屏左右两栏;窄屏(< lg)一次只显示一栏:没选表时是清单,选了表是详情(详情里有「返回表清单」)。
|
||||
//
|
||||
// 清单的搜索词、筛选、排序和「只看运行中查询过的表」记在地址的查询参数里(q / filter / sort / used),刷新、前进后退
|
||||
// 都不丢;在清单和表详情之间跳转时带着它们走。
|
||||
// ===========================================================================
|
||||
|
||||
export function CatalogPage() {
|
||||
@@ -62,11 +65,39 @@ export function CatalogPage() {
|
||||
}, [load])
|
||||
useOnReconnect(() => { void load() })
|
||||
|
||||
const [query, setQuery] = useState('')
|
||||
const [filter, setFilter] = useState<ListFilter>('all')
|
||||
const [sort, setSort] = useState<ListSort>('usage')
|
||||
// 清单的状态以页面上的为准(点了立刻生效),再写进地址:搜索词停手 200ms 再写,其余马上写,都替换当前这条历史
|
||||
// (筛选不该占满后退键)。地址被别处改了(前进后退、点了带参数的链接)时按地址重来
|
||||
const [params, setParams] = useSearchParams()
|
||||
const fromUrl = useMemo(() => listParamsOf(params), [params])
|
||||
const urlSig = useMemo(() => writeListParams(new URLSearchParams(), fromUrl).toString(), [fromUrl])
|
||||
const [list, setList] = useState<ListParams>(fromUrl)
|
||||
const wrote = useRef(urlSig)
|
||||
useEffect(() => {
|
||||
if (urlSig === wrote.current) return
|
||||
wrote.current = urlSig
|
||||
setList(fromUrl)
|
||||
}, [urlSig, fromUrl])
|
||||
const listSig = useMemo(() => writeListParams(new URLSearchParams(), list).toString(), [list])
|
||||
const typedAt = useRef(0)
|
||||
useEffect(() => {
|
||||
if (listSig === wrote.current) return
|
||||
const t = setTimeout(() => {
|
||||
wrote.current = listSig
|
||||
setParams((prev) => writeListParams(prev, list), { replace: true })
|
||||
}, Date.now() - typedAt.current < 200 ? 200 : 0)
|
||||
return () => clearTimeout(t)
|
||||
}, [listSig, list, setParams])
|
||||
const { query, filter, sort } = list
|
||||
/** 只看运行中查询过的表:从顶部摘要点进来时打开,筛选结果和摘要的数字一致 */
|
||||
const [usedOnly, setUsedOnly] = useState(false)
|
||||
const usedOnly = list.used
|
||||
const setQuery = useCallback((q: string) => { typedAt.current = Date.now(); setList((c) => ({ ...c, query: q })) }, [])
|
||||
const setFilter = useCallback((f: ListFilter) => setList((c) => ({ ...c, filter: f })), [])
|
||||
const setSort = useCallback((v: ListSort) => setList((c) => ({ ...c, sort: v })), [])
|
||||
const setUsedOnly = useCallback((v: boolean) => setList((c) => ({ ...c, used: v })), [])
|
||||
/** 跳到别的表、回到清单时带上清单的查询参数(按页面上的算:搜索词可能还没写进地址) */
|
||||
const searchRef = useRef('')
|
||||
const search = writeListParams(params, list).toString()
|
||||
searchRef.current = search ? `?${search}` : ''
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set())
|
||||
const [drafting, setDrafting] = useState(false)
|
||||
/** 起草写进了这些表:详情据此重新载入(编辑中的不动,保存时服务端会用版本号拦下) */
|
||||
@@ -94,7 +125,8 @@ export function CatalogPage() {
|
||||
const visible = useMemo(() => visibleRows(rows, query, filter, sort, usedOnly), [rows, query, filter, sort, usedOnly])
|
||||
const counts = useMemo(() => filterCounts(rows, query, usedOnly), [rows, query, usedOnly])
|
||||
const base = `/data/catalog/${encodeURIComponent(sourceId)}`
|
||||
const openTable = useCallback((t: string) => navigate(`${base}/${encodeURIComponent(t)}`), [navigate, base])
|
||||
const openTable = useCallback((t: string) => navigate(`${base}/${encodeURIComponent(t)}${searchRef.current}`), [navigate, base])
|
||||
const backToList = useCallback(() => navigate(`${base}${searchRef.current}`), [navigate, base])
|
||||
const back = source?.origin === 'upload' ? '/data/tables' : '/data/databases'
|
||||
|
||||
/** 写入、审阅之后按返回的单表目录更新清单里那一行 */
|
||||
@@ -198,12 +230,13 @@ export function CatalogPage() {
|
||||
* 正在看表详情时回到清单才看得到筛选结果;宽屏两栏都在,正在看的表不动
|
||||
*/
|
||||
const focusUsed = useCallback((f: ListFilter) => {
|
||||
setQuery('')
|
||||
setFilter(f)
|
||||
setUsedOnly(true)
|
||||
setSort('usage')
|
||||
if (table && typeof matchMedia === 'function' && matchMedia('(max-width: 1023px)').matches) navigate(base)
|
||||
}, [table, navigate, base])
|
||||
const next: ListParams = { query: '', filter: f, used: true, sort: 'usage' }
|
||||
setList(next)
|
||||
if (table && typeof matchMedia === 'function' && matchMedia('(max-width: 1023px)').matches) {
|
||||
const q = writeListParams(params, next).toString()
|
||||
navigate(`${base}${q ? `?${q}` : ''}`)
|
||||
}
|
||||
}, [table, navigate, base, params])
|
||||
|
||||
const filtered = !!query.trim() || filter !== 'all' || usedOnly
|
||||
const empty = rows.length > 0 && rows.every((r) => progressOf(r.counts) === 'none')
|
||||
@@ -290,7 +323,7 @@ export function CatalogPage() {
|
||||
{table
|
||||
? (
|
||||
<TableDetail sourceId={sourceId} table={table} rows={rows} drafted={drafted} fillCodes={fillCodes} prev={prev} next={next}
|
||||
onOpen={openTable} onBack={() => navigate(base)} onDetail={onDetail} />
|
||||
onOpen={openTable} onBack={backToList} onDetail={onDetail} />
|
||||
)
|
||||
: <Overview rows={rows} empty={empty} systemNotes={data.system_notes} draft={draftButton(true)} onOpen={openTable} />}
|
||||
</section>
|
||||
|
||||
@@ -155,6 +155,34 @@ export function filterCounts(rows: CatalogTableRow[], q: string, used = false):
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 清单的筛选、搜索、排序记在地址栏里(刷新、前进后退、把链接发给同事都不丢):q 搜索词,filter 筛选,sort 排序,
|
||||
* used=1 只看运行中查询过的表。默认值不写进地址;认不出的值按默认
|
||||
*/
|
||||
export interface ListParams { query: string; filter: ListFilter; sort: ListSort; used: boolean }
|
||||
|
||||
export function listParamsOf(p: URLSearchParams): ListParams {
|
||||
const filter = p.get('filter') as ListFilter | null
|
||||
const sort = p.get('sort') as ListSort | null
|
||||
return {
|
||||
query: p.get('q') ?? '',
|
||||
filter: filter && LIST_FILTERS.includes(filter) ? filter : 'all',
|
||||
sort: sort && LIST_SORTS.includes(sort) ? sort : 'usage',
|
||||
used: p.get('used') === '1',
|
||||
}
|
||||
}
|
||||
|
||||
/** 把清单的状态写回地址的查询参数:只动这几个键,别的参数原样留着 */
|
||||
export function writeListParams(p: URLSearchParams, s: ListParams): URLSearchParams {
|
||||
const next = new URLSearchParams(p)
|
||||
const put = (k: string, v: string | null) => (v ? next.set(k, v) : next.delete(k))
|
||||
put('q', s.query.trim() ? s.query : null)
|
||||
put('filter', s.filter === 'all' ? null : s.filter)
|
||||
put('sort', s.sort === 'usage' ? null : s.sort)
|
||||
put('used', s.used ? '1' : null)
|
||||
return next
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 批量确认:把选中的推断项改成已确认,整份提交(服务端把「值没动、状态改成确认」当作单项审阅,来源不变)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -580,6 +580,33 @@ await section('数据目录 · 表清单的排序与筛选', async () => {
|
||||
&& names.at(-1) === 'legacy_coupons', names.slice(0, 3).join(','))
|
||||
await page.locator('[data-catalog-sort]').selectOption('usage')
|
||||
|
||||
// 筛选、搜索、排序写进地址栏:刷新不丢,打开一张表、再回到清单都带着
|
||||
await page.locator('[data-filter="pending"]').click()
|
||||
await page.locator('[data-catalog-sort]').selectOption('name')
|
||||
await search.fill('order')
|
||||
await until(async () => new URL(page.url()).searchParams.get('q') === 'order', 3000)
|
||||
const listUrl = new URL(page.url())
|
||||
check('筛选、搜索、排序写进地址栏的查询参数', listUrl.searchParams.get('filter') === 'pending' && listUrl.searchParams.get('sort') === 'name'
|
||||
&& listUrl.searchParams.get('q') === 'order', listUrl.search)
|
||||
await page.reload({ waitUntil: 'networkidle' })
|
||||
await page.locator('[data-catalog-row]').first().waitFor()
|
||||
names = await rowNames(page)
|
||||
check('刷新之后筛选、搜索、排序都还在', (await page.locator('[data-catalog-filter]').getAttribute('data-catalog-filter')) === 'pending'
|
||||
&& await page.locator('[data-catalog-sort]').inputValue() === 'name' && await search.inputValue() === 'order'
|
||||
&& names.join(',') === 'order_items', `${names.join(',')} ${page.url()}`)
|
||||
await page.locator('[data-catalog-row="order_items"] [data-catalog-open-row]').click()
|
||||
await page.waitForURL(`**/data/catalog/${S1}/order_items?**`, { timeout: 5000 }).catch(() => {})
|
||||
const tableUrl = new URL(page.url())
|
||||
check('打开一张表:地址带着清单的查询参数,清单照旧筛着', tableUrl.pathname === `/data/catalog/${S1}/order_items`
|
||||
&& tableUrl.search === listUrl.search && (await rowNames(page)).join(',') === 'order_items', page.url())
|
||||
await page.goBack({ waitUntil: 'networkidle' })
|
||||
check('后退回到清单:筛选照旧', new URL(page.url()).search === listUrl.search
|
||||
&& (await page.locator('[data-catalog-filter]').getAttribute('data-catalog-filter')) === 'pending', page.url())
|
||||
await page.locator('[data-filter="all"]').click()
|
||||
await page.locator('[data-catalog-sort]').selectOption('usage')
|
||||
await search.fill('')
|
||||
check('回到默认(全部、按使用次数、不搜索):地址里不留这几个参数', await until(async () => new URL(page.url()).search === '', 3000), page.url())
|
||||
|
||||
// 键盘:↑↓ 在行之间移动,Enter 打开
|
||||
await page.locator('[data-catalog-row="visits"] [data-catalog-open-row]').focus()
|
||||
await page.keyboard.press('ArrowDown')
|
||||
|
||||
Reference in New Issue
Block a user