diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index d282d61d4e..90eb061313 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -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() - const Logs: FC = ({ 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(cachedState?.queryParams ?? defaultQueryParams) - const [currPage, setCurrPage] = React.useState(() => cachedState?.currPage ?? getPageFromParams()) - const [limit, setLimit] = React.useState(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 = ({ 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 = ({ 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 (

{t('description', { ns: 'appLog' })}

- + {total === undefined ? : total > 0 @@ -130,11 +117,11 @@ const Logs: FC = ({ appDetail }) => { {(total && total > APP_PAGE_LIMIT) ? ( ) : null} diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 7a7cf4ffd3..d5cb6de29e 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -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>>(CONVERSATION_ID_INFO, { defaultValue: {}, diff --git a/web/app/components/billing/partner-stack/use-ps-info.ts b/web/app/components/billing/partner-stack/use-ps-info.ts index 51d693f358..e089fa7fff 100644 --- a/web/app/components/billing/partner-stack/use-ps-info.ts +++ b/web/app/components/billing/partner-stack/use-ps-info.ts @@ -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, diff --git a/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts index 505f15efc0..dd5418927d 100644 --- a/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts +++ b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts @@ -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) => { + 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) => { - 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 diff --git a/web/app/signin/page.tsx b/web/app/signin/page.tsx index 6f3632393c..34147d449f 100644 --- a/web/app/signin/page.tsx +++ b/web/app/signin/page.tsx @@ -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(() => {