mirror of https://github.com/langgenius/dify.git
fix: prevent popup blocker from blocking async window.open
Add useAsyncWindowOpen hook to handle async URL fetching with placeholder window pattern. This prevents browser popup blockers (especially Safari) from blocking windows opened after async operations. - Create reusable useAsyncWindowOpen hook with placeholder window pattern - Fix billing subscription management popup (cloud-plan-item) - Fix app card explore popup - Fix app publisher explore popup Fixes #29389 Ref: #29390
This commit is contained in:
parent
51330c0ee6
commit
e95b7b57c9
|
|
@ -21,7 +21,6 @@ import {
|
|||
import { useKeyPress } from 'ahooks'
|
||||
import Divider from '../../base/divider'
|
||||
import Loading from '../../base/loading'
|
||||
import Toast from '../../base/toast'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
|
|
@ -50,6 +49,7 @@ import { AppModeEnum } from '@/types/app'
|
|||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
|
|
@ -216,18 +216,23 @@ const AppPublisher = ({
|
|||
setPublished(false)
|
||||
}, [disabled, onToggle, open])
|
||||
|
||||
const handleOpenInExplore = useCallback(async () => {
|
||||
try {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
else
|
||||
const { openAsync } = useAsyncWindowOpen()
|
||||
|
||||
const handleOpenInExplore = useCallback(() => {
|
||||
if (!appDetail?.id) return
|
||||
|
||||
openAsync(
|
||||
async () => {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(appDetail.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||
}
|
||||
}, [appDetail?.id])
|
||||
},
|
||||
{
|
||||
errorMessage: 'Failed to open app in Explore',
|
||||
},
|
||||
)
|
||||
}, [appDetail?.id, openAsync])
|
||||
|
||||
const handleAccessControlUpdate = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
|
@ -31,6 +31,7 @@ import { AccessMode } from '@/models/access-control'
|
|||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { formatTime } from '@/utils/time'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), {
|
||||
|
|
@ -242,20 +243,24 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||
e.preventDefault()
|
||||
setShowAccessControl(true)
|
||||
}
|
||||
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const { openAsync } = useAsyncWindowOpen()
|
||||
|
||||
const onClickInstalledApp = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
try {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
window.open(`${basePath}/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||
else
|
||||
|
||||
openAsync(
|
||||
async () => {
|
||||
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||
if (installed_apps?.length > 0)
|
||||
return `${basePath}/explore/installed/${installed_apps[0].id}`
|
||||
throw new Error('No app found in Explore')
|
||||
}
|
||||
catch (e: any) {
|
||||
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||
}
|
||||
},
|
||||
{
|
||||
errorMessage: 'Failed to open app in Explore',
|
||||
},
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Toast from '../../../../base/toast'
|
|||
import { PlanRange } from '../../plan-switcher/plan-range-switcher'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
|
||||
import List from './list'
|
||||
import Button from './button'
|
||||
import { Professional, Sandbox, Team } from '../../assets'
|
||||
|
|
@ -54,6 +55,8 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||
})[plan]
|
||||
}, [isCurrent, plan, t])
|
||||
|
||||
const { openAsync } = useAsyncWindowOpen()
|
||||
|
||||
const handleGetPayUrl = async () => {
|
||||
if (loading)
|
||||
return
|
||||
|
|
@ -72,8 +75,13 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
|||
setLoading(true)
|
||||
try {
|
||||
if (isCurrentPaidPlan) {
|
||||
const res = await fetchBillingUrl()
|
||||
window.open(res.url, '_blank')
|
||||
openAsync(
|
||||
() => fetchBillingUrl().then(res => res.url),
|
||||
{
|
||||
errorMessage: 'Failed to open billing page',
|
||||
windowFeatures: 'noopener,noreferrer',
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { useCallback } from 'react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
export type AsyncWindowOpenOptions = {
|
||||
successMessage?: string
|
||||
errorMessage?: string
|
||||
windowFeatures?: string
|
||||
onError?: (error: any) => void
|
||||
onSuccess?: (url: string) => void
|
||||
}
|
||||
|
||||
export const useAsyncWindowOpen = () => {
|
||||
const openAsync = useCallback(async (
|
||||
fetchUrl: () => Promise<string>,
|
||||
options: AsyncWindowOpenOptions = {},
|
||||
) => {
|
||||
const {
|
||||
successMessage,
|
||||
errorMessage = 'Failed to open page',
|
||||
windowFeatures = 'noopener,noreferrer',
|
||||
onError,
|
||||
onSuccess,
|
||||
} = options
|
||||
|
||||
const newWindow = window.open('', '_blank', windowFeatures)
|
||||
|
||||
try {
|
||||
const url = await fetchUrl()
|
||||
|
||||
if (url && newWindow) {
|
||||
newWindow.location.href = url
|
||||
onSuccess?.(url)
|
||||
|
||||
if (successMessage) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: successMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
newWindow?.close()
|
||||
const error = new Error('Invalid URL or window was closed')
|
||||
onError?.(error)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
newWindow?.close()
|
||||
onError?.(error)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { openAsync }
|
||||
}
|
||||
Loading…
Reference in New Issue