Merge remote-tracking branch 'origin/main' into feat/trigger-saas

This commit is contained in:
lyzno1
2025-11-17 16:27:50 +08:00
6 changed files with 139 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/app-card' import AppCard from '@/app/components/app/overview/app-card'
@@ -24,6 +24,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppWorkflow } from '@/service/use-workflow' import { useAppWorkflow } from '@/service/use-workflow'
import type { BlockEnum } from '@/app/components/workflow/types' import type { BlockEnum } from '@/app/components/workflow/types'
import { isTriggerNode } from '@/app/components/workflow/types' import { isTriggerNode } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
export type ICardViewProps = { export type ICardViewProps = {
appId: string appId: string
@@ -33,6 +34,7 @@ export type ICardViewProps = {
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => { const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const { t } = useTranslation() const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail) const setAppDetail = useAppStore(state => state.setAppDetail)
@@ -53,6 +55,35 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
}) })
}, [isWorkflowApp, currentWorkflow]) }, [isWorkflowApp, currentWorkflow])
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
const disableAppCards = !shouldRenderAppCards
const triggerDocUrl = docLink('/guides/workflow/node/start')
const buildTriggerModeMessage = useCallback((featureName: string) => (
<div className='flex flex-col gap-1'>
<div className='text-xs text-text-secondary'>
{t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })}
</div>
<div
className='cursor-pointer text-xs font-medium text-text-accent hover:underline'
onClick={(event) => {
event.stopPropagation()
window.open(triggerDocUrl, '_blank')
}}
>
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</div>
), [t, triggerDocUrl])
const disableWebAppTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.appInfo.title'))
: null
const disableApiTooltip = disableAppCards
? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title'))
: null
const disableMcpTooltip = disableAppCards
? buildTriggerModeMessage(t('tools.mcp.server.title'))
: null
const updateAppDetail = async () => { const updateAppDetail = async () => {
try { try {
@@ -124,39 +155,48 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
if (!appDetail) if (!appDetail)
return <Loading /> return <Loading />
return ( const appCards = (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}> <>
{ <AppCard
shouldRenderAppCards && ( appInfo={appDetail}
<> cardType="webapp"
<AppCard isInPanel={isInPanel}
appInfo={appDetail} triggerModeDisabled={disableAppCards}
cardType="webapp" triggerModeMessage={disableWebAppTooltip}
isInPanel={isInPanel} onChangeStatus={onChangeSiteStatus}
onChangeStatus={onChangeSiteStatus} onGenerateCode={onGenerateCode}
onGenerateCode={onGenerateCode} onSaveSiteConfig={onSaveSiteConfig}
onSaveSiteConfig={onSaveSiteConfig} />
/> <AppCard
<AppCard cardType="api"
cardType="api" appInfo={appDetail}
appInfo={appDetail} isInPanel={isInPanel}
isInPanel={isInPanel} triggerModeDisabled={disableAppCards}
onChangeStatus={onChangeApiStatus} triggerModeMessage={disableApiTooltip}
/> onChangeStatus={onChangeApiStatus}
{showMCPCard && ( />
<MCPServiceCard {showMCPCard && (
appInfo={appDetail} <MCPServiceCard
/>
)}
</>
)
}
{showTriggerCard && (
<TriggerCard
appInfo={appDetail} appInfo={appDetail}
onToggleResult={handleCallbackResult} triggerModeDisabled={disableAppCards}
triggerModeMessage={disableMcpTooltip}
/> />
)} )}
</>
)
const triggerCardNode = showTriggerCard ? (
<TriggerCard
appInfo={appDetail}
onToggleResult={handleCallbackResult}
/>
) : null
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
{disableAppCards && triggerCardNode}
{appCards}
{!disableAppCards && triggerCardNode}
</div> </div>
) )
} }

View File

@@ -51,6 +51,8 @@ export type IAppCardProps = {
isInPanel?: boolean isInPanel?: boolean
cardType?: 'api' | 'webapp' cardType?: 'api' | 'webapp'
customBgColor?: string customBgColor?: string
triggerModeDisabled?: boolean // true when Trigger Node mode needs UI locked to avoid conflicting actions
triggerModeMessage?: React.ReactNode // contextual copy explaining why the card is disabled in trigger mode
onChangeStatus: (val: boolean) => Promise<void> onChangeStatus: (val: boolean) => Promise<void>
onSaveSiteConfig?: (params: ConfigParams) => Promise<void> onSaveSiteConfig?: (params: ConfigParams) => Promise<void>
onGenerateCode?: () => Promise<void> onGenerateCode?: () => Promise<void>
@@ -61,6 +63,8 @@ function AppCard({
isInPanel, isInPanel,
cardType = 'webapp', cardType = 'webapp',
customBgColor, customBgColor,
triggerModeDisabled = false,
triggerModeMessage = '',
onChangeStatus, onChangeStatus,
onSaveSiteConfig, onSaveSiteConfig,
onGenerateCode, onGenerateCode,
@@ -111,7 +115,7 @@ function AppCard({
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start) const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
const missingStartNode = isWorkflowApp && !hasStartNode const missingStartNode = isWorkflowApp && !hasStartNode
const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api) const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api)
const isMinimalState = appUnpublished || missingStartNode const isMinimalState = appUnpublished || missingStartNode
const { app_base_url, access_token } = appInfo.site ?? {} const { app_base_url, access_token } = appInfo.site ?? {}
@@ -189,7 +193,20 @@ function AppCard({
className={ className={
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`} `${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`}
> >
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}> <div className={`${customBgColor ?? 'bg-background-default'} relative rounded-xl ${triggerModeDisabled ? 'opacity-60' : ''}`}>
{triggerModeDisabled && (
triggerModeMessage
? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
</Tooltip>
)
: <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
)}
<div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}> <div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}>
<div className='flex w-full items-center gap-3 self-stretch'> <div className='flex w-full items-center gap-3 self-stretch'>
<AppBasic <AppBasic
@@ -214,18 +231,23 @@ function AppCard({
</div> </div>
<Tooltip <Tooltip
popupContent={ popupContent={
toggleDisabled && (appUnpublished || missingStartNode) ? ( toggleDisabled ? (
<> triggerModeDisabled && triggerModeMessage
<div className="mb-1 text-xs font-normal text-text-secondary"> ? triggerModeMessage
{t('appOverview.overview.appInfo.enableTooltip.description')} : (appUnpublished || missingStartNode) ? (
</div> <>
<div <div className="mb-1 text-xs font-normal text-text-secondary">
className="cursor-pointer text-xs font-normal text-text-accent hover:underline" {t('appOverview.overview.appInfo.enableTooltip.description')}
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')} </div>
> <div
{t('appOverview.overview.appInfo.enableTooltip.learnMore')} className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
</div> onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
</> >
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div>
</>
)
: ''
) : '' ) : ''
} }
position="right" position="right"
@@ -329,9 +351,11 @@ function AppCard({
{!isApp && <SecretKeyButton appId={appInfo.id} />} {!isApp && <SecretKeyButton appId={appInfo.id} />}
{OPERATIONS_MAP[cardType].map((op) => { {OPERATIONS_MAP[cardType].map((op) => {
const disabled const disabled
= op.opName === t('appOverview.overview.appInfo.settings.entry') = triggerModeDisabled
? false ? true
: !runningStatus : op.opName === t('appOverview.overview.appInfo.settings.entry')
? false
: !runningStatus
return ( return (
<Button <Button
className="mr-1 min-w-[88px]" className="mr-1 min-w-[88px]"

View File

@@ -30,10 +30,14 @@ import { useDocLink } from '@/context/i18n'
export type IAppCardProps = { export type IAppCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO> appInfo: AppDetailResponse & Partial<AppSSO>
triggerModeDisabled?: boolean // align with Trigger Node vs User Input exclusivity
triggerModeMessage?: React.ReactNode // display-only message explaining the trigger restriction
} }
function MCPServiceCard({ function MCPServiceCard({
appInfo, appInfo,
triggerModeDisabled = false,
triggerModeMessage = '',
}: IAppCardProps) { }: IAppCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
const docLink = useDocLink() const docLink = useDocLink()
@@ -79,7 +83,7 @@ function MCPServiceCard({
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start) const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
const missingStartNode = isWorkflowApp && !hasStartNode const missingStartNode = isWorkflowApp && !hasStartNode
const hasInsufficientPermissions = !isCurrentWorkspaceEditor const hasInsufficientPermissions = !isCurrentWorkspaceEditor
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled
const isMinimalState = appUnpublished || missingStartNode const isMinimalState = appUnpublished || missingStartNode
const [activated, setActivated] = useState(serverActivated) const [activated, setActivated] = useState(serverActivated)
@@ -144,7 +148,18 @@ function MCPServiceCard({
return ( return (
<> <>
<div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight', isMinimalState && 'h-12')}> <div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight', isMinimalState && 'h-12')}>
<div className='rounded-xl bg-background-default'> <div className={cn('relative rounded-xl bg-background-default', triggerModeDisabled && 'opacity-60')}>
{triggerModeDisabled && (
triggerModeMessage ? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
</Tooltip>
) : <div className='absolute inset-0 z-10 cursor-not-allowed rounded-xl' aria-hidden="true"></div>
)}
<div className={cn('flex w-full flex-col items-start justify-center gap-3 self-stretch p-3', isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle')}> <div className={cn('flex w-full flex-col items-start justify-center gap-3 self-stretch p-3', isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle')}>
<div className='flex w-full items-center gap-3 self-stretch'> <div className='flex w-full items-center gap-3 self-stretch'>
<div className='flex grow items-center'> <div className='flex grow items-center'>
@@ -182,7 +197,7 @@ function MCPServiceCard({
{t('appOverview.overview.appInfo.enableTooltip.learnMore')} {t('appOverview.overview.appInfo.enableTooltip.learnMore')}
</div> </div>
</> </>
) : '' ) : triggerModeMessage || ''
) : '' ) : ''
} }
position="right" position="right"

View File

@@ -138,6 +138,9 @@ const translation = {
running: 'In Service', running: 'In Service',
disable: 'Disabled', disable: 'Disabled',
}, },
disableTooltip: {
triggerMode: 'The {{feature}} feature is not supported in Trigger Node mode.',
},
}, },
analysis: { analysis: {
title: 'Analysis', title: 'Analysis',

View File

@@ -138,6 +138,9 @@ const translation = {
running: '稼働中', running: '稼働中',
disable: '無効', disable: '無効',
}, },
disableTooltip: {
triggerMode: 'トリガーノードモードでは{{feature}}機能を使用できません。',
},
}, },
analysis: { analysis: {
title: '分析', title: '分析',

View File

@@ -138,6 +138,9 @@ const translation = {
running: '运行中', running: '运行中',
disable: '已停用', disable: '已停用',
}, },
disableTooltip: {
triggerMode: '触发节点模式下不支持{{feature}}功能。',
},
}, },
analysis: { analysis: {
title: '分析', title: '分析',