mirror of
https://github.com/langgenius/dify.git
synced 2026-07-31 01:09:32 +08:00
refactor(web): internalize trigger limit modal state (#39794)
This commit is contained in:
parent
01ff1b5128
commit
0bb1b4d6d2
@ -71,7 +71,6 @@ const buildModalContext = (): ModalContextState => ({
|
||||
setShowExternalKnowledgeAPIModal: vi.fn(),
|
||||
setShowOpeningModal: vi.fn(),
|
||||
setShowUpdatePluginModal: vi.fn(),
|
||||
setShowTriggerEventsLimitModal: vi.fn(),
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
|
||||
@ -33,7 +33,6 @@ vi.mock('@/context/modal-context', () => ({
|
||||
setShowExternalKnowledgeAPIModal: vi.fn(),
|
||||
setShowOpeningModal: vi.fn(),
|
||||
setShowUpdatePluginModal: vi.fn(),
|
||||
setShowTriggerEventsLimitModal: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { ModalState } from '../modal-context'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
@ -8,12 +6,15 @@ import { Plan } from '@/app/components/billing/type'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { isServer } from '@/utils/client'
|
||||
|
||||
export type TriggerEventsLimitModalPayload = {
|
||||
type TriggerEventsLimitModalContent = {
|
||||
usage: number
|
||||
total: number
|
||||
resetInDays?: number
|
||||
storageKey?: string
|
||||
persistDismiss?: boolean
|
||||
}
|
||||
|
||||
type TriggerEventsLimitModalState = TriggerEventsLimitModalContent & {
|
||||
storageKey: string
|
||||
persistDismiss: boolean
|
||||
}
|
||||
|
||||
type TriggerPlanInfo = {
|
||||
@ -30,11 +31,8 @@ type UseTriggerEventsLimitModalOptions = {
|
||||
}
|
||||
|
||||
type UseTriggerEventsLimitModalResult = {
|
||||
showTriggerEventsLimitModal: ModalState<TriggerEventsLimitModalPayload> | null
|
||||
setShowTriggerEventsLimitModal: Dispatch<
|
||||
SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>
|
||||
>
|
||||
persistTriggerEventsLimitModalDismiss: () => void
|
||||
triggerEventsLimitModal: TriggerEventsLimitModalContent | null
|
||||
dismissTriggerEventsLimitModal: () => void
|
||||
}
|
||||
|
||||
const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed'
|
||||
@ -48,8 +46,8 @@ export const useTriggerEventsLimitModal = ({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: ({ deployment_edition }) => deployment_edition,
|
||||
})
|
||||
const [showTriggerEventsLimitModal, setShowTriggerEventsLimitModal] =
|
||||
useState<ModalState<TriggerEventsLimitModalPayload> | null>(null)
|
||||
const [triggerEventsLimitModal, setTriggerEventsLimitModal] =
|
||||
useState<TriggerEventsLimitModalState | null>(null)
|
||||
const dismissedTriggerEventsLimitStorageKeysRef = useRef<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
@ -57,7 +55,7 @@ export const useTriggerEventsLimitModal = ({
|
||||
if (isServer) return
|
||||
if (!currentWorkspaceId) return
|
||||
if (!isFetchedPlan) {
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
setTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
@ -66,7 +64,7 @@ export const useTriggerEventsLimitModal = ({
|
||||
const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents
|
||||
|
||||
if (type === Plan.team || isUnlimited || !reachedLimit) {
|
||||
if (showTriggerEventsLimitModal) setShowTriggerEventsLimitModal(null)
|
||||
if (triggerEventsLimitModal) setTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
@ -92,36 +90,36 @@ export const useTriggerEventsLimitModal = ({
|
||||
}
|
||||
if (hasDismissed) return
|
||||
|
||||
if (showTriggerEventsLimitModal?.payload.storageKey === storageKey) return
|
||||
if (triggerEventsLimitModal?.storageKey === storageKey) return
|
||||
|
||||
setShowTriggerEventsLimitModal({
|
||||
payload: {
|
||||
usage: usage.triggerEvents,
|
||||
total: total.triggerEvents,
|
||||
resetInDays: triggerResetInDays,
|
||||
storageKey,
|
||||
persistDismiss,
|
||||
},
|
||||
setTriggerEventsLimitModal({
|
||||
usage: usage.triggerEvents,
|
||||
total: total.triggerEvents,
|
||||
resetInDays: triggerResetInDays,
|
||||
storageKey,
|
||||
persistDismiss,
|
||||
})
|
||||
}, [plan, isFetchedPlan, showTriggerEventsLimitModal, currentWorkspaceId, deploymentEdition])
|
||||
}, [plan, isFetchedPlan, triggerEventsLimitModal, currentWorkspaceId, deploymentEdition])
|
||||
|
||||
const persistTriggerEventsLimitModalDismiss = useCallback(() => {
|
||||
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
||||
if (!storageKey) return
|
||||
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
|
||||
const dismissTriggerEventsLimitModal = useCallback(() => {
|
||||
if (!triggerEventsLimitModal) return
|
||||
|
||||
const { storageKey, persistDismiss } = triggerEventsLimitModal
|
||||
if (persistDismiss) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, '1')
|
||||
setTriggerEventsLimitModal(null)
|
||||
return
|
||||
} catch {
|
||||
// ignore error and fall back to in-memory guard
|
||||
}
|
||||
}
|
||||
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
|
||||
}, [showTriggerEventsLimitModal])
|
||||
setTriggerEventsLimitModal(null)
|
||||
}, [triggerEventsLimitModal])
|
||||
|
||||
return {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
triggerEventsLimitModal,
|
||||
dismissTriggerEventsLimitModal,
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,11 +93,7 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
} = useTriggerEventsLimitModal({
|
||||
const { triggerEventsLimitModal, dismissTriggerEventsLimitModal } = useTriggerEventsLimitModal({
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
currentWorkspaceId,
|
||||
@ -204,7 +200,7 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
showExternalKnowledgeAPIModal ||
|
||||
showOpeningModal ||
|
||||
showUpdatePluginModal ||
|
||||
showTriggerEventsLimitModal,
|
||||
triggerEventsLimitModal,
|
||||
)
|
||||
|
||||
return (
|
||||
@ -219,7 +215,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
setShowExternalKnowledgeAPIModal,
|
||||
setShowOpeningModal,
|
||||
setShowUpdatePluginModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
@ -298,19 +293,15 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!!showTriggerEventsLimitModal && (
|
||||
{!!triggerEventsLimitModal && (
|
||||
<TriggerEventsLimitModal
|
||||
show
|
||||
usage={showTriggerEventsLimitModal.payload.usage}
|
||||
total={showTriggerEventsLimitModal.payload.total}
|
||||
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
|
||||
onClose={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
}}
|
||||
usage={triggerEventsLimitModal.usage}
|
||||
total={triggerEventsLimitModal.total}
|
||||
resetInDays={triggerEventsLimitModal.resetInDays}
|
||||
onClose={dismissTriggerEventsLimitModal}
|
||||
onUpgrade={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
dismissTriggerEventsLimitModal()
|
||||
handleShowPricingModal()
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { render } from '@/test/console/render'
|
||||
@ -60,9 +61,13 @@ const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
|
||||
},
|
||||
})
|
||||
|
||||
const renderProvider = (
|
||||
children: React.ReactNode = <div data-testid="modal-context-test-child" />,
|
||||
) => {
|
||||
const ModalBlockingState = () => {
|
||||
const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen)
|
||||
|
||||
return <output>{hasBlockingModalOpen ? 'blocked' : 'clear'}</output>
|
||||
}
|
||||
|
||||
const renderProvider = (children: React.ReactNode = <ModalBlockingState />) => {
|
||||
const { wrapper: QueryWrapper } = createConsoleQueryWrapper({
|
||||
systemFeatures: { deployment_edition: 'CLOUD' },
|
||||
})
|
||||
@ -112,10 +117,12 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
expect(screen.getAllByText('3000')).toHaveLength(2)
|
||||
expect(screen.getByText('blocked')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(screen.getByText('clear')).toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
|
||||
})
|
||||
@ -141,13 +148,20 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
const setItemSpy = vi.spyOn(localStorage, 'setItem')
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider()
|
||||
const { rerender } = renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
rerender(
|
||||
<ModalContextProvider>
|
||||
<ModalBlockingState />
|
||||
</ModalContextProvider>,
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(screen.getByText('clear')).toBeInTheDocument()
|
||||
expect(setItemSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -167,13 +181,20 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider()
|
||||
const { rerender } = renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
rerender(
|
||||
<ModalContextProvider>
|
||||
<ModalBlockingState />
|
||||
</ModalContextProvider>,
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(screen.getByText('clear')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes the trigger events limit modal and opens pricing when upgrading', async () => {
|
||||
@ -199,5 +220,6 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
expect(screen.getByText('billing.plansCommon.mostPopular')).toBeInTheDocument(),
|
||||
)
|
||||
expect(screen.queryByText('400')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('blocked')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { TriggerEventsLimitModalPayload } from './hooks/use-trigger-events-limit-modal'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
|
||||
import type {
|
||||
@ -60,9 +59,6 @@ export type ModalContextState = {
|
||||
> | null>
|
||||
>
|
||||
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
|
||||
setShowTriggerEventsLimitModal: Dispatch<
|
||||
SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>
|
||||
>
|
||||
}
|
||||
|
||||
export const ModalContext = createContext<ModalContextState>({
|
||||
@ -75,7 +71,6 @@ export const ModalContext = createContext<ModalContextState>({
|
||||
setShowExternalKnowledgeAPIModal: noop,
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
setShowTriggerEventsLimitModal: noop,
|
||||
})
|
||||
|
||||
export const useModalContext = () => useContext(ModalContext)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user