mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:06:51 +08:00
refactor(web): migrate HITL overlays to base dialog (#35792)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
1f29565673
commit
8e2b8168be
@ -1969,11 +1969,6 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/plan-upgrade-modal/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/billing/plan/assets/index.tsx": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 4
|
||||
@ -4195,37 +4190,6 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-non-null-asserted-optional-chain": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/components/form-content-preview.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
@ -4250,11 +4214,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/panel.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/human-input/types.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 2
|
||||
@ -5284,21 +5243,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/context/modal-context-provider.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/context/modal-context.test.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"web/context/modal-context.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/context/provider-context-provider.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@ -9,7 +9,7 @@ import Billing from '@/app/components/billing/billing-page'
|
||||
import { defaultPlan, NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import HeaderBillingBtn from '@/app/components/billing/header-billing-btn'
|
||||
import PlanComp from '@/app/components/billing/plan'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import PriorityLabel from '@/app/components/billing/priority-label'
|
||||
import TriggerEventsLimitModal from '@/app/components/billing/trigger-events-limit-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
|
||||
75
web/app/components/base/upgrade-modal/index.tsx
Normal file
75
web/app/components/base/upgrade-modal/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import styles from './style.module.css'
|
||||
|
||||
type UpgradeModalClassNames = {
|
||||
content?: string
|
||||
heroOverlay?: string
|
||||
body?: string
|
||||
icon?: string
|
||||
copy?: string
|
||||
title?: string
|
||||
description?: string
|
||||
footer?: string
|
||||
}
|
||||
|
||||
type UpgradeModalProps = {
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
Icon?: ComponentType<{ className?: string }>
|
||||
title: ReactNode
|
||||
description: ReactNode
|
||||
extraInfo?: ReactNode
|
||||
footer: ReactNode
|
||||
classNames?: UpgradeModalClassNames
|
||||
}
|
||||
|
||||
export function UpgradeModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
Icon,
|
||||
title,
|
||||
description,
|
||||
extraInfo,
|
||||
footer,
|
||||
classNames,
|
||||
}: UpgradeModalProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent className={cn(styles.surface, 'w-[580px] max-w-[480px] overflow-hidden rounded-2xl p-0', classNames?.content)}>
|
||||
<div className="relative">
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(styles.heroOverlay, 'pointer-events-none absolute inset-0', classNames?.heroOverlay)}
|
||||
/>
|
||||
<div className={cn('px-8 pt-8', classNames?.body)}>
|
||||
{Icon && (
|
||||
<div className={cn(styles.icon, 'flex size-12 items-center justify-center rounded-xl shadow-lg backdrop-blur-[5px]', classNames?.icon)}>
|
||||
<Icon className="size-6 text-text-primary-on-surface" />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('mt-6 space-y-2', classNames?.copy)}>
|
||||
<DialogTitle className={cn(styles.highlight, 'title-3xl-semi-bold', classNames?.title)}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription className={cn('system-md-regular text-text-tertiary', classNames?.description)}>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
{extraInfo}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn('mt-10 mb-8 flex justify-end space-x-2 px-8', classNames?.footer)}>
|
||||
{footer}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -1,19 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import PlanUpgradeModal from '../index'
|
||||
import { PlanUpgradeModal } from '../index'
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/modal', () => {
|
||||
const MockModal = ({ isShow, children }: { isShow: boolean, children: React.ReactNode }) => (
|
||||
isShow ? <div data-testid="plan-upgrade-modal">{children}</div> : null
|
||||
)
|
||||
return {
|
||||
default: MockModal,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
@ -70,6 +60,16 @@ describe('PlanUpgradeModal', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when dialog requests close', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
renderComponent({ onClose })
|
||||
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Upgrade path uses provided callback over pricing modal
|
||||
it('should call onUpgrade and onClose when upgrade button is clicked with onUpgrade provided', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
@ -1,26 +1,24 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { UpgradeModal } from '@/app/components/base/upgrade-modal'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { SquareChecklist } from '../../base/icons/src/vender/other'
|
||||
import styles from './style.module.css'
|
||||
|
||||
type Props = {
|
||||
Icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
||||
Icon?: ComponentType<{ className?: string }>
|
||||
title: string
|
||||
description: string
|
||||
extraInfo?: React.ReactNode
|
||||
extraInfo?: ReactNode
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
onUpgrade?: () => void
|
||||
}
|
||||
|
||||
const PlanUpgradeModal: FC<Props> = ({
|
||||
export function PlanUpgradeModal({
|
||||
Icon = SquareChecklist,
|
||||
title,
|
||||
description,
|
||||
@ -28,7 +26,7 @@ const PlanUpgradeModal: FC<Props> = ({
|
||||
show,
|
||||
onClose,
|
||||
onUpgrade,
|
||||
}) => {
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
|
||||
@ -41,51 +39,30 @@ const PlanUpgradeModal: FC<Props> = ({
|
||||
}, [onClose, onUpgrade, setShowPricingModal])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onClose}
|
||||
closable={false}
|
||||
clickOutsideNotClose
|
||||
className={`${styles.surface} w-[580px] rounded-2xl p-0!`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div
|
||||
aria-hidden
|
||||
className={`${styles.heroOverlay} pointer-events-none absolute inset-0`}
|
||||
/>
|
||||
<div className="px-8 pt-8">
|
||||
<div className={`${styles.icon} flex size-12 items-center justify-center rounded-xl shadow-lg backdrop-blur-[5px]`}>
|
||||
<Icon className="size-6 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<div className={`${styles.highlight} title-3xl-semi-bold`}>
|
||||
{title}
|
||||
</div>
|
||||
<div className="system-md-regular text-text-tertiary">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
{extraInfo}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 mb-8 flex justify-end space-x-2 px-8">
|
||||
<Button
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('triggerLimitModal.dismiss', { ns: 'billing' })}
|
||||
</Button>
|
||||
<UpgradeBtn
|
||||
size="custom"
|
||||
isShort
|
||||
onClick={handleUpgrade}
|
||||
className="h-8! rounded-lg! px-2"
|
||||
labelKey="triggerLimitModal.upgrade"
|
||||
loc="trigger-events-limit-modal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
<UpgradeModal
|
||||
open={show}
|
||||
onOpenChange={open => !open && onClose()}
|
||||
Icon={Icon}
|
||||
title={title}
|
||||
description={description}
|
||||
extraInfo={extraInfo}
|
||||
footer={(
|
||||
<>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('triggerLimitModal.dismiss', { ns: 'billing' })}
|
||||
</Button>
|
||||
<UpgradeBtn
|
||||
size="custom"
|
||||
isShort
|
||||
onClick={handleUpgrade}
|
||||
className="h-8! rounded-lg! px-2"
|
||||
labelKey="triggerLimitModal.upgrade"
|
||||
loc="trigger-events-limit-modal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PlanUpgradeModal)
|
||||
|
||||
@ -4,21 +4,6 @@ import TriggerEventsLimitModal from '../index'
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnUpgrade = vi.fn()
|
||||
|
||||
const planUpgradeModalMock = vi.fn((props: { show: boolean, title: string, description: string, extraInfo?: React.ReactNode, onClose: () => void, onUpgrade: () => void }) => (
|
||||
<div
|
||||
data-testid="plan-upgrade-modal"
|
||||
data-show={props.show}
|
||||
data-title={props.title}
|
||||
data-description={props.description}
|
||||
>
|
||||
{props.extraInfo}
|
||||
</div>
|
||||
))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: (props: { show: boolean, title: string, description: string, extraInfo?: React.ReactNode, onClose: () => void, onUpgrade: () => void }) => planUpgradeModalMock(props),
|
||||
}))
|
||||
|
||||
describe('TriggerEventsLimitModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -36,16 +21,9 @@ describe('TriggerEventsLimitModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const modal = screen.getByTestId('plan-upgrade-modal')
|
||||
expect(modal.getAttribute('data-show')).toBe('true')
|
||||
expect(modal.getAttribute('data-title')).toContain('billing.triggerLimitModal.title')
|
||||
expect(modal.getAttribute('data-description')).toContain('billing.triggerLimitModal.description')
|
||||
expect(planUpgradeModalMock).toHaveBeenCalled()
|
||||
|
||||
const passedProps = planUpgradeModalMock.mock.calls[0]![0]
|
||||
expect(passedProps.onClose).toBe(mockOnClose)
|
||||
expect(passedProps.onUpgrade).toBe(mockOnUpgrade)
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.triggerLimitModal.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.triggerLimitModal.description')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.triggerLimitModal.usageTitle'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('12'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('20'))!.toBeInTheDocument()
|
||||
@ -62,8 +40,7 @@ describe('TriggerEventsLimitModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(planUpgradeModalMock).toHaveBeenCalled()
|
||||
expect(screen.getByTestId('plan-upgrade-modal').getAttribute('data-show')).toBe('false')
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders reset info when resetInDays is provided', () => {
|
||||
@ -94,9 +71,8 @@ describe('TriggerEventsLimitModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const modal = screen.getByTestId('plan-upgrade-modal')
|
||||
expect(modal.getAttribute('data-title')).toBe('billing.triggerLimitModal.title')
|
||||
expect(modal.getAttribute('data-description')).toBe('billing.triggerLimitModal.description')
|
||||
expect(screen.getByText('billing.triggerLimitModal.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.triggerLimitModal.description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('passes onClose and onUpgrade callbacks to PlanUpgradeModal', () => {
|
||||
@ -110,8 +86,10 @@ describe('TriggerEventsLimitModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const passedProps = planUpgradeModalMock.mock.calls[0]![0]
|
||||
expect(passedProps.onClose).toBe(mockOnClose)
|
||||
expect(passedProps.onUpgrade).toBe(mockOnUpgrade)
|
||||
screen.getByText('billing.triggerLimitModal.dismiss').click()
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
|
||||
screen.getByText('billing.triggerLimitModal.upgrade').click()
|
||||
expect(mockOnUpgrade).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import UsageInfo from '@/app/components/billing/usage-info'
|
||||
|
||||
type Props = {
|
||||
@ -15,14 +13,14 @@ type Props = {
|
||||
resetInDays?: number
|
||||
}
|
||||
|
||||
const TriggerEventsLimitModal: FC<Props> = ({
|
||||
export default function TriggerEventsLimitModal({
|
||||
show,
|
||||
onClose,
|
||||
onUpgrade,
|
||||
usage,
|
||||
total,
|
||||
resetInDays,
|
||||
}) => {
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@ -30,7 +28,7 @@ const TriggerEventsLimitModal: FC<Props> = ({
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
onUpgrade={onUpgrade}
|
||||
Icon={TriggerAll as React.ComponentType<React.SVGProps<SVGSVGElement>>}
|
||||
Icon={TriggerAll}
|
||||
title={t('triggerLimitModal.title', { ns: 'billing' })}
|
||||
description={t('triggerLimitModal.description', { ns: 'billing' })}
|
||||
extraInfo={(
|
||||
@ -47,5 +45,3 @@ const TriggerEventsLimitModal: FC<Props> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TriggerEventsLimitModal)
|
||||
|
||||
@ -92,18 +92,6 @@ vi.mock('@/app/components/billing/vector-space-full', () => ({
|
||||
default: () => <div data-testid="vector-space-full">Vector Space Full</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose }: { show: boolean, onClose: () => void }) => (
|
||||
show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<button data-testid="close-upgrade-modal" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../file-preview', () => ({
|
||||
default: ({ file, hidePreview }: { file: File, hidePreview: () => void }) => (
|
||||
<div data-testid="file-preview">
|
||||
@ -388,7 +376,7 @@ describe('StepOne', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show upgrade card when in sandbox plan with files', () => {
|
||||
|
||||
@ -31,17 +31,6 @@ vi.mock('../../../website/preview', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose, title }: { show: boolean, onClose: () => void, title: string }) => show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<span>{title}</span>
|
||||
<button data-testid="close-modal" onClick={onClose}>close-modal</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
const { default: PreviewPanel } = await import('../preview-panel')
|
||||
|
||||
describe('PreviewPanel', () => {
|
||||
@ -87,7 +76,7 @@ describe('PreviewPanel', () => {
|
||||
|
||||
it('should render plan upgrade modal when isShowPlanUpgradeModal is true', () => {
|
||||
render(<PreviewPanel {...defaultProps} isShowPlanUpgradeModal />)
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -100,7 +89,7 @@ describe('PreviewPanel', () => {
|
||||
|
||||
it('should call hidePlanUpgradeModal when modal close clicked', () => {
|
||||
render(<PreviewPanel {...defaultProps} isShowPlanUpgradeModal />)
|
||||
fireEvent.click(screen.getByTestId('close-modal'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
expect(defaultProps.hidePlanUpgradeModal).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import FilePreview from '../../file-preview'
|
||||
import NotionPagePreview from '../../notion-page-preview'
|
||||
import WebsitePreview from '../../website/preview'
|
||||
|
||||
@ -112,18 +112,6 @@ vi.mock('@/app/components/billing/vector-space-full', () => ({
|
||||
default: () => <div data-testid="vector-space-full">Vector Space Full</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose }: { show: boolean, onClose: () => void }) => (
|
||||
show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<button data-testid="close-modal" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/create/step-one/upgrade-card', () => ({
|
||||
default: () => <div data-testid="upgrade-card">Upgrade Card</div>,
|
||||
}))
|
||||
|
||||
@ -8,7 +8,7 @@ import { useBoolean } from 'ahooks'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
|
||||
@ -14,21 +14,6 @@ vi.mock('@/context/provider-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock PlanUpgradeModal
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose, title, description }: { show: boolean, onClose: () => void, title?: string, description?: string }) => (
|
||||
show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<span data-testid="modal-title">{title}</span>
|
||||
<span data-testid="modal-description">{description}</span>
|
||||
<button onClick={onClose} data-testid="close-modal">Close</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
describe('SegmentAdd', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -189,7 +174,7 @@ describe('SegmentAdd', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
|
||||
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not call showNewSegmentModal for sandbox users', () => {
|
||||
@ -219,11 +204,11 @@ describe('SegmentAdd', () => {
|
||||
|
||||
// Show modal
|
||||
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-modal'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
expect(screen.queryByTestId('plan-upgrade-modal')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
|
||||
@ -30,11 +30,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>tooltip</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
__esModule: true,
|
||||
default: (props: {
|
||||
|
||||
@ -92,9 +92,9 @@ describe('human-input/delivery-method/email-configure-modal', () => {
|
||||
|
||||
render(
|
||||
<EmailConfigureModal
|
||||
isShow
|
||||
open
|
||||
config={createEmailConfig()}
|
||||
onClose={vi.fn()}
|
||||
onOpenChange={vi.fn()}
|
||||
onConfirm={handleConfirm}
|
||||
/>,
|
||||
)
|
||||
@ -127,8 +127,8 @@ describe('human-input/delivery-method/email-configure-modal', () => {
|
||||
|
||||
render(
|
||||
<EmailConfigureModal
|
||||
isShow
|
||||
onClose={vi.fn()}
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
onConfirm={handleConfirm}
|
||||
/>,
|
||||
)
|
||||
@ -162,12 +162,12 @@ describe('human-input/delivery-method/email-configure-modal', () => {
|
||||
})
|
||||
|
||||
it('should close from both the icon trigger and the cancel button', () => {
|
||||
const handleClose = vi.fn()
|
||||
const handleOpenChange = vi.fn()
|
||||
render(
|
||||
<EmailConfigureModal
|
||||
isShow
|
||||
open
|
||||
config={createEmailConfig()}
|
||||
onClose={handleClose}
|
||||
onOpenChange={handleOpenChange}
|
||||
onConfirm={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
@ -175,6 +175,7 @@ describe('human-input/delivery-method/email-configure-modal', () => {
|
||||
fireEvent.click(screen.getByRole('dialog').querySelector('.absolute') as HTMLDivElement)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(handleClose).toHaveBeenCalledTimes(2)
|
||||
expect(handleOpenChange).toHaveBeenCalledTimes(2)
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { DeliveryMethodType } from '../../../types'
|
||||
import DeliveryMethodForm from '../index'
|
||||
|
||||
@ -9,11 +9,6 @@ vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => mockUseTranslation(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
__esModule: true,
|
||||
default: ({ popupContent }: { popupContent: string }) => <div data-testid="tooltip">{popupContent}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesSyncDraft: () => mockUseNodesSyncDraft(),
|
||||
}))
|
||||
@ -62,15 +57,6 @@ vi.mock('../method-item', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../upgrade-modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<button type="button" onClick={onClose}>
|
||||
upgrade-modal
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('DeliveryMethodForm', () => {
|
||||
const onChange = vi.fn()
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
@ -132,7 +118,7 @@ describe('DeliveryMethodForm', () => {
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should open and close the upgrade modal', () => {
|
||||
it('should open and close the upgrade modal', async () => {
|
||||
render(
|
||||
<DeliveryMethodForm
|
||||
nodeId="node-1"
|
||||
@ -142,9 +128,9 @@ describe('DeliveryMethodForm', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('show-upgrade'))
|
||||
expect(screen.getByText('upgrade-modal')).toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('upgrade-modal'))
|
||||
expect(screen.queryByText('upgrade-modal')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'nodes.humanInput.deliveryMethod.upgradeTipHide' }))
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
})
|
||||
})
|
||||
|
||||
@ -6,15 +6,15 @@ import { DeliveryMethodType } from '../../../types'
|
||||
import DeliveryMethodItem from '../method-item'
|
||||
|
||||
type EmailConfigureModalProps = {
|
||||
isShow: boolean
|
||||
open: boolean
|
||||
config?: EmailConfig
|
||||
onClose: () => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (data: EmailConfig) => void
|
||||
}
|
||||
|
||||
type TestEmailSenderProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
jumpToEmailConfigModal: () => void
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ vi.mock('@/context/app-context', () => ({
|
||||
vi.mock('../email-configure-modal', () => ({
|
||||
default: (props: EmailConfigureModalProps) => {
|
||||
mockEmailConfigureModal(props)
|
||||
return props.isShow
|
||||
return props.open
|
||||
? (
|
||||
<div data-testid="email-configure-modal">
|
||||
<button
|
||||
@ -44,7 +44,7 @@ vi.mock('../email-configure-modal', () => ({
|
||||
>
|
||||
confirm-email-config
|
||||
</button>
|
||||
<button type="button" onClick={props.onClose}>close-email-config</button>
|
||||
<button type="button" onClick={() => props.onOpenChange(false)}>close-email-config</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
@ -54,11 +54,11 @@ vi.mock('../email-configure-modal', () => ({
|
||||
vi.mock('../test-email-sender', () => ({
|
||||
default: (props: TestEmailSenderProps) => {
|
||||
mockTestEmailSender(props)
|
||||
return props.isShow
|
||||
return props.open
|
||||
? (
|
||||
<div data-testid="test-email-sender">
|
||||
<button type="button" onClick={props.jumpToEmailConfigModal}>jump-to-config</button>
|
||||
<button type="button" onClick={props.onClose}>close-test-sender</button>
|
||||
<button type="button" onClick={() => props.onOpenChange(false)}>close-test-sender</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
@ -140,14 +140,14 @@ describe('human-input/delivery-method/method-item', () => {
|
||||
|
||||
const row = getMethodRow('webapp')
|
||||
const actionButtons = within(row).getAllByRole('button')
|
||||
const deleteButtonWrapper = actionButtons[0]!.parentElement as HTMLDivElement
|
||||
const deleteButton = actionButtons[0]!
|
||||
|
||||
fireEvent.mouseEnter(deleteButtonWrapper)
|
||||
fireEvent.mouseEnter(deleteButton)
|
||||
expect(row)!.toHaveClass('border-state-destructive-border')
|
||||
fireEvent.mouseLeave(deleteButtonWrapper)
|
||||
fireEvent.mouseLeave(deleteButton)
|
||||
expect(row).not.toHaveClass('border-state-destructive-border')
|
||||
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(deleteButton)
|
||||
expect(handleDelete).toHaveBeenCalledWith(DeliveryMethodType.WebApp)
|
||||
})
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ describe('human-input/delivery-method/method-selector', () => {
|
||||
})
|
||||
expect(handleShowUpgradeTip).not.toHaveBeenCalled()
|
||||
expect(screen.getByText('workflow.nodes.humanInput.deliveryMethod.contactTip1')).toBeInTheDocument()
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('nodes.humanInput.deliveryMethod.contactTip2')
|
||||
expect(screen.getByText('nodes.humanInput.deliveryMethod.contactTip2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable webapp in trigger mode and show added states without creating duplicates', () => {
|
||||
|
||||
@ -0,0 +1,298 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { EmailConfig, FormInputItem } from '../../../types'
|
||||
import type { App, AppSSO } from '@/types/app'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider'
|
||||
import { createHooksStore } from '@/app/components/workflow/hooks-store/store'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import { AppContext, initialLangGeniusVersionInfo, initialWorkspaceInfo, userProfilePlaceholder } from '@/context/app-context'
|
||||
import EmailSenderModal from '../test-email-sender'
|
||||
|
||||
type RecordedRequest = {
|
||||
url: string
|
||||
method: string
|
||||
body?: unknown
|
||||
}
|
||||
|
||||
const createQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithProviders = (ui: ReactNode) => {
|
||||
const queryClient = createQueryClient()
|
||||
const hooksStore = createHooksStore({})
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
userProfile: {
|
||||
...userProfilePlaceholder,
|
||||
id: 'user-1',
|
||||
email: 'owner@example.com',
|
||||
name: 'Owner',
|
||||
},
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
id: 'workspace-1',
|
||||
name: 'Product Team',
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
mutateUserProfile: vi.fn(),
|
||||
mutateCurrentWorkspace: vi.fn(),
|
||||
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||
useSelector: selector => selector({
|
||||
userProfile: {
|
||||
...userProfilePlaceholder,
|
||||
id: 'user-1',
|
||||
email: 'owner@example.com',
|
||||
name: 'Owner',
|
||||
},
|
||||
currentWorkspace: {
|
||||
...initialWorkspaceInfo,
|
||||
id: 'workspace-1',
|
||||
name: 'Product Team',
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceDatasetOperator: true,
|
||||
mutateUserProfile: vi.fn(),
|
||||
mutateCurrentWorkspace: vi.fn(),
|
||||
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||
useSelector: vi.fn(),
|
||||
isLoadingCurrentWorkspace: false,
|
||||
isValidatingCurrentWorkspace: false,
|
||||
}),
|
||||
isLoadingCurrentWorkspace: false,
|
||||
isValidatingCurrentWorkspace: false,
|
||||
}}
|
||||
>
|
||||
<HooksStoreContext.Provider value={hooksStore}>
|
||||
{ui}
|
||||
</HooksStoreContext.Provider>
|
||||
</AppContext.Provider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
const setupFetch = () => {
|
||||
const requests: RecordedRequest[] = []
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (resource: RequestInfo | URL, options?: RequestInit) => {
|
||||
const request = resource instanceof Request ? resource : new Request(resource, options)
|
||||
const body = request.method === 'GET' ? undefined : await request.clone().json()
|
||||
requests.push({
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
body,
|
||||
})
|
||||
|
||||
if (request.url.includes('/workspaces/current/members')) {
|
||||
return new Response(JSON.stringify({
|
||||
accounts: [
|
||||
{
|
||||
id: 'member-1',
|
||||
email: 'member@example.com',
|
||||
name: 'Member One',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
status: 'active',
|
||||
role: 'normal',
|
||||
created_at: '',
|
||||
last_active_at: '',
|
||||
last_login_at: '',
|
||||
},
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ result: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
fetchSpy,
|
||||
requests,
|
||||
}
|
||||
}
|
||||
|
||||
const createConfig = (overrides: Partial<EmailConfig> = {}): EmailConfig => ({
|
||||
recipients: {
|
||||
whole_workspace: true,
|
||||
items: [],
|
||||
},
|
||||
subject: 'Review request',
|
||||
body: 'Please review {{#start.score#}}',
|
||||
debug_mode: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createFormInput = (overrides: Partial<FormInputItem> = {}): FormInputItem => ({
|
||||
type: InputVarType.textInput,
|
||||
output_variable_name: 'user_name',
|
||||
default: {
|
||||
type: 'variable',
|
||||
selector: ['start', 'user_name'],
|
||||
value: '',
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('human-input/delivery-method/test-email-sender', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({
|
||||
appDetail: {
|
||||
id: 'app-1',
|
||||
name: 'Workflow App',
|
||||
} as App & Partial<AppSSO>,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should submit generated variable inputs and show the success state', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { requests } = setupFetch()
|
||||
const handleOpenChange = vi.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<EmailSenderModal
|
||||
nodeId="human-node"
|
||||
deliveryId="delivery-1"
|
||||
open
|
||||
onOpenChange={handleOpenChange}
|
||||
jumpToEmailConfigModal={vi.fn()}
|
||||
config={createConfig()}
|
||||
formInputs={[createFormInput()]}
|
||||
availableNodes={[
|
||||
{
|
||||
id: 'start',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: 'Start',
|
||||
desc: '',
|
||||
type: BlockEnum.Start,
|
||||
},
|
||||
},
|
||||
]}
|
||||
nodesOutputVars={[
|
||||
{
|
||||
nodeId: 'start',
|
||||
title: 'Start',
|
||||
vars: [
|
||||
{
|
||||
variable: 'user_name',
|
||||
type: VarType.string,
|
||||
},
|
||||
{
|
||||
variable: 'score',
|
||||
type: VarType.number,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
|
||||
const sendButton = screen.getByRole('button', { name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send' })
|
||||
expect(sendButton).toBeDisabled()
|
||||
|
||||
await user.type(screen.getByPlaceholderText('user_name'), 'Ada')
|
||||
await user.type(screen.getByPlaceholderText('score'), '42')
|
||||
expect(sendButton).toBeEnabled()
|
||||
|
||||
await user.click(sendButton)
|
||||
|
||||
await waitFor(() => expect(screen.getByText('workflow.nodes.humanInput.deliveryMethod.emailSender.done')).toBeInTheDocument())
|
||||
expect(requests).toContainEqual(expect.objectContaining({
|
||||
url: 'http://localhost:5001/console/api/apps/app-1/workflows/draft/human-input/nodes/human-node/delivery-test',
|
||||
method: 'POST',
|
||||
body: {
|
||||
delivery_method_id: 'delivery-1',
|
||||
inputs: {
|
||||
'#start.user_name#': 'Ada',
|
||||
'#start.score#': '42',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.ok' }))
|
||||
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should render fallback variable inputs and allow cancelling', async () => {
|
||||
const user = userEvent.setup()
|
||||
setupFetch()
|
||||
const handleOpenChange = vi.fn()
|
||||
|
||||
renderWithProviders(
|
||||
<EmailSenderModal
|
||||
nodeId="human-node"
|
||||
deliveryId="delivery-1"
|
||||
open
|
||||
onOpenChange={handleOpenChange}
|
||||
jumpToEmailConfigModal={vi.fn()}
|
||||
config={createConfig({
|
||||
body: 'Please review {{#unknown.message#}}',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByPlaceholderText('message')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('workflow.nodes.humanInput.deliveryMethod.emailSender.vars'))
|
||||
|
||||
expect(screen.queryByPlaceholderText('message')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(handleOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should show selected recipients with the email configuration tip', () => {
|
||||
setupFetch()
|
||||
|
||||
renderWithProviders(
|
||||
<EmailSenderModal
|
||||
nodeId="human-node"
|
||||
deliveryId="delivery-1"
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
jumpToEmailConfigModal={vi.fn()}
|
||||
config={createConfig({
|
||||
recipients: {
|
||||
whole_workspace: true,
|
||||
items: [{ type: 'external', email: 'external@example.com' }],
|
||||
},
|
||||
body: 'Please review {{#url#}}',
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('external@example.com')).toBeInTheDocument()
|
||||
expect(screen.getByText('nodes.humanInput.deliveryMethod.emailSender.tip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import UpgradeModal from '../upgrade-modal'
|
||||
import { UpgradeModal } from '../upgrade-modal'
|
||||
|
||||
const mockUseModalContextSelector = vi.hoisted(() => vi.fn())
|
||||
|
||||
@ -32,8 +32,8 @@ describe('human-input/delivery-method/upgrade-modal', () => {
|
||||
|
||||
render(
|
||||
<UpgradeModal
|
||||
isShow
|
||||
onClose={handleClose}
|
||||
open
|
||||
onOpenChange={handleClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
@ -41,7 +41,7 @@ describe('human-input/delivery-method/upgrade-modal', () => {
|
||||
expect(screen.getByText('workflow.nodes.humanInput.deliveryMethod.upgradeTipContent')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.humanInput.deliveryMethod.upgradeTipHide' }))
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
expect(handleClose).toHaveBeenCalledWith(false)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /billing.upgradeBtn.encourageShort/i }))
|
||||
expect(handleShowPricingModal).toHaveBeenCalledTimes(1)
|
||||
|
||||
@ -4,15 +4,13 @@ import type {
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiBugLine, RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { RiBugLine } from '@remixicon/react'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import MailBodyInput from './mail-body-input'
|
||||
import Recipient from './recipient'
|
||||
@ -20,8 +18,8 @@ import Recipient from './recipient'
|
||||
const i18nPrefix = 'nodes.humanInput'
|
||||
|
||||
type EmailConfigureModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (data: EmailConfig) => void
|
||||
config?: EmailConfig
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
@ -29,8 +27,8 @@ type EmailConfigureModalProps = {
|
||||
}
|
||||
|
||||
const EmailConfigureModal = ({
|
||||
isShow,
|
||||
onClose,
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
config,
|
||||
nodesOutputVars = [],
|
||||
@ -78,89 +76,87 @@ const EmailConfigureModal = ({
|
||||
}, [checkValidConfig, onConfirm, recipients, subject, body, debugMode])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className="relative max-w-[720px]! p-0!"
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<div className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="space-y-1 p-6 pb-3">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.title`, { ns: 'workflow' })}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.description`, { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
<div className="space-y-5 px-6 py-3">
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.subject`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={subject}
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.deliveryMethod.emailConfigure.subjectPlaceholder`, { ns: 'workflow' })}
|
||||
/>
|
||||
<DialogContent className="max-h-[calc(100dvh-64px)]! w-[720px]!">
|
||||
<DialogCloseButton />
|
||||
<div className="space-y-1 pr-8">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.title`, { ns: 'workflow' })}</DialogTitle>
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.description`, { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.body`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<MailBodyInput
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<Recipient
|
||||
data={recipients}
|
||||
onChange={setRecipients}
|
||||
/>
|
||||
</div>
|
||||
<Divider className="my-0! mt-5! h-px!" />
|
||||
<div className="flex items-start justify-between gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-3 pl-2.5 shadow-xs">
|
||||
<div className="rounded-sm border border-divider-regular bg-components-icon-bg-orange-dark-solid p-0.5">
|
||||
<RiBugLine className="h-3.5 w-3.5 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="grow space-y-1">
|
||||
<div className="system-sm-medium text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugMode`, { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip1`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="body-md-medium text-text-primary">{email}</span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
<div>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip2`, { ns: 'workflow' })}</div>
|
||||
<div className="mt-6 space-y-5">
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.subject`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={subject}
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.deliveryMethod.emailConfigure.subjectPlaceholder`, { ns: 'workflow' })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.body`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<MailBodyInput
|
||||
value={body}
|
||||
onChange={setBody}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<Recipient
|
||||
data={recipients}
|
||||
onChange={setRecipients}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-3 pl-2.5 shadow-xs">
|
||||
<div className="rounded-sm border border-divider-regular bg-components-icon-bg-orange-dark-solid p-0.5">
|
||||
<RiBugLine className="h-3.5 w-3.5 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="grow space-y-1">
|
||||
<div className="system-sm-medium text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugMode`, { ns: 'workflow' })}</div>
|
||||
<div className="body-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip1`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="body-md-medium text-text-primary">{email}</span> }}
|
||||
values={{ email }}
|
||||
/>
|
||||
<div>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.debugModeTip2`, { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={debugMode}
|
||||
onCheckedChange={checked => setDebugMode(checked)}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={debugMode}
|
||||
onCheckedChange={checked => setDebugMode(checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-[72px]"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="mt-6 flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-[72px]"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,14 +3,14 @@ import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
|
||||
import MethodItem from './method-item'
|
||||
import MethodSelector from './method-selector'
|
||||
import UpgradeModal from './upgrade-modal'
|
||||
import { UpgradeModal } from './upgrade-modal'
|
||||
|
||||
const i18nPrefix = 'nodes.humanInput'
|
||||
|
||||
@ -62,27 +62,15 @@ const DeliveryMethodForm: React.FC<Props> = ({
|
||||
const handleShowUpgradeModal = () => {
|
||||
setShowUpgradeModal(true)
|
||||
}
|
||||
const handleCloseUpgradeModal = () => {
|
||||
setShowUpgradeModal(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 py-2">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span className="flex h-3.5 w-3.5 shrink-0 p-px">
|
||||
<span aria-hidden className="i-ri-question-line h-full w-full text-text-quaternary hover:text-text-tertiary" />
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Infotip aria-label={t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })}>
|
||||
{t(`${i18nPrefix}.deliveryMethod.tooltip`, { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex items-center px-1">
|
||||
@ -115,12 +103,10 @@ const DeliveryMethodForm: React.FC<Props> = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showUpgradeModal && (
|
||||
<UpgradeModal
|
||||
isShow={showUpgradeModal}
|
||||
onClose={handleCloseUpgradeModal}
|
||||
/>
|
||||
)}
|
||||
<UpgradeModal
|
||||
open={showUpgradeModal}
|
||||
onOpenChange={setShowUpgradeModal}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
@ -18,7 +19,6 @@ import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge/index'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { DeliveryMethodType } from '../../types'
|
||||
@ -79,6 +79,8 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
|
||||
}
|
||||
return t(`${i18nPrefix}.deliveryMethod.emailSender.testSendTip`, { ns: 'workflow' })
|
||||
}, [method.type, method.config?.debug_mode, t, email])
|
||||
const configureLabel = t('common.configure', { ns: 'workflow' })
|
||||
const removeLabel = t('operation.remove', { ns: 'common' })
|
||||
|
||||
const jumpToEmailConfigModal = useCallback(() => {
|
||||
setShowTestEmailModal(false)
|
||||
@ -114,47 +116,49 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
|
||||
<div className="hidden items-end gap-1 group-hover:flex">
|
||||
{method.type === DeliveryMethodType.Email && method.config && (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={emailSenderTooltipContent}
|
||||
asChild={false}
|
||||
needsDelay={false}
|
||||
>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
setShowTestEmailModal(true)
|
||||
}}
|
||||
>
|
||||
<RiSendPlane2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={emailSenderTooltipContent}
|
||||
onClick={() => setShowTestEmailModal(true)}
|
||||
>
|
||||
<RiSendPlane2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>{emailSenderTooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={t('common.configure', { ns: 'workflow' })}
|
||||
asChild={false}
|
||||
needsDelay={false}
|
||||
>
|
||||
<ActionButton onClick={() => setShowEmailModal(true)}>
|
||||
<RiEqualizer2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={configureLabel}
|
||||
onClick={() => setShowEmailModal(true)}
|
||||
>
|
||||
<RiEqualizer2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>{configureLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
popupContent={t('operation.remove', { ns: 'common' })}
|
||||
asChild={false}
|
||||
needsDelay={false}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<ActionButton
|
||||
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onClick={() => onDelete(method.type)}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={removeLabel}
|
||||
state={isHovering ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
onClick={() => onDelete(method.type)}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>{removeLabel}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
@ -178,33 +182,29 @@ const DeliveryMethodItem: FC<DeliveryMethodItemProps> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showEmailModal && (
|
||||
<EmailConfigureModal
|
||||
isShow={showEmailModal}
|
||||
config={method.config as EmailConfig}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onClose={() => setShowEmailModal(false)}
|
||||
onConfirm={(data) => {
|
||||
handleConfigChange(data)
|
||||
setShowEmailModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showTestEmailModal && (
|
||||
<TestEmailSender
|
||||
nodeId={nodeId}
|
||||
deliveryId={method.id}
|
||||
isShow={showTestEmailModal}
|
||||
config={method.config as EmailConfig}
|
||||
formContent={formContent}
|
||||
formInputs={formInputs}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onClose={() => setShowTestEmailModal(false)}
|
||||
jumpToEmailConfigModal={jumpToEmailConfigModal}
|
||||
/>
|
||||
)}
|
||||
<EmailConfigureModal
|
||||
open={showEmailModal}
|
||||
config={method.config as EmailConfig}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onOpenChange={setShowEmailModal}
|
||||
onConfirm={(data) => {
|
||||
handleConfigChange(data)
|
||||
setShowEmailModal(false)
|
||||
}}
|
||||
/>
|
||||
<TestEmailSender
|
||||
nodeId={nodeId}
|
||||
deliveryId={method.id}
|
||||
open={showTestEmailModal}
|
||||
config={method.config as EmailConfig}
|
||||
formContent={formContent}
|
||||
formInputs={formInputs}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onOpenChange={setShowTestEmailModal}
|
||||
jumpToEmailConfigModal={jumpToEmailConfigModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,11 @@
|
||||
import type { FC } from 'react'
|
||||
import type { DeliveryMethod } from '../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDiscordFill,
|
||||
@ -9,17 +14,12 @@ import {
|
||||
RiMailSendFill,
|
||||
RiRobot2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { Slack, Teams } from '@/app/components/base/icons/src/public/other'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import useWorkflowNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { isTriggerWorkflow } from '@/app/components/workflow/utils/workflow-entry'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
@ -40,20 +40,10 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
onShowUpgradeTip,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, doSetOpen] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const humanInputEmailDeliveryEnabled = useProviderContextSelector(s => s.humanInputEmailDeliveryEnabled)
|
||||
const openRef = useRef(open)
|
||||
const nodes = useWorkflowNodes()
|
||||
|
||||
const setOpen = useCallback((v: boolean) => {
|
||||
doSetOpen(v)
|
||||
openRef.current = v
|
||||
}, [doSetOpen])
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
setOpen(!openRef.current)
|
||||
}, [setOpen])
|
||||
|
||||
const webAppDeliveryInfo = useMemo(() => {
|
||||
const isTriggerMode = isTriggerWorkflow(nodes)
|
||||
return {
|
||||
@ -71,23 +61,25 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
}, [data, humanInputEmailDeliveryEnabled])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-end"
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 12,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<div>
|
||||
<ActionButton className={cn(open && 'bg-state-base-hover')}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={t(`${i18nPrefix}.deliveryMethod.title`, { ns: 'workflow' })}
|
||||
className={cn(open && 'bg-state-base-hover')}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs">
|
||||
<div className="p-1">
|
||||
<div
|
||||
@ -215,8 +207,8 @@ const MethodSelector: FC<MethodSelectorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
export default memo(MethodSelector)
|
||||
|
||||
@ -3,16 +3,17 @@ import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowRightSFill, RiCloseLine } from '@remixicon/react'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { RiArrowRightSFill } from '@remixicon/react'
|
||||
import { noop, unionBy } from 'es-toolkit/compat'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
|
||||
import FormItem from '@/app/components/workflow/nodes/_base/components/before-run-form/form-item'
|
||||
import {
|
||||
@ -30,11 +31,11 @@ import EmailInput from './recipient/email-input'
|
||||
|
||||
const i18nPrefix = 'nodes.humanInput'
|
||||
|
||||
type EmailConfigureModalProps = {
|
||||
type EmailSenderModalProps = {
|
||||
nodeId: string
|
||||
deliveryId: string
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
jumpToEmailConfigModal: () => void
|
||||
config?: EmailConfig
|
||||
formContent?: string
|
||||
@ -48,18 +49,22 @@ const getOriginVar = (valueSelector: string[], list: NodeOutPutVar[]) => {
|
||||
if (!targetVar)
|
||||
return undefined
|
||||
|
||||
let curr: any = targetVar.vars
|
||||
let curr: Var[] | undefined = targetVar.vars
|
||||
for (let i = 1; i < valueSelector.length; i++) {
|
||||
const key = valueSelector[i]
|
||||
const isLast = i === valueSelector.length - 1
|
||||
const currentVar: Var | undefined = curr?.find(v => v.variable.replace('conversation.', '') === key)
|
||||
|
||||
if (Array.isArray(curr))
|
||||
curr = curr.find((v: any) => v.variable.replace('conversation.', '') === key)
|
||||
if (!currentVar)
|
||||
return undefined
|
||||
|
||||
if (isLast)
|
||||
return curr
|
||||
else if (curr?.type === VarType.object || curr?.type === VarType.file)
|
||||
curr = curr.children
|
||||
return currentVar
|
||||
|
||||
if ((currentVar.type === VarType.object || currentVar.type === VarType.file) && Array.isArray(currentVar.children))
|
||||
curr = currentVar.children
|
||||
else
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
@ -68,15 +73,15 @@ const getOriginVar = (valueSelector: string[], list: NodeOutPutVar[]) => {
|
||||
const EmailSenderModal = ({
|
||||
nodeId,
|
||||
deliveryId,
|
||||
isShow,
|
||||
onClose,
|
||||
open,
|
||||
onOpenChange,
|
||||
jumpToEmailConfigModal,
|
||||
config,
|
||||
formContent,
|
||||
formInputs,
|
||||
nodesOutputVars = [],
|
||||
availableNodes = [],
|
||||
}: EmailConfigureModalProps) => {
|
||||
}: EmailSenderModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, currentWorkspace } = useAppContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
@ -104,7 +109,7 @@ const EmailSenderModal = ({
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.type,
|
||||
nodeName: varInfo?.title || availableNodes[0]?.data.title!, // default start node title
|
||||
nodeName: varInfo?.title || availableNodes[0]?.data.title || '',
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1]!,
|
||||
isChatVar: isConversationVar(item),
|
||||
},
|
||||
@ -178,194 +183,194 @@ const EmailSenderModal = ({
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className="relative max-w-[480px]! p-0!"
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<div className="space-y-2 p-6 pb-3">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.done`, { ns: 'workflow' })}</div>
|
||||
{debugEnabled && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-md-semibold text-text-secondary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
<DialogContent>
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.done`, { ns: 'workflow' })}</DialogTitle>
|
||||
{debugEnabled && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-md-semibold text-text-secondary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlyWholeTeam && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlySpecificUsers && (
|
||||
<div className="system-md-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`, { ns: 'workflow' })}</div>
|
||||
)}
|
||||
{!debugEnabled && combinedRecipients && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && (
|
||||
<div className="mt-4">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6 flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-[72px]"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('operation.ok', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogCloseButton />
|
||||
<div className="space-y-1 pr-8">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.title`, { ns: 'workflow' })}</DialogTitle>
|
||||
{debugEnabled && (
|
||||
<>
|
||||
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`, { ns: 'workflow' })}</div>
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!debugEnabled && onlyWholeTeam && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`}
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
|
||||
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlySpecificUsers && (
|
||||
<div className="system-md-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`, { ns: 'workflow' })}</div>
|
||||
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`, { ns: 'workflow' })}</div>
|
||||
)}
|
||||
{!debugEnabled && combinedRecipients && (
|
||||
<div className="system-md-regular text-text-secondary">
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`}
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-md-medium text-text-secondary"></span> }}
|
||||
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && (
|
||||
<div className="px-5">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-[72px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.ok', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className="relative max-w-[480px]! p-0!"
|
||||
>
|
||||
<div className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="space-y-1 p-6 pb-3">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.deliveryMethod.emailSender.title`, { ns: 'workflow' })}</div>
|
||||
{debugEnabled && (
|
||||
<>
|
||||
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`, { ns: 'workflow' })}</div>
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<div className="mt-4">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.tip`}
|
||||
ns="workflow"
|
||||
components={{ email: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
components={{
|
||||
strong: <span onClick={jumpToEmailConfigModal} className="cursor-pointer system-xs-regular text-text-accent"></span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!debugEnabled && onlyWholeTeam && (
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlySpecificUsers && (
|
||||
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`, { ns: 'workflow' })}</div>
|
||||
)}
|
||||
{!debugEnabled && combinedRecipients && (
|
||||
<div className="system-sm-regular text-text-secondary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`}
|
||||
ns="workflow"
|
||||
components={{ team: <span className="system-sm-semibold text-text-primary"></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(onlySpecificUsers || combinedRecipients) && !debugEnabled && (
|
||||
<>
|
||||
<div className="px-5">
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-6 pt-1 system-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.tip`}
|
||||
ns="workflow"
|
||||
components={{
|
||||
strong: <span onClick={jumpToEmailConfigModal} className="cursor-pointer system-xs-regular text-text-accent"></span>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* vars */}
|
||||
{generatedInputs.length > 0 && (
|
||||
<>
|
||||
<div className="px-6">
|
||||
<Divider className="mt-4! mb-2! h-px! w-12! bg-divider-regular" />
|
||||
</div>
|
||||
<div className="px-6 py-2">
|
||||
<div className="group flex h-6 cursor-pointer items-center" onClick={() => setCollapsed(!collapsed)}>
|
||||
<div className="mr-1 system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.vars`, { ns: 'workflow' })}</div>
|
||||
<RiArrowRightSFill className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-primary', !collapsed && 'rotate-90')} />
|
||||
{/* vars */}
|
||||
{generatedInputs.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<Divider className="mt-4! mb-2! h-px! w-12! bg-divider-regular" />
|
||||
</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailSender.varsTip`, { ns: 'workflow' })}</div>
|
||||
{!collapsed && (
|
||||
<div className="mt-3 space-y-4">
|
||||
{generatedInputs.map((variable, index) => (
|
||||
<div
|
||||
key={variable.variable}
|
||||
className="mb-4 last-of-type:mb-0"
|
||||
>
|
||||
<FormItem
|
||||
autoFocus={index === 0}
|
||||
payload={variable}
|
||||
value={inputs[variable.variable]}
|
||||
onChange={v => handleValueChange(variable.variable, v)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="py-2">
|
||||
<div className="group flex h-6 cursor-pointer items-center" onClick={() => setCollapsed(!collapsed)}>
|
||||
<div className="mr-1 system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.deliveryMethod.emailSender.vars`, { ns: 'workflow' })}</div>
|
||||
<RiArrowRightSFill className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-primary', !collapsed && 'rotate-90')} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-row-reverse gap-2 p-6 pt-5">
|
||||
<Button
|
||||
disabled={sendingEmail || !confirmChecked}
|
||||
loading={sendingEmail}
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailSender.send`, { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.deliveryMethod.emailSender.varsTip`, { ns: 'workflow' })}</div>
|
||||
{!collapsed && (
|
||||
<div className="mt-3 space-y-4">
|
||||
{generatedInputs.map((variable, index) => (
|
||||
<div
|
||||
key={variable.variable}
|
||||
className="mb-4 last-of-type:mb-0"
|
||||
>
|
||||
<FormItem
|
||||
autoFocus={index === 0}
|
||||
payload={variable}
|
||||
value={inputs[variable.variable]}
|
||||
onChange={v => handleValueChange(variable.variable, v)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-6 flex flex-row-reverse gap-2">
|
||||
<Button
|
||||
disabled={sendingEmail || !confirmChecked}
|
||||
loading={sendingEmail}
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailSender.send`, { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,76 +1,60 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiMailSendFill,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { RiMailSendFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { UpgradeModal as BaseUpgradeModal } from '@/app/components/base/upgrade-modal'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
|
||||
type UpgradeModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const UpgradeModal: React.FC<UpgradeModalProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
}) => {
|
||||
export function UpgradeModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: UpgradeModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
|
||||
const handleUpgrade = () => {
|
||||
setShowPricingModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className="relative w-[580px]! max-w-[580px]! p-8!"
|
||||
>
|
||||
<div className="pb-6">
|
||||
<div
|
||||
className={cn(
|
||||
'mb-6 inline-flex rounded-xl border border-divider-regular bg-util-colors-blue-brand-blue-brand-500 p-2',
|
||||
)}
|
||||
>
|
||||
<RiMailSendFill className="h-6 w-6 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<p
|
||||
className="bg-[linear-gradient(271deg,var(--components-input-border-active-prompt-1,#155AEF)_-12.85%,var(--components-input-border-active-prompt-2,#0BA5EC)_95.4%)] bg-clip-text title-3xl-semi-bold text-transparent"
|
||||
>
|
||||
{t('nodes.humanInput.deliveryMethod.upgradeTip', { ns: 'workflow' })}
|
||||
</p>
|
||||
<p className="mt-2 system-md-regular text-text-tertiary">
|
||||
{t('nodes.humanInput.deliveryMethod.upgradeTipContent', { ns: 'workflow' })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end pt-5">
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('nodes.humanInput.deliveryMethod.upgradeTipHide', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<PremiumBadge
|
||||
size="custom"
|
||||
color="blue"
|
||||
allowHover={true}
|
||||
className="ml-3 h-8 w-[93px]"
|
||||
onClick={() => {
|
||||
setShowPricingModal()
|
||||
}}
|
||||
>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-sm-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
</Modal>
|
||||
<BaseUpgradeModal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
Icon={RiMailSendFill}
|
||||
title={t('nodes.humanInput.deliveryMethod.upgradeTip', { ns: 'workflow' })}
|
||||
description={t('nodes.humanInput.deliveryMethod.upgradeTipContent', { ns: 'workflow' })}
|
||||
classNames={{
|
||||
content: 'max-w-[580px]',
|
||||
}}
|
||||
footer={(
|
||||
<>
|
||||
<Button
|
||||
className="w-[72px]"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{t('nodes.humanInput.deliveryMethod.upgradeTipHide', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<PremiumBadge
|
||||
size="custom"
|
||||
color="blue"
|
||||
allowHover={true}
|
||||
className="h-8 w-[93px]"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-sm-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeModal
|
||||
|
||||
@ -18,7 +18,7 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
@ -108,9 +108,9 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
|
||||
<div className="mb-1 flex shrink-0 items-center justify-between">
|
||||
<div className="flex h-6 items-center gap-0.5">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.formContent.title`, { ns: 'workflow' })}</div>
|
||||
<Tooltip
|
||||
popupContent={t(`${i18nPrefix}.formContent.tooltip`, { ns: 'workflow' })}
|
||||
/>
|
||||
<Infotip aria-label={t(`${i18nPrefix}.formContent.tooltip`, { ns: 'workflow' })}>
|
||||
{t(`${i18nPrefix}.formContent.tooltip`, { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex items-center">
|
||||
@ -164,9 +164,9 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="system-sm-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}.userActions.title`, { ns: 'workflow' })}</div>
|
||||
<Tooltip
|
||||
popupContent={t(`${i18nPrefix}.userActions.tooltip`, { ns: 'workflow' })}
|
||||
/>
|
||||
<Infotip aria-label={t(`${i18nPrefix}.userActions.tooltip`, { ns: 'workflow' })}>
|
||||
{t(`${i18nPrefix}.userActions.tooltip`, { ns: 'workflow' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="flex items-center px-1">
|
||||
|
||||
@ -169,13 +169,13 @@ export const ModalContextProvider = ({
|
||||
showModelModal.onCancelCallback()
|
||||
}, [showModelModal])
|
||||
|
||||
const handleSaveModelModal = useCallback((formValues?: Record<string, any>) => {
|
||||
const handleSaveModelModal = useCallback((formValues?: Record<string, unknown>) => {
|
||||
if (showModelModal?.onSaveCallback)
|
||||
showModelModal.onSaveCallback(showModelModal.payload, formValues)
|
||||
setShowModelModal(null)
|
||||
}, [showModelModal])
|
||||
|
||||
const handleRemoveModelModal = useCallback((formValues?: Record<string, any>) => {
|
||||
const handleRemoveModelModal = useCallback((formValues?: Record<string, unknown>) => {
|
||||
if (showModelModal?.onRemoveCallback)
|
||||
showModelModal.onRemoveCallback(showModelModal.payload, formValues)
|
||||
setShowModelModal(null)
|
||||
@ -369,7 +369,7 @@ export const ModalContextProvider = ({
|
||||
}}
|
||||
onSave={() => {
|
||||
setShowUpdatePluginModal(null)
|
||||
showUpdatePluginModal.onSaveCallback?.({} as any)
|
||||
showUpdatePluginModal.onSaveCallback?.()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
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'
|
||||
@ -27,21 +28,6 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => mockUseAppContext(),
|
||||
}))
|
||||
|
||||
let latestTriggerEventsModalProps: any = null
|
||||
const triggerEventsLimitModalMock = vi.fn((props: any) => {
|
||||
latestTriggerEventsModalProps = props
|
||||
return (
|
||||
<div data-testid="trigger-limit-modal">
|
||||
<button type="button" onClick={props.onClose}>dismiss</button>
|
||||
<button type="button" onClick={props.onUpgrade}>upgrade</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
|
||||
default: (props: any) => triggerEventsLimitModalMock(props),
|
||||
}))
|
||||
|
||||
type DefaultPlanShape = typeof defaultPlan
|
||||
type ResetShape = {
|
||||
apiRateLimit: number | null
|
||||
@ -79,8 +65,6 @@ const renderProvider = () => renderWithNuqs(
|
||||
|
||||
describe('ModalContextProvider trigger events limit modal', () => {
|
||||
beforeEach(() => {
|
||||
latestTriggerEventsModalProps = null
|
||||
triggerEventsLimitModalMock.mockClear()
|
||||
mockUseAppContext.mockReset()
|
||||
mockUseProviderContext.mockReset()
|
||||
window.localStorage.clear()
|
||||
@ -109,25 +93,20 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
// Note: vitest.setup.ts replaces localStorage with a mock object that has vi.fn() methods
|
||||
// We need to spy on the mock's setItem, not Storage.prototype.setItem
|
||||
const setItemSpy = vi.spyOn(localStorage, 'setItem')
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
||||
expect(latestTriggerEventsModalProps).toMatchObject({
|
||||
usage: 3000,
|
||||
total: 3000,
|
||||
resetInDays: 5,
|
||||
})
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
expect(screen.getAllByText('3000')).toHaveLength(2)
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onClose()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
await waitFor(() => {
|
||||
expect(setItemSpy.mock.calls.length).toBeGreaterThan(0)
|
||||
})
|
||||
const [key, value] = (setItemSpy.mock.calls[0] ?? []) as [any, any]
|
||||
const [key, value] = (setItemSpy.mock.calls[0] ?? []) as [string, string]
|
||||
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
||||
expect(value).toBe('1')
|
||||
})
|
||||
@ -147,18 +126,16 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
throw new Error('Storage disabled')
|
||||
})
|
||||
const setItemSpy = vi.spyOn(localStorage, 'setItem')
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onClose()
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
expect(setItemSpy).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
|
||||
@ -175,16 +152,37 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
|
||||
throw new Error('Quota exceeded')
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal'))!.toBeInTheDocument())
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onClose()
|
||||
await user.click(screen.getByRole('button', { name: 'billing.triggerLimitModal.dismiss' }))
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('closes the trigger events limit modal and opens pricing when upgrading', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 400 },
|
||||
total: { triggerEvents: 400 },
|
||||
reset: { triggerEvents: 6 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
|
||||
|
||||
await user.click(screen.getByText('billing.triggerLimitModal.upgrade'))
|
||||
|
||||
await waitFor(() => expect(screen.getByText('billing.plansCommon.mostPopular')).toBeInTheDocument())
|
||||
expect(screen.queryByText('400')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,8 +28,8 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
|
||||
export type ModalState<T> = {
|
||||
payload: T
|
||||
onCancelCallback?: () => void
|
||||
onSaveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
|
||||
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
|
||||
onSaveCallback?: (newPayload?: T, formValues?: Record<string, unknown>) => void
|
||||
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, unknown>) => void
|
||||
onEditCallback?: (newPayload: T) => void
|
||||
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
|
||||
isEditMode?: boolean
|
||||
|
||||
Loading…
Reference in New Issue
Block a user