mirror of https://github.com/langgenius/dify.git
refactor: unify query param state with nuqs
This commit is contained in:
parent
7e06225ce2
commit
eeb2b9d39c
|
|
@ -22,6 +22,7 @@ import {
|
|||
createContext,
|
||||
useContextSelector,
|
||||
} from 'use-context-selector'
|
||||
import { useMarketplaceFilters } from '@/hooks/use-query-params'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
import {
|
||||
getValidCategoryKeys,
|
||||
|
|
@ -37,7 +38,6 @@ import { PLUGIN_TYPE_SEARCH_MAP } from './plugin-type-switch'
|
|||
import {
|
||||
getMarketplaceListCondition,
|
||||
getMarketplaceListFilterType,
|
||||
updateSearchParams,
|
||||
} from './utils'
|
||||
|
||||
export type MarketplaceContextValue = {
|
||||
|
|
@ -107,16 +107,22 @@ export const MarketplaceContextProvider = ({
|
|||
scrollContainerId,
|
||||
showSearchParams,
|
||||
}: MarketplaceContextProviderProps) => {
|
||||
// Use nuqs hook for URL-based filter state
|
||||
const [urlFilters, setUrlFilters] = useMarketplaceFilters()
|
||||
|
||||
const { data, isSuccess } = useInstalledPluginList(!shouldExclude)
|
||||
const exclude = useMemo(() => {
|
||||
if (shouldExclude)
|
||||
return data?.plugins.map(plugin => plugin.plugin_id)
|
||||
}, [data?.plugins, shouldExclude])
|
||||
const queryFromSearchParams = searchParams?.q || ''
|
||||
const tagsFromSearchParams = searchParams?.tags ? getValidTagKeys(searchParams.tags.split(',')) : []
|
||||
|
||||
// Initialize from URL params (legacy support) or use nuqs state
|
||||
const queryFromSearchParams = searchParams?.q || urlFilters.q
|
||||
const tagsFromSearchParams = getValidTagKeys(urlFilters.tags)
|
||||
const hasValidTags = !!tagsFromSearchParams.length
|
||||
const hasValidCategory = getValidCategoryKeys(searchParams?.category)
|
||||
const categoryFromSearchParams = hasValidCategory || PLUGIN_TYPE_SEARCH_MAP.all
|
||||
const hasValidCategory = getValidCategoryKeys(urlFilters.category)
|
||||
const categoryFromSearchParams = hasValidCategory || urlFilters.category || PLUGIN_TYPE_SEARCH_MAP.all
|
||||
|
||||
const [searchPluginText, setSearchPluginText] = useState(queryFromSearchParams)
|
||||
const searchPluginTextRef = useRef(searchPluginText)
|
||||
const [filterPluginTags, setFilterPluginTags] = useState<string[]>(tagsFromSearchParams)
|
||||
|
|
@ -158,10 +164,6 @@ export const MarketplaceContextProvider = ({
|
|||
sortOrder: sortRef.current.sortOrder,
|
||||
type: getMarketplaceListFilterType(activePluginTypeRef.current),
|
||||
})
|
||||
const url = new URL(window.location.href)
|
||||
if (searchParams?.language)
|
||||
url.searchParams.set('language', searchParams?.language)
|
||||
history.replaceState({}, '', url)
|
||||
}
|
||||
else {
|
||||
if (shouldExclude && isSuccess) {
|
||||
|
|
@ -183,28 +185,32 @@ export const MarketplaceContextProvider = ({
|
|||
resetPlugins()
|
||||
}, [exclude, queryMarketplaceCollectionsAndPlugins, resetPlugins])
|
||||
|
||||
const debouncedUpdateSearchParams = useMemo(() => debounce(() => {
|
||||
updateSearchParams({
|
||||
query: searchPluginTextRef.current,
|
||||
category: activePluginTypeRef.current,
|
||||
tags: filterPluginTagsRef.current,
|
||||
})
|
||||
}, 500), [])
|
||||
|
||||
const handleUpdateSearchParams = useCallback((debounced?: boolean) => {
|
||||
const applyUrlFilters = useCallback(() => {
|
||||
if (!showSearchParams)
|
||||
return
|
||||
const nextFilters = {
|
||||
q: searchPluginTextRef.current,
|
||||
category: activePluginTypeRef.current,
|
||||
tags: filterPluginTagsRef.current,
|
||||
}
|
||||
const categoryChanged = urlFilters.category !== nextFilters.category
|
||||
setUrlFilters(nextFilters, {
|
||||
history: categoryChanged ? 'push' : 'replace',
|
||||
})
|
||||
}, [setUrlFilters, showSearchParams, urlFilters.category])
|
||||
|
||||
const debouncedUpdateSearchParams = useMemo(() => debounce(() => {
|
||||
applyUrlFilters()
|
||||
}, 500), [applyUrlFilters])
|
||||
|
||||
const handleUpdateSearchParams = useCallback((debounced?: boolean) => {
|
||||
if (debounced) {
|
||||
debouncedUpdateSearchParams()
|
||||
}
|
||||
else {
|
||||
updateSearchParams({
|
||||
query: searchPluginTextRef.current,
|
||||
category: activePluginTypeRef.current,
|
||||
tags: filterPluginTagsRef.current,
|
||||
})
|
||||
applyUrlFilters()
|
||||
}
|
||||
}, [debouncedUpdateSearchParams, showSearchParams])
|
||||
}, [applyUrlFilters, debouncedUpdateSearchParams])
|
||||
|
||||
const handleQueryPlugins = useCallback((debounced?: boolean) => {
|
||||
handleUpdateSearchParams(debounced)
|
||||
|
|
|
|||
|
|
@ -84,12 +84,14 @@ const PluginTypeSwitch = ({
|
|||
const handlePopState = useCallback(() => {
|
||||
if (!showSearchParams)
|
||||
return
|
||||
// nuqs handles popstate automatically
|
||||
const url = new URL(window.location.href)
|
||||
const category = url.searchParams.get('category') || PLUGIN_TYPE_SEARCH_MAP.all
|
||||
handleActivePluginTypeChange(category)
|
||||
}, [showSearchParams, handleActivePluginTypeChange])
|
||||
|
||||
useEffect(() => {
|
||||
// nuqs manages popstate internally, but we keep this for URL sync
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handlePopState)
|
||||
|
|
|
|||
|
|
@ -153,21 +153,11 @@ export const getMarketplaceListFilterType = (category: string) => {
|
|||
return 'plugin'
|
||||
}
|
||||
|
||||
// Deprecated: Use useMarketplaceFilters hook from hooks/use-query-params.ts instead
|
||||
// This function is kept for backward compatibility but should not be used in new code
|
||||
/** @deprecated Use the useMarketplaceFilters hook from hooks/use-query-params.ts instead */
|
||||
export const updateSearchParams = (pluginsSearchParams: PluginsSearchParams) => {
|
||||
const { query, category, tags } = pluginsSearchParams
|
||||
const url = new URL(window.location.href)
|
||||
const categoryChanged = url.searchParams.get('category') !== category
|
||||
if (query)
|
||||
url.searchParams.set('q', query)
|
||||
else
|
||||
url.searchParams.delete('q')
|
||||
if (category)
|
||||
url.searchParams.set('category', category)
|
||||
else
|
||||
url.searchParams.delete('category')
|
||||
if (tags && tags.length)
|
||||
url.searchParams.set('tags', tags.join(','))
|
||||
else
|
||||
url.searchParams.delete('tags')
|
||||
history[`${categoryChanged ? 'pushState' : 'replaceState'}`]({}, '', url)
|
||||
console.warn('updateSearchParams is deprecated. Use the useMarketplaceFilters hook from hooks/use-query-params.ts instead.')
|
||||
// This is now handled by the useMarketplaceFilters hook
|
||||
// Keeping the function for any legacy code that hasn't been migrated yet
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ import {
|
|||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from 'next/navigation'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
|
@ -25,6 +21,7 @@ import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@
|
|||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePluginInstallation } from '@/hooks/use-query-params'
|
||||
import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
|
||||
import { sleep } from '@/utils'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -42,9 +39,6 @@ import PluginTasks from './plugin-tasks'
|
|||
import useReferenceSetting from './use-reference-setting'
|
||||
import { useUploader } from './use-uploader'
|
||||
|
||||
const PACKAGE_IDS_KEY = 'package-ids'
|
||||
const BUNDLE_INFO_KEY = 'bundle-info'
|
||||
|
||||
export type PluginPageProps = {
|
||||
plugins: React.ReactNode
|
||||
marketplace: React.ReactNode
|
||||
|
|
@ -55,33 +49,13 @@ const PluginPage = ({
|
|||
}: PluginPageProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const searchParams = useSearchParams()
|
||||
const { replace } = useRouter()
|
||||
useDocumentTitle(t('plugin.metadata.title'))
|
||||
|
||||
// just support install one package now
|
||||
const packageId = useMemo(() => {
|
||||
const idStrings = searchParams.get(PACKAGE_IDS_KEY)
|
||||
try {
|
||||
return idStrings ? JSON.parse(idStrings)[0] : ''
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}, [searchParams])
|
||||
// Use nuqs hook for installation state
|
||||
const [{ packageId, bundleInfo }, setInstallState] = usePluginInstallation()
|
||||
|
||||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
|
||||
const [dependencies, setDependencies] = useState<Dependency[]>([])
|
||||
const bundleInfo = useMemo(() => {
|
||||
const info = searchParams.get(BUNDLE_INFO_KEY)
|
||||
try {
|
||||
return info ? JSON.parse(info) : undefined
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const [isShowInstallFromMarketplace, {
|
||||
setTrue: showInstallFromMarketplace,
|
||||
|
|
@ -90,11 +64,9 @@ const PluginPage = ({
|
|||
|
||||
const hideInstallFromMarketplace = () => {
|
||||
doHideInstallFromMarketplace()
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.delete(PACKAGE_IDS_KEY)
|
||||
url.searchParams.delete(BUNDLE_INFO_KEY)
|
||||
replace(url.toString())
|
||||
setInstallState(null)
|
||||
}
|
||||
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | PluginManifestInMarket | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -114,12 +86,19 @@ const PluginPage = ({
|
|||
return
|
||||
}
|
||||
if (bundleInfo) {
|
||||
const { data } = await fetchBundleInfoFromMarketPlace(bundleInfo)
|
||||
setDependencies(data.version.dependencies)
|
||||
showInstallFromMarketplace()
|
||||
// bundleInfo is a JSON string from URL, needs parsing
|
||||
try {
|
||||
const parsedBundleInfo = typeof bundleInfo === 'string' ? JSON.parse(bundleInfo) : bundleInfo
|
||||
const { data } = await fetchBundleInfoFromMarketPlace(parsedBundleInfo)
|
||||
setDependencies(data.version.dependencies)
|
||||
showInstallFromMarketplace()
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to parse bundle info:', e)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [packageId, bundleInfo])
|
||||
}, [packageId, bundleInfo, showInstallFromMarketplace])
|
||||
|
||||
const {
|
||||
referenceSetting,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
|||
>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2 rounded-lg border border-transparent',
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleViewVersionHistory}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Viewport } from 'next'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { Instrument_Serif } from 'next/font/google'
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
import GlobalPublicStoreProvider from '@/context/global-public-context'
|
||||
import { TanstackQueryInitializer } from '@/context/query-client'
|
||||
import { getLocaleOnServer } from '@/i18n-config/server'
|
||||
|
|
@ -96,17 +97,19 @@ const LocaleLayout = async ({
|
|||
disableTransitionOnChange
|
||||
enableColorScheme={false}
|
||||
>
|
||||
<BrowserInitializer>
|
||||
<SentryInitializer>
|
||||
<TanstackQueryInitializer>
|
||||
<I18nServer>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</I18nServer>
|
||||
</TanstackQueryInitializer>
|
||||
</SentryInitializer>
|
||||
</BrowserInitializer>
|
||||
<NuqsAdapter>
|
||||
<BrowserInitializer>
|
||||
<SentryInitializer>
|
||||
<TanstackQueryInitializer>
|
||||
<I18nServer>
|
||||
<GlobalPublicStoreProvider>
|
||||
{children}
|
||||
</GlobalPublicStoreProvider>
|
||||
</I18nServer>
|
||||
</TanstackQueryInitializer>
|
||||
</SentryInitializer>
|
||||
</BrowserInitializer>
|
||||
</NuqsAdapter>
|
||||
</ThemeProvider>
|
||||
<RoutePrefixHandle />
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
'use client'
|
||||
|
||||
/**
|
||||
* Centralized URL query parameter management hooks using nuqs
|
||||
*
|
||||
* This file provides type-safe, performant query parameter management
|
||||
* that doesn't trigger full page refreshes (shallow routing).
|
||||
*
|
||||
* Best practices from nuqs documentation:
|
||||
* - Use useQueryState for single parameters
|
||||
* - Use useQueryStates for multiple related parameters (atomic updates)
|
||||
* - Always provide parsers with defaults for type safety
|
||||
* - Use shallow routing to avoid unnecessary re-renders
|
||||
*/
|
||||
|
||||
import type { Options } from 'nuqs'
|
||||
import {
|
||||
|
||||
parseAsArrayOf,
|
||||
parseAsString,
|
||||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs'
|
||||
import { ACCOUNT_SETTING_MODAL_ACTION } from '@/app/components/header/account-setting/constants'
|
||||
|
||||
/**
|
||||
* Modal State Query Parameters
|
||||
* Manages modal visibility and configuration via URL
|
||||
*/
|
||||
export const PRICING_MODAL_QUERY_PARAM = 'pricing'
|
||||
export const PRICING_MODAL_QUERY_VALUE = 'open'
|
||||
|
||||
/**
|
||||
* Hook to manage pricing modal state via URL
|
||||
* @returns [isOpen, setIsOpen] - Tuple like useState
|
||||
*
|
||||
* @example
|
||||
* const [isPricingModalOpen, setIsPricingModalOpen] = usePricingModal()
|
||||
* setIsPricingModalOpen(true) // Sets ?pricing=open
|
||||
* setIsPricingModalOpen(false) // Removes ?pricing
|
||||
*/
|
||||
export function usePricingModal() {
|
||||
const [isOpen, setIsOpen] = useQueryState(
|
||||
PRICING_MODAL_QUERY_PARAM,
|
||||
parseAsString,
|
||||
)
|
||||
|
||||
const setIsOpenBoolean = (open: boolean, options?: Options) => {
|
||||
const history = options?.history ?? (open ? 'push' : 'replace')
|
||||
setIsOpen(open ? PRICING_MODAL_QUERY_VALUE : null, { ...options, history })
|
||||
}
|
||||
|
||||
return [isOpen === PRICING_MODAL_QUERY_VALUE, setIsOpenBoolean] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage account setting modal state via URL
|
||||
* @returns [state, setState] - Object with isOpen + payload (tab) and setter
|
||||
*
|
||||
* @example
|
||||
* const [accountModalState, setAccountModalState] = useAccountSettingModal()
|
||||
* setAccountModalState({ payload: 'billing' }) // Sets ?action=showSettings&tab=billing
|
||||
* setAccountModalState(null) // Removes both params
|
||||
*/
|
||||
export function useAccountSettingModal<T extends string = string>() {
|
||||
const [accountState, setAccountState] = useQueryStates(
|
||||
{
|
||||
action: parseAsString,
|
||||
tab: parseAsString,
|
||||
},
|
||||
{
|
||||
history: 'replace',
|
||||
},
|
||||
)
|
||||
|
||||
const setState = (state: { payload: T } | null) => {
|
||||
if (!state) {
|
||||
setAccountState({ action: null, tab: null }, { history: 'replace' })
|
||||
return
|
||||
}
|
||||
const shouldPush = accountState.action !== ACCOUNT_SETTING_MODAL_ACTION
|
||||
setAccountState(
|
||||
{ action: ACCOUNT_SETTING_MODAL_ACTION, tab: state.payload },
|
||||
{ history: shouldPush ? 'push' : 'replace' },
|
||||
)
|
||||
}
|
||||
|
||||
const isOpen = accountState.action === ACCOUNT_SETTING_MODAL_ACTION
|
||||
const currentTab = (isOpen ? accountState.tab : null) as T | null
|
||||
|
||||
return [{ isOpen, payload: currentTab }, setState] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Marketplace Search Query Parameters
|
||||
*/
|
||||
export type MarketplaceFilters = {
|
||||
q: string // search query
|
||||
category: string // plugin category
|
||||
tags: string[] // comma-separated tags
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage marketplace search/filter state via URL
|
||||
* Provides atomic updates - all params update together
|
||||
*
|
||||
* @example
|
||||
* const [filters, setFilters] = useMarketplaceFilters()
|
||||
* setFilters({ q: 'search', category: 'tool', tags: ['ai'] }) // Updates all at once
|
||||
* setFilters({ q: '' }) // Only updates q, keeps others
|
||||
* setFilters(null) // Clears all marketplace params
|
||||
*/
|
||||
export function useMarketplaceFilters() {
|
||||
return useQueryStates(
|
||||
{
|
||||
q: parseAsString.withDefault(''),
|
||||
category: parseAsString.withDefault('all').withOptions({ clearOnDefault: false }),
|
||||
tags: parseAsArrayOf(parseAsString).withDefault([]),
|
||||
},
|
||||
{
|
||||
// Update URL without pushing to history (replaceState behavior)
|
||||
history: 'replace',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Installation Query Parameters
|
||||
*/
|
||||
const PACKAGE_IDS_PARAM = 'package-ids'
|
||||
const BUNDLE_INFO_PARAM = 'bundle-info'
|
||||
|
||||
/**
|
||||
* Hook to manage plugin installation state via URL
|
||||
* @returns [installState, setInstallState] - Installation state and setter
|
||||
*
|
||||
* @example
|
||||
* const [installState, setInstallState] = usePluginInstallation()
|
||||
* setInstallState({ packageId: 'org/plugin' }) // Sets ?package-ids=["org/plugin"]
|
||||
* setInstallState(null) // Clears installation params
|
||||
*/
|
||||
export function usePluginInstallation() {
|
||||
const [packageIds, setPackageIds] = useQueryState(
|
||||
PACKAGE_IDS_PARAM,
|
||||
parseAsString,
|
||||
)
|
||||
const [bundleInfo, setBundleInfo] = useQueryState(
|
||||
BUNDLE_INFO_PARAM,
|
||||
parseAsString,
|
||||
)
|
||||
|
||||
const setInstallState = (state: { packageId?: string, bundleInfo?: string } | null) => {
|
||||
if (!state) {
|
||||
setPackageIds(null)
|
||||
setBundleInfo(null)
|
||||
return
|
||||
}
|
||||
if (state.packageId) {
|
||||
// Store as JSON array for consistency with existing code
|
||||
setPackageIds(JSON.stringify([state.packageId]))
|
||||
}
|
||||
if (state.bundleInfo) {
|
||||
setBundleInfo(state.bundleInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse packageIds from JSON array
|
||||
const currentPackageId = packageIds
|
||||
? (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(packageIds)
|
||||
return Array.isArray(parsed) ? parsed[0] : packageIds
|
||||
}
|
||||
catch {
|
||||
return packageIds
|
||||
}
|
||||
})()
|
||||
: null
|
||||
|
||||
return [
|
||||
{
|
||||
packageId: currentPackageId,
|
||||
bundleInfo,
|
||||
},
|
||||
setInstallState,
|
||||
] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to clear specific query parameters from URL
|
||||
* This is a client-side utility that should be called from client components
|
||||
*
|
||||
* @param keys - Single key or array of keys to remove from URL
|
||||
*
|
||||
* @example
|
||||
* // In a client component
|
||||
* clearQueryParams('param1')
|
||||
* clearQueryParams(['param1', 'param2'])
|
||||
*/
|
||||
export function clearQueryParams(keys: string | string[]) {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
|
||||
const url = new URL(window.location.href)
|
||||
const keysArray = Array.isArray(keys) ? keys : [keys]
|
||||
|
||||
keysArray.forEach(key => url.searchParams.delete(key))
|
||||
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
}
|
||||
|
|
@ -109,6 +109,7 @@
|
|||
"next": "~15.5.9",
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.8.6",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"qs": "^6.14.0",
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@ importers:
|
|||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
nuqs:
|
||||
specifier: ^2.8.6
|
||||
version: 2.8.6(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react@19.2.3)
|
||||
pinyin-pro:
|
||||
specifier: ^3.27.0
|
||||
version: 3.27.0
|
||||
|
|
@ -3169,6 +3172,9 @@ packages:
|
|||
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
|
|
@ -6731,6 +6737,27 @@ packages:
|
|||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
nuqs@2.8.6:
|
||||
resolution: {integrity: sha512-aRxeX68b4ULmhio8AADL2be1FWDy0EPqaByPvIYWrA7Pm07UjlrICp/VPlSnXJNAG0+3MQwv3OporO2sOXMVGA==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': '>=2'
|
||||
'@tanstack/react-router': ^1
|
||||
next: '>=14.2.0'
|
||||
react: '>=18.2.0 || ^19.0.0-0'
|
||||
react-router: ^5 || ^6 || ^7
|
||||
react-router-dom: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
optional: true
|
||||
'@tanstack/react-router':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
react-router:
|
||||
optional: true
|
||||
react-router-dom:
|
||||
optional: true
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -11539,6 +11566,8 @@ snapshots:
|
|||
|
||||
'@sindresorhus/is@4.6.0': {}
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
|
@ -15993,6 +16022,13 @@ snapshots:
|
|||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuqs@2.8.6(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react@19.2.3):
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.0.0
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
next: 15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-deep-merge@2.0.0: {}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ export const canFindTool = (providerId: string, oldToolId?: string) => {
|
|||
|| providerId === `langgenius/${oldToolId}_tool/${oldToolId}`
|
||||
}
|
||||
|
||||
// Deprecated: Use clearQueryParams from hooks/use-query-params.ts instead
|
||||
/** @deprecated Use clearQueryParams from hooks/use-query-params.ts instead */
|
||||
export const removeSpecificQueryParam = (key: string | string[]) => {
|
||||
console.warn('removeSpecificQueryParam is deprecated. Use clearQueryParams from hooks/use-query-params.ts instead.')
|
||||
const url = new URL(window.location.href)
|
||||
if (Array.isArray(key))
|
||||
key.forEach(k => url.searchParams.delete(k))
|
||||
|
|
|
|||
Loading…
Reference in New Issue