Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
b3b20a999a Fix clearQueryParams to check window at runtime instead of using isServer constant
Replace the pre-evaluated isServer check with a direct typeof window === 'undefined' check to properly handle test scenarios where window is stubbed after module load.

Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
2026-01-10 06:05:36 +00:00
copilot-swe-agent[bot]
cdf926cf82 Initial plan 2026-01-10 05:57:18 +00:00
yyh
e8c736afbf refactor(web): migrate embedded-chatbot header to use isClient utility
Missed migration from the initial refactoring - removes local isClient
variable definition and imports from the centralized utility.
2026-01-10 12:54:17 +08:00
yyh
6285a59508 refactor(web): extract isServer/isClient utility for consistent environment detection
Centralize server/client environment detection by introducing a dedicated utility
file instead of repeating `typeof window === 'undefined'` checks across the codebase.

This improves code maintainability and consistency across 8 files with 15 occurrences.

Closes #30802
2026-01-10 12:51:39 +08:00
9 changed files with 48 additions and 14 deletions

View File

@@ -29,6 +29,7 @@ import { CheckModal } from '@/hooks/use-pay'
import { useInfiniteAppList } from '@/service/use-apps' import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app' import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import { isServer } from '@/utils/client'
import AppCard from './app-card' import AppCard from './app-card'
import { AppCardSkeleton } from './app-card-skeleton' import { AppCardSkeleton } from './app-card-skeleton'
import Empty from './empty' import Empty from './empty'
@@ -71,7 +72,7 @@ const List = () => {
// 1) Normalize legacy/incorrect query params like ?mode=discover -> ?category=all // 1) Normalize legacy/incorrect query params like ?mode=discover -> ?category=all
useEffect(() => { useEffect(() => {
// avoid running on server // avoid running on server
if (typeof window === 'undefined') if (isServer)
return return
const mode = searchParams.get('mode') const mode = searchParams.get('mode')
if (!mode) if (!mode)

View File

@@ -11,6 +11,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import { isClient } from '@/utils/client'
import { import {
useEmbeddedChatbotContext, useEmbeddedChatbotContext,
} from '../context' } from '../context'
@@ -40,7 +41,6 @@ const Header: FC<IHeaderProps> = ({
allInputsHidden, allInputsHidden,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const isClient = typeof window !== 'undefined'
const isIframe = isClient ? window.self !== window.top : false const isIframe = isClient ? window.self !== window.top : false
const [parentOrigin, setParentOrigin] = useState('') const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false) const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)

View File

@@ -13,6 +13,7 @@ import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
import { useGetLanguage } from '@/context/i18n' import { useGetLanguage } from '@/context/i18n'
import { isServer } from '@/utils/client'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var' import { getMarketplaceUrl } from '@/utils/var'
import BlockIcon from '../block-icon' import BlockIcon from '../block-icon'
@@ -49,14 +50,14 @@ const FeaturedTools = ({
const language = useGetLanguage() const language = useGetLanguage()
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => { const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (typeof window === 'undefined') if (isServer)
return false return false
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
return stored === 'true' return stored === 'true'
}) })
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored !== null) if (stored !== null)
@@ -64,7 +65,7 @@ const FeaturedTools = ({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
}, [isCollapsed]) }, [isCollapsed])

View File

@@ -12,6 +12,7 @@ import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
import { useGetLanguage } from '@/context/i18n' import { useGetLanguage } from '@/context/i18n'
import { isServer } from '@/utils/client'
import { formatNumber } from '@/utils/format' import { formatNumber } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var' import { getMarketplaceUrl } from '@/utils/var'
import BlockIcon from '../block-icon' import BlockIcon from '../block-icon'
@@ -42,14 +43,14 @@ const FeaturedTriggers = ({
const language = useGetLanguage() const language = useGetLanguage()
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => { const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (typeof window === 'undefined') if (isServer)
return false return false
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
return stored === 'true' return stored === 'true'
}) })
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored !== null) if (stored !== null)
@@ -57,7 +58,7 @@ const FeaturedTriggers = ({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
}, [isCollapsed]) }, [isCollapsed])

View File

@@ -11,6 +11,7 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
import { useRAGRecommendedPlugins } from '@/service/use-tools' import { useRAGRecommendedPlugins } from '@/service/use-tools'
import { isServer } from '@/utils/client'
import { getMarketplaceUrl } from '@/utils/var' import { getMarketplaceUrl } from '@/utils/var'
import List from './list' import List from './list'
@@ -29,14 +30,14 @@ const RAGToolRecommendations = ({
}: RAGToolRecommendationsProps) => { }: RAGToolRecommendationsProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => { const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (typeof window === 'undefined') if (isServer)
return false return false
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
return stored === 'true' return stored === 'true'
}) })
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
const stored = window.localStorage.getItem(STORAGE_KEY) const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored !== null) if (stored !== null)
@@ -44,7 +45,7 @@ const RAGToolRecommendations = ({
}, []) }, [])
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') if (isServer)
return return
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
}, [isCollapsed]) }, [isCollapsed])

View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { NUM_INFINITE } from '@/app/components/billing/config' import { NUM_INFINITE } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { IS_CLOUD_EDITION } from '@/config' import { IS_CLOUD_EDITION } from '@/config'
import { isServer } from '@/utils/client'
export type TriggerEventsLimitModalPayload = { export type TriggerEventsLimitModalPayload = {
usage: number usage: number
@@ -46,7 +47,7 @@ export const useTriggerEventsLimitModal = ({
useEffect(() => { useEffect(() => {
if (!IS_CLOUD_EDITION) if (!IS_CLOUD_EDITION)
return return
if (typeof window === 'undefined') if (isServer)
return return
if (!currentWorkspaceId) if (!currentWorkspaceId)
return return

View File

@@ -5,12 +5,13 @@ import type { FC, PropsWithChildren } from 'react'
import { QueryClientProvider } from '@tanstack/react-query' import { QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react' import { useState } from 'react'
import { TanStackDevtoolsLoader } from '@/app/components/devtools/tanstack/loader' import { TanStackDevtoolsLoader } from '@/app/components/devtools/tanstack/loader'
import { isServer } from '@/utils/client'
import { makeQueryClient } from './query-client-server' import { makeQueryClient } from './query-client-server'
let browserQueryClient: QueryClient | undefined let browserQueryClient: QueryClient | undefined
function getQueryClient() { function getQueryClient() {
if (typeof window === 'undefined') { if (isServer) {
return makeQueryClient() return makeQueryClient()
} }
if (!browserQueryClient) if (!browserQueryClient)

26
web/utils/client.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* Server/Client environment detection utilities
*
* Use these constants and functions to safely detect the runtime environment
* in Next.js applications where code may execute on both server and client.
*/
/**
* Check if code is running on server-side (SSR)
*
* @example
* if (isServer) {
* // Server-only logic
* }
*/
export const isServer = typeof window === 'undefined'
/**
* Check if code is running on client-side (browser)
*
* @example
* if (isClient) {
* localStorage.setItem('key', 'value')
* }
*/
export const isClient = typeof window !== 'undefined'

View File

@@ -1,3 +1,5 @@
import { isServer } from '@/utils/client'
/** /**
* Send Google Analytics event * Send Google Analytics event
* @param eventName - event name * @param eventName - event name
@@ -7,7 +9,7 @@ export const sendGAEvent = (
eventName: string, eventName: string,
eventParams?: GtagEventParams, eventParams?: GtagEventParams,
): void => { ): void => {
if (typeof window === 'undefined' || typeof (window as any).gtag !== 'function') { if (isServer || typeof (window as any).gtag !== 'function') {
return return
} }
(window as any).gtag('event', eventName, eventParams) (window as any).gtag('event', eventName, eventParams)