diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx
new file mode 100644
index 0000000000..2939924340
--- /dev/null
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx
@@ -0,0 +1,323 @@
+'use client'
+import { BaseForm } from '@/app/components/base/form/components/base'
+import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
+import { FormTypeEnum } from '@/app/components/base/form/types'
+import Modal from '@/app/components/base/modal/modal'
+import Toast from '@/app/components/base/toast'
+import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
+import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
+import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
+import { parsePluginErrorMessage } from '@/utils/error-parser'
+import { useMemo, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { usePluginStore } from '../../store'
+import { useSubscriptionList } from '../use-subscription-list'
+import { ReadmeShowType } from '../../../readme-panel/store'
+import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
+
+type Props = {
+ onClose: () => void
+ subscription: TriggerSubscription
+ pluginDetail?: PluginDetail
+}
+
+enum EditStep {
+ EditCredentials = 'edit_credentials',
+ EditConfiguration = 'edit_configuration',
+}
+
+const normalizeFormType = (type: string): FormTypeEnum => {
+ switch (type) {
+ case 'string':
+ case 'text':
+ return FormTypeEnum.textInput
+ case 'password':
+ case 'secret':
+ return FormTypeEnum.secretInput
+ case 'number':
+ case 'integer':
+ return FormTypeEnum.textNumber
+ case 'boolean':
+ return FormTypeEnum.boolean
+ case 'select':
+ return FormTypeEnum.select
+ default:
+ if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
+ return type as FormTypeEnum
+ return FormTypeEnum.textInput
+ }
+}
+
+const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
+ return
+ {isActive && (
+
+ )}
+ {text}
+
+}
+
+const MultiSteps = ({ currentStep }: { currentStep: EditStep }) => {
+ const { t } = useTranslation()
+ return
+}
+
+export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
+ const { t } = useTranslation()
+ const detail = usePluginStore(state => state.detail)
+ const { refetch } = useSubscriptionList()
+
+ const [currentStep, setCurrentStep] = useState(EditStep.EditCredentials)
+
+ const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
+ const { mutate: verifyCredentials, isPending: isVerifying } = useVerifyTriggerSubscription()
+
+ const parametersSchema = useMemo(
+ () => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
+ [detail?.declaration?.trigger?.subscription_constructor?.parameters],
+ )
+
+ const rawApiKeyCredentialsSchema = detail?.declaration.trigger?.subscription_constructor?.credentials_schema || []
+ const apiKeyCredentialsSchema = useMemo(() => {
+ return rawApiKeyCredentialsSchema.map(schema => ({
+ ...schema,
+ tooltip: schema.help,
+ }))
+ }, [rawApiKeyCredentialsSchema])
+
+ const basicFormRef = useRef(null)
+ const parametersFormRef = useRef(null)
+ const credentialsFormRef = useRef(null)
+
+ const handleVerifyCredentials = () => {
+ const credentialsFormValues = credentialsFormRef.current?.getFormValues({
+ needTransformWhenSecretFieldIsPristine: true,
+ }) || { values: {}, isCheckValidated: false }
+
+ if (!credentialsFormValues.isCheckValidated)
+ return
+
+ const credentials = credentialsFormValues.values
+
+ // Clear previous errors
+ if (Object.keys(credentials).length > 0) {
+ credentialsFormRef.current?.setFields([{
+ name: Object.keys(credentials)[0],
+ errors: [],
+ }])
+ }
+
+ verifyCredentials(
+ {
+ provider: subscription.provider,
+ subscriptionId: subscription.id,
+ credentials,
+ },
+ {
+ onSuccess: () => {
+ Toast.notify({
+ type: 'success',
+ message: t('pluginTrigger.modal.apiKey.verify.success'),
+ })
+ setCurrentStep(EditStep.EditConfiguration)
+ },
+ onError: async (error: any) => {
+ const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
+ if (Object.keys(credentials).length > 0) {
+ credentialsFormRef.current?.setFields([{
+ name: Object.keys(credentials)[0],
+ errors: [errorMessage],
+ }])
+ }
+ },
+ },
+ )
+ }
+
+ const handleUpdate = () => {
+ const basicFormValues = basicFormRef.current?.getFormValues({})
+ if (!basicFormValues?.isCheckValidated)
+ return
+
+ const name = basicFormValues.values.subscription_name as string
+
+ let parameters: Record | undefined
+
+ if (parametersSchema.length > 0) {
+ const paramsFormValues = parametersFormRef.current?.getFormValues({
+ needTransformWhenSecretFieldIsPristine: true,
+ })
+ if (!paramsFormValues?.isCheckValidated)
+ return
+ parameters = Object.keys(paramsFormValues.values).length > 0 ? paramsFormValues.values : undefined
+ }
+
+ updateSubscription(
+ {
+ subscriptionId: subscription.id,
+ name,
+ parameters,
+ },
+ {
+ onSuccess: () => {
+ Toast.notify({
+ type: 'success',
+ message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
+ })
+ refetch?.()
+ onClose()
+ },
+ onError: (error: any) => {
+ Toast.notify({
+ type: 'error',
+ message: error?.message || t('pluginTrigger.subscription.list.item.actions.edit.error'),
+ })
+ },
+ },
+ )
+ }
+
+ const handleConfirm = () => {
+ if (currentStep === EditStep.EditCredentials)
+ handleVerifyCredentials()
+ else
+ handleUpdate()
+ }
+
+ const handleCredentialsChange = () => {
+ if (apiKeyCredentialsSchema.length > 0) {
+ credentialsFormRef.current?.setFields([{
+ name: apiKeyCredentialsSchema[0].name,
+ errors: [],
+ }])
+ }
+ }
+
+ const basicFormSchemas: FormSchema[] = useMemo(() => [
+ {
+ name: 'subscription_name',
+ label: t('pluginTrigger.modal.form.subscriptionName.label'),
+ placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
+ type: FormTypeEnum.textInput,
+ required: true,
+ default: subscription.name,
+ },
+ {
+ name: 'callback_url',
+ label: t('pluginTrigger.modal.form.callbackUrl.label'),
+ placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
+ type: FormTypeEnum.textInput,
+ required: false,
+ default: subscription.endpoint || '',
+ disabled: true,
+ tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
+ showCopy: true,
+ },
+ ], [t, subscription.name, subscription.endpoint])
+
+ const credentialsFormSchemas: FormSchema[] = useMemo(() => {
+ return apiKeyCredentialsSchema.map(schema => ({
+ ...schema,
+ type: normalizeFormType(schema.type as string),
+ tooltip: schema.help,
+ default: subscription.credentials?.[schema.name] || schema.default,
+ }))
+ }, [apiKeyCredentialsSchema, subscription.credentials])
+
+ const parametersFormSchemas: FormSchema[] = useMemo(() => {
+ return parametersSchema.map((schema: ParametersSchema) => {
+ const normalizedType = normalizeFormType(schema.type as string)
+ return {
+ ...schema,
+ type: normalizedType,
+ tooltip: schema.description,
+ default: subscription.parameters?.[schema.name] || schema.default,
+ dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
+ ? {
+ plugin_id: detail?.plugin_id || '',
+ provider: detail?.provider || '',
+ action: 'provider',
+ parameter: schema.name,
+ credential_id: subscription.id,
+ }
+ : undefined,
+ fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
+ labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
+ }
+ })
+ }, [parametersSchema, subscription.parameters, subscription.id, detail?.plugin_id, detail?.provider])
+
+ const getConfirmButtonText = () => {
+ if (currentStep === EditStep.EditCredentials)
+ return isVerifying ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
+
+ return isUpdating ? t('common.operation.saving') : t('common.operation.save')
+ }
+
+ return (
+ : null}
+ >
+ {pluginDetail && (
+
+ )}
+
+ {/* Multi-step indicator */}
+
+
+ {/* Step 1: Edit Credentials */}
+ {currentStep === EditStep.EditCredentials && (
+
+ {credentialsFormSchemas.length > 0 && (
+
+ )}
+
+ )}
+
+ {/* Step 2: Edit Configuration */}
+ {currentStep === EditStep.EditConfiguration && (
+
+ {/* Basic form: subscription name and callback URL */}
+
+
+ {/* Parameters */}
+ {parametersFormSchemas.length > 0 && (
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx
new file mode 100644
index 0000000000..f39df561ab
--- /dev/null
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx
@@ -0,0 +1,28 @@
+'use client'
+import type { PluginDetail } from '@/app/components/plugins/types'
+import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
+import { ManualEditModal } from './manual-edit-modal'
+import { OAuthEditModal } from './oauth-edit-modal'
+import { ApiKeyEditModal } from './apikey-edit-modal'
+
+type Props = {
+ onClose: () => void
+ subscription: TriggerSubscription
+ pluginDetail?: PluginDetail
+}
+
+export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
+ const credentialType = subscription.credential_type
+
+ switch (credentialType) {
+ case TriggerCredentialTypeEnum.Unauthorized:
+ return
+ case TriggerCredentialTypeEnum.Oauth2:
+ return
+ case TriggerCredentialTypeEnum.ApiKey:
+ return
+ default:
+ return null
+ }
+}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
similarity index 88%
rename from web/app/components/plugins/plugin-detail-panel/subscription-list/edit-modal.tsx
rename to web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
index 8602fc0edf..496e6622f1 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit-modal.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx
@@ -10,9 +10,9 @@ import type { TriggerSubscription } from '@/app/components/workflow/block-select
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
-import { usePluginStore } from '../store'
-import { useSubscriptionList } from './use-subscription-list'
-import { ReadmeShowType } from '../../readme-panel/store'
+import { usePluginStore } from '../../store'
+import { useSubscriptionList } from '../use-subscription-list'
+import { ReadmeShowType } from '../../../readme-panel/store'
type Props = {
onClose: () => void
@@ -20,7 +20,6 @@ type Props = {
pluginDetail?: PluginDetail
}
-// Normalize backend type to FormTypeEnum
const normalizeFormType = (type: string): FormTypeEnum => {
switch (type) {
case 'string':
@@ -43,7 +42,7 @@ const normalizeFormType = (type: string): FormTypeEnum => {
}
}
-export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
+export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const { refetch } = useSubscriptionList()
@@ -54,12 +53,10 @@ export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
() => detail?.declaration?.trigger?.subscription_schema || [],
[detail?.declaration?.trigger?.subscription_schema],
)
+
const formRef = useRef(null)
const handleConfirm = () => {
- // Use needTransformWhenSecretFieldIsPristine to handle secret fields
- // When secret field is not modified, it will be transformed to '[__HIDDEN__]'
- // Backend will preserve original value when receiving '[__HIDDEN__]'
const formValues = formRef.current?.getFormValues({
needTransformWhenSecretFieldIsPristine: true,
})
@@ -68,7 +65,7 @@ export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
const name = formValues.values.subscription_name as string
- // Extract properties values (exclude subscription_name and callback_url)
+ // Extract properties (exclude subscription_name and callback_url)
const properties = { ...formValues.values }
delete properties.subscription_name
delete properties.callback_url
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx
new file mode 100644
index 0000000000..d386acafec
--- /dev/null
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx
@@ -0,0 +1,162 @@
+'use client'
+import { BaseForm } from '@/app/components/base/form/components/base'
+import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
+import { FormTypeEnum } from '@/app/components/base/form/types'
+import Modal from '@/app/components/base/modal/modal'
+import Toast from '@/app/components/base/toast'
+import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
+import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
+import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
+import { useUpdateTriggerSubscription } from '@/service/use-triggers'
+import { useMemo, useRef } from 'react'
+import { useTranslation } from 'react-i18next'
+import { usePluginStore } from '../../store'
+import { useSubscriptionList } from '../use-subscription-list'
+import { ReadmeShowType } from '../../../readme-panel/store'
+
+type Props = {
+ onClose: () => void
+ subscription: TriggerSubscription
+ pluginDetail?: PluginDetail
+}
+
+const normalizeFormType = (type: string): FormTypeEnum => {
+ switch (type) {
+ case 'string':
+ case 'text':
+ return FormTypeEnum.textInput
+ case 'password':
+ case 'secret':
+ return FormTypeEnum.secretInput
+ case 'number':
+ case 'integer':
+ return FormTypeEnum.textNumber
+ case 'boolean':
+ return FormTypeEnum.boolean
+ case 'select':
+ return FormTypeEnum.select
+ default:
+ if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
+ return type as FormTypeEnum
+ return FormTypeEnum.textInput
+ }
+}
+
+export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
+ const { t } = useTranslation()
+ const detail = usePluginStore(state => state.detail)
+ const { refetch } = useSubscriptionList()
+
+ const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
+
+ const parametersSchema = useMemo(
+ () => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
+ [detail?.declaration?.trigger?.subscription_constructor?.parameters],
+ )
+
+ const formRef = useRef(null)
+
+ const handleConfirm = () => {
+ const formValues = formRef.current?.getFormValues({
+ needTransformWhenSecretFieldIsPristine: true,
+ })
+ if (!formValues?.isCheckValidated)
+ return
+
+ const name = formValues.values.subscription_name as string
+
+ // Extract parameters (exclude subscription_name and callback_url)
+ const parameters = { ...formValues.values }
+ delete parameters.subscription_name
+ delete parameters.callback_url
+
+ updateSubscription(
+ {
+ subscriptionId: subscription.id,
+ name,
+ parameters: Object.keys(parameters).length > 0 ? parameters : undefined,
+ },
+ {
+ onSuccess: () => {
+ Toast.notify({
+ type: 'success',
+ message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
+ })
+ refetch?.()
+ onClose()
+ },
+ onError: (error: any) => {
+ Toast.notify({
+ type: 'error',
+ message: error?.message || t('pluginTrigger.subscription.list.item.actions.edit.error'),
+ })
+ },
+ },
+ )
+ }
+
+ const formSchemas: FormSchema[] = useMemo(() => [
+ {
+ name: 'subscription_name',
+ label: t('pluginTrigger.modal.form.subscriptionName.label'),
+ placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
+ type: FormTypeEnum.textInput,
+ required: true,
+ default: subscription.name,
+ },
+ {
+ name: 'callback_url',
+ label: t('pluginTrigger.modal.form.callbackUrl.label'),
+ placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
+ type: FormTypeEnum.textInput,
+ required: false,
+ default: subscription.endpoint || '',
+ disabled: true,
+ tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
+ showCopy: true,
+ },
+ ...parametersSchema.map((schema: ParametersSchema) => {
+ const normalizedType = normalizeFormType(schema.type as string)
+ return {
+ ...schema,
+ type: normalizedType,
+ tooltip: schema.description,
+ default: subscription.parameters?.[schema.name] || schema.default,
+ dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
+ ? {
+ plugin_id: detail?.plugin_id || '',
+ provider: detail?.provider || '',
+ action: 'provider',
+ parameter: schema.name,
+ credential_id: subscription.id,
+ }
+ : undefined,
+ fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
+ labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
+ }
+ }),
+ ], [t, subscription.name, subscription.endpoint, subscription.parameters, subscription.id, parametersSchema, detail?.plugin_id, detail?.provider])
+
+ return (
+
+ {pluginDetail && (
+
+ )}
+
+
+ )
+}
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx
index 7e5641eae4..b1b5c8338b 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx
@@ -12,7 +12,7 @@ import {
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { DeleteConfirm } from './delete-confirm'
-import { EditModal } from './edit-modal'
+import { EditModal } from './edit'
type Props = {
data: TriggerSubscription
diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts
index b42f51ffef..1f755f953d 100644
--- a/web/service/use-triggers.ts
+++ b/web/service/use-triggers.ts
@@ -180,6 +180,24 @@ export const useVerifyAndUpdateTriggerSubscriptionBuilder = () => {
})
}
+export const useVerifyTriggerSubscription = () => {
+ return useMutation({
+ mutationKey: [NAME_SPACE, 'verify-subscription'],
+ mutationFn: (payload: {
+ provider: string;
+ subscriptionId: string;
+ credentials?: Record;
+ }) => {
+ const { provider, subscriptionId, ...body } = payload
+ return post<{ verified: boolean }>(
+ `/workspaces/current/trigger-provider/${provider}/subscriptions/verify/${subscriptionId}`,
+ { body },
+ { silent: true },
+ )
+ },
+ })
+}
+
export type BuildTriggerSubscriptionPayload = {
provider: string
subscriptionBuilderId: string
@@ -215,6 +233,7 @@ export type UpdateTriggerSubscriptionPayload = {
subscriptionId: string
name?: string
properties?: Record
+ parameters?: Record
}
export const useUpdateTriggerSubscription = () => {