mirror of https://github.com/langgenius/dify.git
feat: enforce sandbox start-node limit by disabling publish and surfacing an upgrade CTA with localized copy
This commit is contained in:
parent
9e763e80e8
commit
1730572498
|
|
@ -49,6 +49,7 @@ import { fetchInstalledAppList } from '@/service/explore'
|
|||
import { AppModeEnum } from '@/types/app'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
|
|
@ -106,6 +107,7 @@ export type AppPublisherProps = {
|
|||
workflowToolAvailable?: boolean
|
||||
missingStartNode?: boolean
|
||||
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
|
||||
startNodeLimitExceeded?: boolean
|
||||
}
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
|
|
@ -127,6 +129,7 @@ const AppPublisher = ({
|
|||
workflowToolAvailable = true,
|
||||
missingStartNode = false,
|
||||
hasTriggerNode = false,
|
||||
startNodeLimitExceeded = false,
|
||||
}: AppPublisherProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
@ -246,6 +249,13 @@ const AppPublisher = ({
|
|||
const hasPublishedVersion = !!publishedAt
|
||||
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
|
||||
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
|
||||
const showStartNodeLimitHint = Boolean(startNodeLimitExceeded)
|
||||
const upgradeHighlightStyle = useMemo(() => ({
|
||||
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -304,29 +314,49 @@ const AppPublisher = ({
|
|||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-3 w-full'
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: (
|
||||
<div className='flex gap-1'>
|
||||
<span>{t('workflow.common.publishUpdate')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
<>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-3 w-full'
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: (
|
||||
<div className='flex gap-1'>
|
||||
<span>{t('workflow.common.publishUpdate')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
{showStartNodeLimitHint && (
|
||||
<div className='mt-3 flex flex-col items-stretch'>
|
||||
<p
|
||||
className='text-sm font-semibold leading-5 text-transparent'
|
||||
style={upgradeHighlightStyle}
|
||||
>
|
||||
<span className='block'>{t('workflow.publishLimit.startNodeTitlePrefix')}</span>
|
||||
<span className='block'>{t('workflow.publishLimit.startNodeTitleSuffix')}</span>
|
||||
</p>
|
||||
<p className='mt-1 text-xs leading-4 text-text-secondary'>
|
||||
{t('workflow.publishLimit.startNodeDesc')}
|
||||
</p>
|
||||
<UpgradeBtn
|
||||
isShort
|
||||
className='mb-[12px] mt-[9px] h-[32px] w-[93px] self-start'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '../../base/premium-badge'
|
||||
|
|
@ -9,6 +9,7 @@ import { useModalContext } from '@/context/modal-context'
|
|||
|
||||
type Props = {
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
isFull?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
isPlain?: boolean
|
||||
|
|
@ -18,6 +19,8 @@ type Props = {
|
|||
}
|
||||
|
||||
const UpgradeBtn: FC<Props> = ({
|
||||
className,
|
||||
style,
|
||||
isPlain = false,
|
||||
isShort = false,
|
||||
onClick: _onClick,
|
||||
|
|
@ -42,7 +45,11 @@ const UpgradeBtn: FC<Props> = ({
|
|||
|
||||
if (isPlain) {
|
||||
return (
|
||||
<Button onClick={onClick}>
|
||||
<Button
|
||||
className={className}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{t('billing.upgradeBtn.plain')}
|
||||
</Button>
|
||||
)
|
||||
|
|
@ -54,6 +61,8 @@ const UpgradeBtn: FC<Props> = ({
|
|||
color='blue'
|
||||
allowHover={true}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<SparklesSoft className='flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0' />
|
||||
<div className='system-xs-medium'>
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import useTheme from '@/hooks/use-theme'
|
|||
import cn from '@/utils/classnames'
|
||||
import { useIsChatMode } from '@/app/components/workflow/hooks'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
|
||||
const FeaturesTrigger = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -50,6 +52,7 @@ const FeaturesTrigger = () => {
|
|||
const appID = appDetail?.id
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
|
|
@ -95,6 +98,15 @@ const FeaturesTrigger = () => {
|
|||
const hasTriggerNode = useMemo(() => (
|
||||
nodes.some(node => isTriggerNode(node.data.type as BlockEnum))
|
||||
), [nodes])
|
||||
const startNodeLimitExceeded = useMemo(() => {
|
||||
const entryCount = nodes.reduce((count, node) => {
|
||||
const nodeType = node.data.type as BlockEnum
|
||||
if (nodeType === BlockEnum.Start || isTriggerNode(nodeType))
|
||||
return count + 1
|
||||
return count
|
||||
}, 0)
|
||||
return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2
|
||||
}, [nodes, plan.type, isFetchedPlan])
|
||||
|
||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
|
|
@ -196,7 +208,8 @@ const FeaturesTrigger = () => {
|
|||
crossAxisOffset: 4,
|
||||
missingStartNode: !startNode,
|
||||
hasTriggerNode,
|
||||
publishDisabled: !hasWorkflowNodes,
|
||||
startNodeLimitExceeded,
|
||||
publishDisabled: !hasWorkflowNodes || startNodeLimitExceeded,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ const translation = {
|
|||
noHistory: 'No History',
|
||||
tagBound: 'Number of apps using this tag',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: 'Upgrade to',
|
||||
startNodeTitleSuffix: 'unlock unlimited start nodes',
|
||||
startNodeDesc: 'You’ve reached the limit of 2 start nodes for your current plan. Upgrade to publish this workflow.',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Environment Variables',
|
||||
envDescription: 'Environment variables can be used to store private information and credentials. They are read-only and can be separated from the DSL file during export.',
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ const translation = {
|
|||
tagBound: 'このタグを使用しているアプリの数',
|
||||
moreActions: 'さらにアクション',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: 'アップグレードして',
|
||||
startNodeTitleSuffix: '開始ノードの上限を解除',
|
||||
startNodeDesc: '現在のプランでは開始ノードは2個までです。公開するにはプランをアップグレードしてください。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境変数',
|
||||
envDescription: '環境変数は、個人情報や認証情報を格納するために使用することができます。これらは読み取り専用であり、DSL ファイルからエクスポートする際には分離されます。',
|
||||
|
|
|
|||
|
|
@ -122,6 +122,11 @@ const translation = {
|
|||
noHistory: '没有历史版本',
|
||||
tagBound: '使用此标签的应用数量',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: '升级以',
|
||||
startNodeTitleSuffix: '解锁无限开始节点',
|
||||
startNodeDesc: '当前套餐最多支持 2 个开始节点。升级套餐即可发布此工作流。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '环境变量',
|
||||
envDescription: '环境变量是一种存储敏感信息的方法,如 API 密钥、数据库密码等。它们被存储在工作流程中,而不是代码中,以便在不同环境中共享。',
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ const translation = {
|
|||
currentWorkflow: '當前工作流程',
|
||||
moreActions: '更多動作',
|
||||
},
|
||||
publishLimit: {
|
||||
startNodeTitlePrefix: '升級以',
|
||||
startNodeTitleSuffix: '解鎖無限開始節點',
|
||||
startNodeDesc: '目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境變數',
|
||||
envDescription: '環境變數可用於存儲私人信息和憑證。它們是唯讀的,並且可以在導出時與 DSL 文件分開。',
|
||||
|
|
|
|||
Loading…
Reference in New Issue