refactor(web): internalize trigger limit modal state (#39794)

This commit is contained in:
yyh 2026-07-30 15:07:23 +08:00 committed by GitHub
parent 01ff1b5128
commit 0bb1b4d6d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 65 additions and 61 deletions

View File

@ -71,7 +71,6 @@ const buildModalContext = (): ModalContextState => ({
setShowExternalKnowledgeAPIModal: vi.fn(),
setShowOpeningModal: vi.fn(),
setShowUpdatePluginModal: vi.fn(),
setShowTriggerEventsLimitModal: vi.fn(),
})
vi.mock('@/context/modal-context', () => ({

View File

@ -33,7 +33,6 @@ vi.mock('@/context/modal-context', () => ({
setShowExternalKnowledgeAPIModal: vi.fn(),
setShowOpeningModal: vi.fn(),
setShowUpdatePluginModal: vi.fn(),
setShowTriggerEventsLimitModal: vi.fn(),
}),
}))

View File

@ -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,
}
}

View File

@ -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()
}}
/>

View File

@ -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()
})
})

View File

@ -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)