refactor(query-state): migrate query param state management to nuqs (#30184)

Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
yyh
2025-12-29 11:24:54 +08:00
committed by GitHub
parent 446df6b50d
commit 3ae7788933
28 changed files with 1336 additions and 1328 deletions

View File

@@ -1,4 +1,5 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import * as React from 'react'
import { defaultPlan } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
@@ -72,9 +73,11 @@ const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
})
const renderProvider = () => render(
<ModalContextProvider>
<div data-testid="modal-context-test-child" />
</ModalContextProvider>,
<NuqsTestingAdapter>
<ModalContextProvider>
<div data-testid="modal-context-test-child" />
</ModalContextProvider>
</NuqsTestingAdapter>,
)
describe('ModalContextProvider trigger events limit modal', () => {

View File

@@ -24,21 +24,22 @@ import type {
import type { ModerationConfig, PromptVariable } from '@/models/debug'
import { noop } from 'es-toolkit/compat'
import dynamic from 'next/dynamic'
import { useSearchParams } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import {
ACCOUNT_SETTING_MODAL_ACTION,
DEFAULT_ACCOUNT_SETTING_TAB,
isValidAccountSettingTab,
} from '@/app/components/header/account-setting/constants'
import {
EDUCATION_PRICING_SHOW_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { removeSpecificQueryParam } from '@/utils'
import {
useAccountSettingModal,
usePricingModal,
} from '@/hooks/use-query-params'
import {
useTriggerEventsLimitModal,
@@ -125,8 +126,6 @@ export type ModalContextState = {
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
}
const PRICING_MODAL_QUERY_PARAM = 'pricing'
const PRICING_MODAL_QUERY_VALUE = 'open'
const ModalContext = createContext<ModalContextState>({
setShowAccountSettingModal: noop,
@@ -157,16 +156,16 @@ type ModalContextProviderProps = {
export const ModalContextProvider = ({
children,
}: ModalContextProviderProps) => {
const searchParams = useSearchParams()
// Use nuqs hooks for URL-based modal state management
const [showPricingModal, setPricingModalOpen] = usePricingModal()
const [urlAccountModalState, setUrlAccountModalState] = useAccountSettingModal<AccountSettingTab>()
const [showAccountSettingModal, setShowAccountSettingModal] = useState<ModalState<AccountSettingTab> | null>(() => {
if (searchParams.get('action') === ACCOUNT_SETTING_MODAL_ACTION) {
const tabParam = searchParams.get('tab')
const tab = isValidAccountSettingTab(tabParam) ? tabParam : DEFAULT_ACCOUNT_SETTING_TAB
return { payload: tab }
}
return null
})
const accountSettingCallbacksRef = useRef<Omit<ModalState<AccountSettingTab>, 'payload'> | null>(null)
const accountSettingTab = urlAccountModalState.isOpen
? (isValidAccountSettingTab(urlAccountModalState.payload)
? urlAccountModalState.payload
: DEFAULT_ACCOUNT_SETTING_TAB)
: null
const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
@@ -182,9 +181,6 @@ export const ModalContextProvider = ({
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const [showPricingModal, setShowPricingModal] = useState(
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
)
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
const handleCancelAccountSettingModal = () => {
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
@@ -192,54 +188,34 @@ export const ModalContextProvider = ({
if (educationVerifying === 'yes')
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
removeSpecificQueryParam('action')
removeSpecificQueryParam('tab')
setShowAccountSettingModal(null)
if (showAccountSettingModal?.onCancelCallback)
showAccountSettingModal?.onCancelCallback()
accountSettingCallbacksRef.current?.onCancelCallback?.()
accountSettingCallbacksRef.current = null
setUrlAccountModalState(null)
}
const handleAccountSettingTabChange = useCallback((tab: AccountSettingTab) => {
setShowAccountSettingModal((prev) => {
if (!prev)
return { payload: tab }
if (prev.payload === tab)
return prev
return { ...prev, payload: tab }
})
}, [setShowAccountSettingModal])
setUrlAccountModalState({ payload: tab })
}, [setUrlAccountModalState])
const setShowAccountSettingModal = useCallback((next: SetStateAction<ModalState<AccountSettingTab> | null>) => {
const currentState = accountSettingTab
? { payload: accountSettingTab, ...(accountSettingCallbacksRef.current ?? {}) }
: null
const resolvedState = typeof next === 'function' ? next(currentState) : next
if (!resolvedState) {
accountSettingCallbacksRef.current = null
setUrlAccountModalState(null)
return
}
const { payload, ...callbacks } = resolvedState
accountSettingCallbacksRef.current = callbacks
setUrlAccountModalState({ payload })
}, [accountSettingTab, setUrlAccountModalState])
useEffect(() => {
if (typeof window === 'undefined')
return
const url = new URL(window.location.href)
if (!showAccountSettingModal?.payload) {
if (url.searchParams.get('action') !== ACCOUNT_SETTING_MODAL_ACTION)
return
url.searchParams.delete('action')
url.searchParams.delete('tab')
window.history.replaceState(null, '', url.toString())
return
}
url.searchParams.set('action', ACCOUNT_SETTING_MODAL_ACTION)
url.searchParams.set('tab', showAccountSettingModal.payload)
window.history.replaceState(null, '', url.toString())
}, [showAccountSettingModal])
useEffect(() => {
if (typeof window === 'undefined')
return
const url = new URL(window.location.href)
if (showPricingModal) {
url.searchParams.set(PRICING_MODAL_QUERY_PARAM, PRICING_MODAL_QUERY_VALUE)
}
else {
url.searchParams.delete(PRICING_MODAL_QUERY_PARAM)
if (url.searchParams.get('action') === EDUCATION_PRICING_SHOW_ACTION)
url.searchParams.delete('action')
}
window.history.replaceState(null, '', url.toString())
}, [showPricingModal])
if (!urlAccountModalState.isOpen)
accountSettingCallbacksRef.current = null
}, [urlAccountModalState.isOpen])
const { plan, isFetchedPlan } = useProviderContext()
const {
@@ -337,12 +313,12 @@ export const ModalContextProvider = ({
}
const handleShowPricingModal = useCallback(() => {
setShowPricingModal(true)
}, [])
setPricingModalOpen(true)
}, [setPricingModalOpen])
const handleCancelPricingModal = useCallback(() => {
setShowPricingModal(false)
}, [])
setPricingModalOpen(false)
}, [setPricingModalOpen])
return (
<ModalContext.Provider value={{
@@ -364,9 +340,9 @@ export const ModalContextProvider = ({
<>
{children}
{
!!showAccountSettingModal && (
accountSettingTab && (
<AccountSetting
activeTab={showAccountSettingModal.payload}
activeTab={accountSettingTab}
onCancel={handleCancelAccountSettingModal}
onTabChange={handleAccountSettingTabChange}
/>