fix: oauth subscription

This commit is contained in:
yessenia 2025-09-26 16:27:39 +08:00
parent 1a9798c559
commit 6b94d30a5f
8 changed files with 89 additions and 893 deletions

View File

@ -58,6 +58,7 @@ const CustomSelect = <T extends Option>({
onOpenChange,
placement,
offset,
triggerPopupSameWidth = true,
} = containerProps || {}
const {
className: triggerClassName,
@ -85,6 +86,7 @@ const CustomSelect = <T extends Option>({
offset={offset || 4}
open={mergedOpen}
onOpenChange={handleOpenChange}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => handleOpenChange(!mergedOpen)}

View File

@ -1,311 +0,0 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
RiArrowRightLine,
RiCloseLine,
} from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import Form from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { usePluginStore } from '../../store'
type Props = {
onClose: () => void
onSuccess: () => void
}
enum ApiKeyStep {
Verify = 'verify',
Configuration = 'configuration',
}
export const ApiKeyCreateModal = ({ onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
// State
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(ApiKeyStep.Verify)
const [subscriptionName, setSubscriptionName] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<any>(null)
const [verificationError, setVerificationError] = useState<string>('')
// Form refs
const credentialsFormRef = React.useRef<FormRefObject>(null)
const parametersFormRef = React.useRef<FormRefObject>(null)
// API mutations
const { mutate: createBuilder, isPending: isCreatingBuilder } = useCreateTriggerSubscriptionBuilder()
const { mutate: verifyBuilder, isPending: isVerifying } = useVerifyTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
// Get provider name and schemas
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const credentialsSchema = detail?.declaration.trigger?.credentials_schema || []
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
const handleVerify = () => {
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
const credentials = credentialsFormValues.values
if (!Object.keys(credentials).length) {
Toast.notify({
type: 'error',
message: 'Please fill in all required credentials',
})
return
}
setVerificationError('')
// First create builder
createBuilder(
{
provider: providerName,
credential_type: TriggerCredentialTypeEnum.ApiKey,
},
{
onSuccess: (response) => {
const builder = response.subscription_builder
setSubscriptionBuilder(builder)
// setCurrentStep('configuration')
verifyBuilder(
{
provider: providerName,
subscriptionBuilderId: builder.id,
credentials,
},
{
onSuccess: () => {
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.apiKey.verify.success'),
})
setCurrentStep(ApiKeyStep.Configuration)
},
onError: (error: any) => {
setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error'))
},
},
)
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.verifyFailed'),
})
},
},
)
}
const handleCreate = () => {
if (!subscriptionName.trim()) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.subscriptionName.required'),
})
return
}
if (!subscriptionBuilder)
return
buildSubscription(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
},
{
onSuccess: () => {
Toast.notify({
type: 'success',
message: 'Subscription created successfully',
})
onSuccess()
onClose()
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('modal.errors.createFailed'),
})
},
},
)
}
const handleBack = () => {
setCurrentStep(ApiKeyStep.Verify)
}
return (
<Modal
isShow
onClose={onClose}
className='!max-w-[520px] !p-0'
wrapperClassName='!z-[1002]'
>
<div className='flex items-center justify-between border-b border-divider-subtle p-6 pb-4'>
<div className='flex items-center gap-3'>
{currentStep === ApiKeyStep.Configuration && (
<Button variant='ghost' size='small' onClick={handleBack}>
<RiArrowLeftLine className='h-4 w-4' />
</Button>
)}
<h3 className='text-lg font-semibold text-text-primary'>
{t('pluginTrigger.modal.apiKey.title')}
</h3>
</div>
<Button variant='ghost' size='small' onClick={onClose}>
<RiCloseLine className='h-4 w-4' />
</Button>
</div>
{/* Step indicator */}
<div className='border-b border-divider-subtle px-6 py-4'>
<div className='flex items-center gap-4'>
<div className={`flex items-center gap-2 ${currentStep === ApiKeyStep.Verify ? 'text-text-accent' : currentStep === ApiKeyStep.Configuration ? 'text-text-success' : 'text-text-tertiary'}`}>
<div className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${currentStep === ApiKeyStep.Verify
? 'bg-util-accent-light-blue text-util-accent-blue'
: currentStep === ApiKeyStep.Configuration
? 'bg-state-success-bg text-state-success-text'
: 'bg-background-default-subtle text-text-tertiary'}`}>
1
</div>
<span className='system-sm-medium'>{t('pluginTrigger.modal.steps.verify')}</span>
</div>
<div className='h-px flex-1 bg-divider-subtle'></div>
<div className={`flex items-center gap-2 ${currentStep === ApiKeyStep.Configuration ? 'text-text-accent' : 'text-text-tertiary'}`}>
<div className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${currentStep === ApiKeyStep.Configuration
? 'bg-util-accent-light-blue text-util-accent-blue'
: 'bg-background-default-subtle text-text-tertiary'}`}>
2
</div>
<span className='system-sm-medium'>{t('pluginTrigger.modal.steps.configuration')}</span>
</div>
</div>
</div>
<div className='p-6'>
{currentStep === ApiKeyStep.Verify ? (
// Step 1: Verify Credentials
<div>
{credentialsSchema.length > 0 && (
<div className='mb-4'>
<Form
formSchemas={credentialsSchema}
ref={credentialsFormRef}
/>
</div>
)}
{verificationError && (
<div className='bg-state-destructive-bg mb-4 rounded-lg border border-state-destructive-border p-3'>
<div className='text-state-destructive-text system-xs-medium'>
{verificationError}
</div>
</div>
)}
</div>
) : (
// Step 2: Configuration
<div>
{/* <div className='mb-4'>
<h4 className='system-sm-semibold mb-2 text-text-primary'>
{t('pluginTrigger.modal.apiKey.configuration.title')}
</h4>
<p className='system-xs-regular text-text-secondary'>
{t('pluginTrigger.modal.apiKey.configuration.description')}
</p>
</div> */}
{/* Subscription Name */}
<div className='mb-4'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.subscriptionName.label')}
</label>
<Input
value={subscriptionName}
onChange={e => setSubscriptionName(e.target.value)}
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
/>
</div>
{/* Callback URL (read-only) */}
{subscriptionBuilder?.endpoint && (
<div className='mb-4'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.callbackUrl.label')}
</label>
<Input
value={subscriptionBuilder.endpoint}
readOnly
className='bg-background-section'
/>
<div className='system-xs-regular mt-1 text-text-tertiary'>
{t('pluginTrigger.modal.form.callbackUrl.description')}
</div>
</div>
)}
{/* Dynamic Parameters Form */}
{parametersSchema.length > 0 && (
<div className='mb-4'>
<div className='system-sm-medium mb-3 text-text-primary'>
Subscription Parameters
</div>
<Form
formSchemas={parametersSchema}
ref={parametersFormRef}
/>
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
<Button variant='secondary' onClick={onClose}>
{t('pluginTrigger.modal.common.cancel')}
</Button>
{currentStep === ApiKeyStep.Verify ? (
<Button
variant='primary'
onClick={handleVerify}
loading={isCreatingBuilder || isVerifying}
// disabled={credentialsSchema.length > 0}
>
{isVerifying ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')}
<RiArrowRightLine className='ml-2 h-4 w-4' />
</Button>
) : (
<Button
variant='primary'
onClick={handleCreate}
loading={isBuilding}
disabled={!subscriptionName.trim()}
>
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
</Button>
)}
</div>
</Modal>
)
}

View File

@ -18,12 +18,13 @@ import {
import { RiLoader2Line } from '@remixicon/react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginStore } from '../../store'
import { usePluginStore, usePluginSubscriptionStore } from '../../store'
import LogViewer from '../log-viewer'
type Props = {
onClose: () => void
createType: SupportedCreationMethods
builder?: TriggerSubscriptionBuilder
}
const CREDENTIAL_TYPE_MAP: Record<SupportedCreationMethods, TriggerCredentialTypeEnum> = {
@ -58,14 +59,15 @@ const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
</div>
}
export const CommonCreateModal = ({ onClose, createType }: Props) => {
export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const { refresh } = usePluginSubscriptionStore()
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
const [subscriptionName, setSubscriptionName] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
const [verificationError, setVerificationError] = useState<string>('')
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
@ -184,8 +186,8 @@ export const CommonCreateModal = ({ onClose, createType }: Props) => {
type: 'success',
message: 'Subscription created successfully',
})
// onSuccess()
onClose()
refresh?.()
},
onError: (error: any) => {
Toast.notify({

View File

@ -1,13 +1,15 @@
import { ActionButton } from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import { Button } from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { PortalSelect } from '@/app/components/base/select'
import type { Option } from '@/app/components/base/select/custom'
import CustomSelect from '@/app/components/base/select/custom'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { openOAuthPopup } from '@/hooks/use-oauth'
import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers'
import cn from '@/utils/classnames'
import { RiAddLine, RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { RiAddLine, RiEqualizer2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -16,30 +18,6 @@ import { usePluginStore } from '../../store'
import { CommonCreateModal } from './common-modal'
import { OAuthClientSettingsModal } from './oauth-client'
export const CreateModal = () => {
const { t } = useTranslation()
return (
<Modal
isShow
// onClose={onClose}
className='!max-w-[520px] p-6'
wrapperClassName='!z-[1002]'
>
<div className='flex items-center justify-between pb-3'>
<h3 className='text-lg font-semibold text-text-primary'>
{t('pluginTrigger.modal.oauth.title')}
</h3>
<ActionButton
// onClick={onClose}
>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
</Modal>
)
}
export enum CreateButtonType {
FULL_BUTTON = 'full-button',
ICON_BUTTON = 'icon-button',
@ -67,7 +45,7 @@ export const DEFAULT_METHOD = 'default'
*/
export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BUTTON }: Props) => {
const { t } = useTranslation()
const [selectedCreateType, setSelectedCreateType] = useState<SupportedCreationMethods | null>(null)
const [selectedCreateInfo, setSelectedCreateInfo] = useState<{ type: SupportedCreationMethods, builder?: TriggerSubscriptionBuilder } | null>(null)
const detail = usePluginStore(state => state.detail)
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
@ -99,25 +77,32 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
showClientSettingsModal()
}
const allOptions = [
{
value: SupportedCreationMethods.OAUTH,
name: t('pluginTrigger.subscription.addType.options.oauth.title'),
extra: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
},
{
value: SupportedCreationMethods.APIKEY,
name: t('pluginTrigger.subscription.addType.options.apiKey.title'),
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
},
{
value: SupportedCreationMethods.MANUAL,
name: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
tooltip: <Tooltip popupContent={t('pluginTrigger.subscription.addType.options.manual.tip')} />,
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
},
]
const allOptions = useMemo(() => {
const showCustomBadge = oauthConfig?.custom_enabled && oauthConfig?.custom_configured
return [
{
value: SupportedCreationMethods.OAUTH,
label: t('pluginTrigger.subscription.addType.options.oauth.title'),
tag: !showCustomBadge ? null : <Badge className='ml-1 mr-0.5'>
{t('plugin.auth.custom')}
</Badge>,
extra: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
},
{
value: SupportedCreationMethods.APIKEY,
label: t('pluginTrigger.subscription.addType.options.apiKey.title'),
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
},
{
value: SupportedCreationMethods.MANUAL,
label: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
extra: <Tooltip popupContent={t('pluginTrigger.subscription.addType.options.manual.tip')} />,
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
},
]
}, [t, oauthConfig, supportedMethods, methodType])
const onChooseCreateType = (type: SupportedCreationMethods) => {
if (type === SupportedCreationMethods.OAUTH) {
@ -130,7 +115,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
type: 'success',
message: t('pluginTrigger.modal.oauth.authorized'),
})
setSelectedCreateType(SupportedCreationMethods.OAUTH)
setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder: response.subscription_builder })
}
})
},
@ -147,7 +132,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
}
}
else {
setSelectedCreateType(type)
setSelectedCreateInfo({ type })
}
}
@ -164,9 +149,23 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
return null
return <>
<PortalSelect
readonly={methodType !== DEFAULT_METHOD}
renderTrigger={() => {
<CustomSelect<Option & { show: boolean; extra?: React.ReactNode; tag?: React.ReactNode }>
options={allOptions.filter(option => option.show)}
value={methodType}
onChange={value => onChooseCreateType(value as any)}
containerProps={{
open: methodType === DEFAULT_METHOD ? undefined : false,
placement: 'bottom-start',
offset: 4,
triggerPopupSameWidth: buttonType === CreateButtonType.FULL_BUTTON,
}}
triggerProps={{
className: cn('h-8 bg-transparent px-0 hover:bg-transparent', methodType !== DEFAULT_METHOD && 'pointer-events-none', buttonType === CreateButtonType.FULL_BUTTON && 'grow'),
}}
popupProps={{
wrapperClassName: 'z-[1000]',
}}
CustomTrigger={() => {
return buttonType === CreateButtonType.FULL_BUTTON ? (
<Button
variant='primary'
@ -174,35 +173,47 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
className='w-full'
onClick={onClickCreate}
>
<RiAddLine className='mr-2 h-4 w-4' />
<RiAddLine className='mr-2 size-4' />
{buttonTextMap[methodType]}
{methodType === SupportedCreationMethods.OAUTH && oauthConfig?.custom_enabled && oauthConfig?.custom_configured && <Badge
className='ml-1 mr-0.5 border-text-primary-on-surface bg-components-badge-bg-dimm text-text-primary-on-surface'
>
{t('plugin.auth.custom')}
</Badge>}
{methodType === SupportedCreationMethods.OAUTH
&& <ActionButton onClick={onClickClientSettings}>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
<RiEqualizer2Line className='size-4 text-text-tertiary' />
</ActionButton>
}
</Button>
) : <ActionButton onClick={onClickCreate}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
) : (
<ActionButton onClick={onClickCreate} className='float-right'>
<RiAddLine className='size-4' />
</ActionButton>
)
}}
triggerClassName='h-8'
popupClassName={cn('z-[1000]')}
value={methodType}
items={allOptions.filter(option => option.show)}
onSelect={item => onChooseCreateType(item.value as any)}
CustomOption={option => (
<>
<div className='mr-8 flex grow items-center gap-1 truncate px-1'>
{option.label}
{option.tag}
</div>
{option.extra}
</>
)}
/>
{selectedCreateType && (
{selectedCreateInfo && (
<CommonCreateModal
createType={selectedCreateType}
onClose={() => setSelectedCreateType(null)}
createType={selectedCreateInfo.type}
builder={selectedCreateInfo.builder}
onClose={() => setSelectedCreateInfo(null)}
/>
)}
{isShowClientSettingsModal && (
<OAuthClientSettingsModal
oauthConfig={oauthConfig}
onClose={hideClientSettingsModal}
showOAuthCreateModal={() => setSelectedCreateType(SupportedCreationMethods.OAUTH)}
showOAuthCreateModal={builder => setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder })}
/>
)}
</>

View File

@ -1,219 +0,0 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCloseLine,
RiLoader2Line,
} from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useTriggerSubscriptionBuilderLogs,
} from '@/service/use-triggers'
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { BaseForm } from '@/app/components/base/form/components/base'
import ActionButton from '@/app/components/base/action-button'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
import type { FormRefObject } from '@/app/components/base/form/types'
import LogViewer from '../log-viewer'
import { usePluginStore } from '../../store'
type Props = {
onClose: () => void
onSuccess: () => void
}
export const ManualCreateModal = ({ onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [subscriptionName, setSubscriptionName] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || []
const propertiesFormRef = React.useRef<FormRefObject>(null)
const { data: logData } = useTriggerSubscriptionBuilderLogs(
providerName,
subscriptionBuilder?.id || '',
{
enabled: !!subscriptionBuilder?.id,
refetchInterval: 3000,
},
)
const logs = logData?.logs || []
useEffect(() => {
if (!subscriptionBuilder) {
createBuilder(
{
provider: providerName,
credential_type: TriggerCredentialTypeEnum.Unauthorized,
},
{
onSuccess: (response) => {
const builder = response.subscription_builder
setSubscriptionBuilder(builder)
},
onError: (error) => {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.errors.createFailed'),
})
console.error('Failed to create subscription builder:', error)
},
},
)
}
}, [createBuilder, providerName, subscriptionBuilder, t])
const handleCreate = () => {
if (!subscriptionName.trim()) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.subscriptionName.required'),
})
return
}
if (!subscriptionBuilder)
return
const formValues = propertiesFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
if (!formValues.isCheckValidated) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.properties.required'),
})
return
}
buildSubscription(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
params: {
name: subscriptionName,
properties: formValues.values,
},
},
{
onSuccess: () => {
Toast.notify({
type: 'success',
message: 'Subscription created successfully',
})
onSuccess()
onClose()
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
})
},
},
)
}
return (
<Modal
isShow
onClose={onClose}
className='!max-w-[640px] !p-0'
wrapperClassName='!z-[1002]'
>
<div className='flex items-center justify-between p-6 pb-3'>
<h3 className='text-lg font-semibold text-text-primary'>
{t('pluginTrigger.modal.manual.title')}
</h3>
<ActionButton onClick={onClose} >
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
<div className='max-h-[70vh] overflow-y-auto p-6 pt-2'>
<div className='mb-6'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.subscriptionName.label')}
</label>
<Input
value={subscriptionName}
onChange={e => setSubscriptionName(e.target.value)}
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
/>
</div>
<div className='mb-6'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.callbackUrl.label')}
</label>
<div className='relative'>
<Input
value={subscriptionBuilder?.endpoint}
readOnly
className='pr-12'
placeholder={t('pluginTrigger.modal.form.callbackUrl.placeholder')}
/>
<CopyFeedbackNew className='absolute right-1 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary' content={subscriptionBuilder?.endpoint || ''} />
</div>
<div className='system-xs-regular mt-1 text-text-tertiary'>
{t('pluginTrigger.modal.form.callbackUrl.description')}
</div>
</div>
{propertiesSchema.length > 0 && (
<div className='mb-6'>
<BaseForm
formSchemas={propertiesSchema}
ref={propertiesFormRef}
/>
</div>
)}
<div className='mb-6'>
<div className='mb-3 flex items-center gap-2'>
<div className='system-xs-medium-uppercase text-text-tertiary'>
REQUESTS HISTORY
</div>
<div className='h-px flex-1 bg-gradient-to-r from-divider-regular to-transparent' />
</div>
<div className='mb-1 flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
<div className='h-3.5 w-3.5'>
<RiLoader2Line className='h-full w-full animate-spin' />
</div>
<div className='system-xs-regular text-text-tertiary'>
Awaiting request from {detail?.declaration.name}...
</div>
</div>
<LogViewer logs={logs} />
</div>
</div>
<div className='flex justify-end gap-2 p-6 pt-4'>
<Button variant='secondary' onClick={onClose}>
{t('pluginTrigger.modal.common.cancel')}
</Button>
<Button
variant='primary'
onClick={handleCreate}
loading={isBuilding}
disabled={!subscriptionName.trim() || !subscriptionBuilder}
>
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
</Button>
</div>
</Modal>
)
}

View File

@ -24,7 +24,7 @@ import { usePluginStore } from '../../store'
type Props = {
oauthConfig?: TriggerOAuthConfig
onClose: () => void
showOAuthCreateModal: () => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
}
enum AuthorizationStatusEnum {
@ -68,7 +68,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
})
onClose()
showOAuthCreateModal()
showOAuthCreateModal(response.subscription_builder)
}
})
},
@ -198,7 +198,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
/>
))}
</div>
{oauthConfig?.redirect_uri && (
{clientType === ClientTypeEnum.Custom && oauthConfig?.redirect_uri && (
<div className='mb-4 flex items-start gap-3 rounded-xl bg-background-section-burn p-4'>
<div className='rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg p-2 shadow-xs shadow-shadow-shadow-3'>
<RiInformation2Fill className='h-5 w-5 shrink-0 text-text-accent' />

View File

@ -1,289 +0,0 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiClipboardLine,
RiCloseLine,
RiInformation2Fill,
} from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import Form from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import {
useBuildTriggerSubscription,
useInitiateTriggerOAuth,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import ActionButton from '@/app/components/base/action-button'
import type { TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { usePluginStore } from '../../store'
type Props = {
oauthConfig?: TriggerOAuthConfig
onClose: () => void
onSuccess: () => void
}
enum OAuthStepEnum {
Setup = 'setup',
Configuration = 'configuration',
}
enum AuthorizationStatusEnum {
Pending = 'pending',
Success = 'success',
Failed = 'failed',
}
export const OAuthCreateModal = ({ oauthConfig, onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup)
const [subscriptionName, setSubscriptionName] = useState('')
const [authorizationUrl, setAuthorizationUrl] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
const [authorizationStatus, setAuthorizationStatus] = useState<AuthorizationStatusEnum>()
const clientFormRef = React.useRef<FormRefObject>(null)
const parametersFormRef = React.useRef<FormRefObject>(null)
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const clientSchema = detail?.declaration.trigger?.oauth_schema?.client_schema || []
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
useEffect(() => {
initiateOAuth(providerName, {
onSuccess: (response) => {
setAuthorizationUrl(response.authorization_url)
setSubscriptionBuilder(response.subscription_builder)
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
})
},
})
}, [initiateOAuth, providerName, t])
useEffect(() => {
if (currentStep === OAuthStepEnum.Setup && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) {
const pollInterval = setInterval(() => {
verifyBuilder(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
},
{
onSuccess: () => {
setAuthorizationStatus(AuthorizationStatusEnum.Success)
setCurrentStep(OAuthStepEnum.Configuration)
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
})
clearInterval(pollInterval)
},
onError: () => {
// Continue polling - auth might still be in progress
},
},
)
}, 3000)
return () => clearInterval(pollInterval)
}
}, [currentStep, subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t])
const handleAuthorize = () => {
const clientFormValues = clientFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
const clientParams = clientFormValues.values
if (!Object.keys(clientParams).length) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.oauth.authorization.authFailed'),
})
return
}
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
if (authorizationUrl) {
// Open authorization URL in new window
window.open(authorizationUrl, '_blank', 'width=500,height=600')
}
}
const handleCreate = () => {
if (!subscriptionName.trim()) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.subscriptionName.required'),
})
return
}
if (!subscriptionBuilder)
return
const parameters = parametersFormRef.current?.getFormValues({})?.values
buildSubscription(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
params: {
name: subscriptionName,
parameters,
} as Record<string, any>,
},
{
onSuccess: () => {
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.configuration.success'),
})
onSuccess()
onClose()
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
})
},
},
)
}
return (
<Modal
isShow
onClose={onClose}
className='!max-w-[520px] p-6'
wrapperClassName='!z-[1002]'
>
<div className='flex items-center justify-between pb-3'>
<h3 className='text-lg font-semibold text-text-primary'>
{t('pluginTrigger.modal.oauth.title')}
</h3>
<ActionButton onClick={onClose}>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
<div className='py-3'>
{currentStep === OAuthStepEnum.Setup && (
<>
{oauthConfig?.redirect_uri && (
<div className='mb-4 flex items-start gap-3 rounded-xl bg-background-section-burn p-4'>
<div className='rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg p-2 shadow-xs shadow-shadow-shadow-3'>
<RiInformation2Fill className='h-5 w-5 shrink-0 text-text-accent' />
</div>
<div className='flex-1 text-text-secondary'>
<div className='system-sm-regular whitespace-pre-wrap leading-4'>
{t('pluginTrigger.modal.oauthRedirectInfo')}
</div>
<div className='system-sm-medium my-1.5 break-all leading-4'>
{oauthConfig.redirect_uri}
</div>
<Button
variant='secondary'
size='small'
onClick={() => {
navigator.clipboard.writeText(oauthConfig.redirect_uri)
Toast.notify({
type: 'success',
message: t('common.actionMsg.copySuccessfully'),
})
}}>
<RiClipboardLine className='mr-1 h-[14px] w-[14px]' />
{t('common.operation.copy')}
</Button>
</div>
</div>
)}
{clientSchema.length > 0 && (
<Form
formSchemas={clientSchema}
ref={clientFormRef}
/>
)}
</>
)}
{currentStep === OAuthStepEnum.Configuration && (
<div>
<div className='mb-4'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.subscriptionName.label')}
</label>
<Input
value={subscriptionName}
onChange={e => setSubscriptionName(e.target.value)}
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
/>
</div>
{subscriptionBuilder?.endpoint && (
<div className='mb-4'>
<label className='system-sm-medium mb-2 block text-text-primary'>
{t('pluginTrigger.modal.form.callbackUrl.label')}
</label>
<Input
value={subscriptionBuilder.endpoint}
readOnly
className='bg-background-section'
/>
<div className='system-xs-regular mt-1 text-text-tertiary'>
{t('pluginTrigger.modal.form.callbackUrl.description')}
</div>
</div>
)}
{parametersSchema.length > 0 && (
<Form
formSchemas={parametersSchema}
ref={parametersFormRef}
/>
)}
</div>
)}
</div>
<div className='flex justify-end gap-2 pt-5'>
<Button variant='secondary' onClick={onClose}>
{t('pluginTrigger.modal.common.cancel')}
</Button>
{currentStep === OAuthStepEnum.Setup && (
<Button
variant='primary'
onClick={handleAuthorize}
loading={authorizationStatus === AuthorizationStatusEnum.Pending}
>
{authorizationStatus === AuthorizationStatusEnum.Pending ? t('pluginTrigger.modal.common.authorizing') : t('pluginTrigger.modal.common.authorize')}
</Button>
)}
{currentStep === OAuthStepEnum.Configuration && (
<Button
variant='primary'
onClick={handleCreate}
loading={isBuilding}
disabled={!subscriptionName.trim()}
>
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
</Button>
)}
</div>
</Modal>
)
}

View File

@ -40,7 +40,7 @@ export const SubscriptionList = () => {
<div className='relative mb-3 flex items-center justify-between'>
{
hasSubscriptions
&& <div className='flex items-center gap-1'>
&& <div className='flex shrink-0 items-center gap-1'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
</span>