refactor: migrate query params to nuqs

This commit is contained in:
yyh 2025-12-30 12:36:51 +08:00
parent 3505516e8e
commit e97857ef7f
No known key found for this signature in database
5 changed files with 102 additions and 154 deletions

View File

@ -4,9 +4,13 @@ import type { App } from '@/types/app'
import { useDebounce } from 'ahooks'
import dayjs from 'dayjs'
import { omit } from 'es-toolkit/compat'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import {
parseAsInteger,
parseAsString,
useQueryStates,
} from 'nuqs'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import Pagination from '@/app/components/base/pagination'
@ -28,54 +32,38 @@ export type QueryParam = {
sort_by?: string
}
const defaultQueryParams: QueryParam = {
period: '2',
annotation_status: 'all',
sort_by: '-created_at',
}
const logsStateCache = new Map<string, {
queryParams: QueryParam
currPage: number
limit: number
}>()
const Logs: FC<ILogsProps> = ({ appDetail }) => {
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const getPageFromParams = useCallback(() => {
const pageParam = Number.parseInt(searchParams.get('page') || '1', 10)
if (Number.isNaN(pageParam) || pageParam < 1)
return 0
return pageParam - 1
}, [searchParams])
const cachedState = logsStateCache.get(appDetail.id)
const [queryParams, setQueryParams] = useState<QueryParam>(cachedState?.queryParams ?? defaultQueryParams)
const [currPage, setCurrPage] = React.useState<number>(() => cachedState?.currPage ?? getPageFromParams())
const [limit, setLimit] = React.useState<number>(cachedState?.limit ?? APP_PAGE_LIMIT)
const [queryParams, setQueryParams] = useQueryStates(
{
page: parseAsInteger.withDefault(1),
limit: parseAsInteger.withDefault(APP_PAGE_LIMIT),
period: parseAsString.withDefault('2'),
annotation_status: parseAsString.withDefault('all'),
keyword: parseAsString,
sort_by: parseAsString.withDefault('-created_at'),
},
{
urlKeys: {
page: 'page',
limit: 'limit',
period: 'period',
annotation_status: 'annotation_status',
keyword: 'keyword',
sort_by: 'sort_by',
},
},
)
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
useEffect(() => {
const pageFromParams = getPageFromParams()
setCurrPage(prev => (prev === pageFromParams ? prev : pageFromParams))
}, [getPageFromParams])
useEffect(() => {
logsStateCache.set(appDetail.id, {
queryParams,
currPage,
limit,
})
}, [appDetail.id, currPage, limit, queryParams])
// Get the app type first
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION
const query = {
page: currPage + 1,
limit,
page: queryParams.page,
limit: queryParams.limit,
...((debouncedQueryParams.period !== '9')
? {
start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'),
@ -83,7 +71,8 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
}
: {}),
...(isChatMode ? { sort_by: debouncedQueryParams.sort_by } : {}),
...omit(debouncedQueryParams, ['period']),
...omit(debouncedQueryParams, ['period', 'page', 'limit']),
keyword: debouncedQueryParams.keyword || undefined,
}
// When the details are obtained, proceed to the next request
@ -100,27 +89,25 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
const total = isChatMode ? chatConversations?.total : completionConversations?.total
const handleQueryParamsChange = useCallback((next: QueryParam) => {
setCurrPage(0)
setQueryParams(next)
}, [])
setQueryParams({
...next,
page: 1, // Reset to page 1 on filter change
})
}, [setQueryParams])
const handlePageChange = useCallback((page: number) => {
setCurrPage(page)
const params = new URLSearchParams(searchParams.toString())
const nextPageValue = page + 1
if (nextPageValue === 1)
params.delete('page')
else
params.set('page', String(nextPageValue))
const queryString = params.toString()
router.replace(queryString ? `${pathname}?${queryString}` : pathname, { scroll: false })
}, [pathname, router, searchParams])
setQueryParams({ page: page + 1 })
}, [setQueryParams])
const handleLimitChange = useCallback((limit: number) => {
setQueryParams({ limit, page: 1 })
}, [setQueryParams])
return (
<div className="flex h-full grow flex-col">
<p className="system-sm-regular shrink-0 text-text-tertiary">{t('description', { ns: 'appLog' })}</p>
<div className="flex max-h-[calc(100%-16px)] flex-1 grow flex-col py-4">
<Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={handleQueryParamsChange} />
<Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={{ ...queryParams, keyword: queryParams.keyword || undefined }} setQueryParams={handleQueryParamsChange} />
{total === undefined
? <Loading type="app" />
: total > 0
@ -130,11 +117,11 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
{(total && total > APP_PAGE_LIMIT)
? (
<Pagination
current={currPage}
current={queryParams.page - 1}
onChange={handlePageChange}
total={total}
limit={limit}
onLimitChange={setLimit}
limit={queryParams.limit}
onLimitChange={handleLimitChange}
/>
)
: null}

View File

@ -11,6 +11,7 @@ import type {
import { useLocalStorageState } from 'ahooks'
import { noop } from 'es-toolkit/compat'
import { produce } from 'immer'
import { parseAsString, useQueryState } from 'nuqs'
import {
useCallback,
useEffect,
@ -82,12 +83,10 @@ export const useEmbeddedChatbot = () => {
setConversationId(embeddedConversationId || undefined)
}, [embeddedConversationId])
const [localeParam] = useQueryState('locale', parseAsString)
useEffect(() => {
const setLanguageFromParams = async () => {
// Check URL parameters for language override
const urlParams = new URLSearchParams(window.location.search)
const localeParam = urlParams.get('locale')
// Check for encoded system variables
const systemVariables = await getProcessedSystemVariablesFromUrlParams()
const localeFromSysVar = systemVariables.locale
@ -107,7 +106,7 @@ export const useEmbeddedChatbot = () => {
}
setLanguageFromParams()
}, [appInfo])
}, [appInfo, localeParam])
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
defaultValue: {},

View File

@ -1,12 +1,13 @@
import { useBoolean } from 'ahooks'
import Cookies from 'js-cookie'
import { useSearchParams } from 'next/navigation'
import { parseAsString, useQueryState } from 'nuqs'
import { useCallback } from 'react'
import { PARTNER_STACK_CONFIG } from '@/config'
import { useBindPartnerStackInfo } from '@/service/use-billing'
const usePSInfo = () => {
const searchParams = useSearchParams()
const [partnerKey] = useQueryState('ps_partner_key', parseAsString)
const [clickId] = useQueryState('ps_xid', parseAsString)
const psInfoInCookie = (() => {
try {
return JSON.parse(Cookies.get(PARTNER_STACK_CONFIG.cookieName) || '{}')
@ -16,8 +17,8 @@ const usePSInfo = () => {
return {}
}
})()
const psPartnerKey = searchParams.get('ps_partner_key') || psInfoInCookie?.partnerKey
const psClickId = searchParams.get('ps_xid') || psInfoInCookie?.clickId
const psPartnerKey = partnerKey || psInfoInCookie?.partnerKey
const psClickId = clickId || psInfoInCookie?.clickId
const isPSChanged = psInfoInCookie?.partnerKey !== psPartnerKey || psInfoInCookie?.clickId !== psClickId
const [hasBind, {
setTrue: setBind,

View File

@ -1,7 +1,6 @@
import type { ReadonlyURLSearchParams } from 'next/navigation'
import type { SortType } from '@/service/datasets'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useCallback, useMemo } from 'react'
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
import { useMemo } from 'react'
import { sanitizeStatusValue } from '../status-filter'
const ALLOWED_SORT_VALUES: SortType[] = ['-created_at', 'created_at', '-hit_count', 'hit_count']
@ -29,89 +28,52 @@ const DEFAULT_QUERY: DocumentListQuery = {
sort: '-created_at',
}
// Parse the query parameters from the URL search string.
function parseParams(params: ReadonlyURLSearchParams): DocumentListQuery {
const page = Number.parseInt(params.get('page') || '1', 10)
const limit = Number.parseInt(params.get('limit') || '10', 10)
const keyword = params.get('keyword') || ''
const status = sanitizeStatusValue(params.get('status'))
const sort = sanitizeSortValue(params.get('sort'))
function useDocumentListQueryState() {
const [query, setQuery] = useQueryStates(
{
page: parseAsInteger.withDefault(DEFAULT_QUERY.page),
limit: parseAsInteger.withDefault(DEFAULT_QUERY.limit),
keyword: parseAsString.withDefault(DEFAULT_QUERY.keyword),
status: parseAsString.withDefault(DEFAULT_QUERY.status),
sort: parseAsString.withDefault(DEFAULT_QUERY.sort),
},
{
urlKeys: {
page: 'page',
limit: 'limit',
keyword: 'keyword',
status: 'status',
sort: 'sort',
},
},
)
const finalQuery = useMemo(() => {
const page = query.page > 0 ? query.page : 1
const limit = (query.limit > 0 && query.limit <= 100) ? query.limit : 10
return {
...query,
page,
limit,
status: sanitizeStatusValue(query.status),
sort: sanitizeSortValue(query.sort),
}
}, [query])
const updateQuery = (updates: Partial<DocumentListQuery>) => {
setQuery(prev => ({ ...prev, ...updates }))
}
const resetQuery = () => {
setQuery(DEFAULT_QUERY)
}
return {
page: page > 0 ? page : 1,
limit: (limit > 0 && limit <= 100) ? limit : 10,
keyword: keyword ? decodeURIComponent(keyword) : '',
status,
sort,
}
}
// Update the URL search string with the given query parameters.
function updateSearchParams(query: DocumentListQuery, searchParams: URLSearchParams) {
const { page, limit, keyword, status, sort } = query || {}
const hasNonDefaultParams = (page && page > 1) || (limit && limit !== 10) || (keyword && keyword.trim())
if (hasNonDefaultParams) {
searchParams.set('page', (page || 1).toString())
searchParams.set('limit', (limit || 10).toString())
}
else {
searchParams.delete('page')
searchParams.delete('limit')
}
if (keyword && keyword.trim())
searchParams.set('keyword', encodeURIComponent(keyword))
else
searchParams.delete('keyword')
const sanitizedStatus = sanitizeStatusValue(status)
if (sanitizedStatus && sanitizedStatus !== 'all')
searchParams.set('status', sanitizedStatus)
else
searchParams.delete('status')
const sanitizedSort = sanitizeSortValue(sort)
if (sanitizedSort !== '-created_at')
searchParams.set('sort', sanitizedSort)
else
searchParams.delete('sort')
}
function useDocumentListQueryState() {
const searchParams = useSearchParams()
const query = useMemo(() => parseParams(searchParams), [searchParams])
const router = useRouter()
const pathname = usePathname()
// Helper function to update specific query parameters
const updateQuery = useCallback((updates: Partial<DocumentListQuery>) => {
const newQuery = { ...query, ...updates }
newQuery.status = sanitizeStatusValue(newQuery.status)
newQuery.sort = sanitizeSortValue(newQuery.sort)
const params = new URLSearchParams()
updateSearchParams(newQuery, params)
const search = params.toString()
const queryString = search ? `?${search}` : ''
router.push(`${pathname}${queryString}`, { scroll: false })
}, [query, router, pathname])
// Helper function to reset query to defaults
const resetQuery = useCallback(() => {
const params = new URLSearchParams()
updateSearchParams(DEFAULT_QUERY, params)
const search = params.toString()
const queryString = search ? `?${search}` : ''
router.push(`${pathname}${queryString}`, { scroll: false })
}, [router, pathname])
return useMemo(() => ({
query,
query: finalQuery,
updateQuery,
resetQuery,
}), [query, updateQuery, resetQuery])
}
}
export default useDocumentListQueryState

View File

@ -1,13 +1,12 @@
'use client'
import { useSearchParams } from 'next/navigation'
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import usePSInfo from '../components/billing/partner-stack/use-ps-info'
import NormalForm from './normal-form'
import OneMoreStep from './one-more-step'
const SignIn = () => {
const searchParams = useSearchParams()
const step = searchParams.get('step')
const [step] = useQueryState('step', parseAsString)
const { saveOrUpdate } = usePSInfo()
useEffect(() => {