mirror of https://github.com/langgenius/dify.git
feat: add validation status for formitem
This commit is contained in:
parent
63dbc7c63d
commit
854a091f82
|
|
@ -0,0 +1,30 @@
|
|||
import cn from '@/utils/classnames'
|
||||
import { RiLock2Fill } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
frontTextKey?: string
|
||||
backTextKey?: string
|
||||
}
|
||||
|
||||
export const EncryptedBottom = (props: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { frontTextKey, backTextKey, className } = props
|
||||
|
||||
return (
|
||||
<div className={cn('system-xs-regular flex items-center border-t-[0.5px] border-divider-subtle bg-background-soft px-2 py-3 text-text-tertiary', className)}>
|
||||
<RiLock2Fill className='mx-1 h-3 w-3 text-text-quaternary' />
|
||||
{t(frontTextKey || 'common.provider.encrypted.front')}
|
||||
<Link
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</Link>
|
||||
{t(backTextKey || 'common.provider.encrypted.back')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import type { FieldState, FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
|
|
@ -31,6 +31,29 @@ const getExtraProps = (type: FormTypeEnum) => {
|
|||
}
|
||||
}
|
||||
|
||||
const VALIDATE_STATUS_STYLE_MAP: Record<FormItemValidateStatusEnum, { componentClassName: string, textClassName: string, infoFieldName: string }> = {
|
||||
[FormItemValidateStatusEnum.Error]: {
|
||||
componentClassName: 'border-components-input-border-destructive focus:border-components-input-border-destructive',
|
||||
textClassName: 'text-text-destructive',
|
||||
infoFieldName: 'errors',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Warning]: {
|
||||
componentClassName: 'border-components-input-border-warning focus:border-components-input-border-warning',
|
||||
textClassName: 'text-text-warning',
|
||||
infoFieldName: 'warnings',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Success]: {
|
||||
componentClassName: '',
|
||||
textClassName: '',
|
||||
infoFieldName: '',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Validating]: {
|
||||
componentClassName: '',
|
||||
textClassName: '',
|
||||
infoFieldName: '',
|
||||
},
|
||||
}
|
||||
|
||||
export type BaseFieldProps = {
|
||||
fieldClassName?: string
|
||||
labelClassName?: string
|
||||
|
|
@ -40,6 +63,7 @@ export type BaseFieldProps = {
|
|||
field: AnyFieldApi
|
||||
disabled?: boolean
|
||||
onChange?: (field: string, value: any) => void
|
||||
fieldState?: FieldState
|
||||
}
|
||||
|
||||
const BaseField = ({
|
||||
|
|
@ -51,6 +75,7 @@ const BaseField = ({
|
|||
field,
|
||||
disabled: propsDisabled,
|
||||
onChange,
|
||||
fieldState,
|
||||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const {
|
||||
|
|
@ -168,7 +193,7 @@ const BaseField = ({
|
|||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
className={cn(inputClassName)}
|
||||
className={cn(inputClassName, VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus as FormItemValidateStatusEnum]?.componentClassName)}
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
handleChange(e.target.value)
|
||||
|
|
@ -266,6 +291,14 @@ const BaseField = ({
|
|||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
|
||||
<div className={cn(
|
||||
'system-xs-regular mt-1 px-0 py-[2px]',
|
||||
VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].textClassName,
|
||||
)}>
|
||||
{fieldState?.[VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].infoFieldName as keyof FieldState]}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
formSchema.url && (
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {
|
||||
AnyFieldApi,
|
||||
|
|
@ -12,9 +13,12 @@ import {
|
|||
useForm,
|
||||
useStore,
|
||||
} from '@tanstack/react-form'
|
||||
import type {
|
||||
FormRef,
|
||||
FormSchema,
|
||||
import {
|
||||
type FieldState,
|
||||
FormItemValidateStatusEnum,
|
||||
type FormRef,
|
||||
type FormSchema,
|
||||
type SetFieldsParam,
|
||||
} from '@/app/components/base/form/types'
|
||||
import {
|
||||
BaseField,
|
||||
|
|
@ -72,6 +76,8 @@ const BaseForm = ({
|
|||
const { getFormValues } = useGetFormValues(form, formSchemas)
|
||||
const { getValidators } = useGetValidators()
|
||||
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, FieldState>>({})
|
||||
|
||||
const showOnValues = useStore(form.store, (s: any) => {
|
||||
const result: Record<string, any> = {}
|
||||
formSchemas.forEach((schema) => {
|
||||
|
|
@ -85,6 +91,34 @@ const BaseForm = ({
|
|||
return result
|
||||
})
|
||||
|
||||
const setFields = useCallback((fields: SetFieldsParam[]) => {
|
||||
const newFieldStates: Record<string, FieldState> = { ...fieldStates }
|
||||
|
||||
for (const field of fields) {
|
||||
const { name, value, errors, warnings, validateStatus, help } = field
|
||||
|
||||
if (value !== undefined)
|
||||
form.setFieldValue(name, value)
|
||||
|
||||
let finalValidateStatus = validateStatus
|
||||
if (!finalValidateStatus) {
|
||||
if (errors && errors.length > 0)
|
||||
finalValidateStatus = FormItemValidateStatusEnum.Error
|
||||
else if (warnings && warnings.length > 0)
|
||||
finalValidateStatus = FormItemValidateStatusEnum.Warning
|
||||
}
|
||||
|
||||
newFieldStates[name] = {
|
||||
validateStatus: finalValidateStatus,
|
||||
help,
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
setFieldStates(newFieldStates)
|
||||
}, [form, fieldStates])
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getForm() {
|
||||
|
|
@ -93,8 +127,9 @@ const BaseForm = ({
|
|||
getFormValues: (option) => {
|
||||
return getFormValues(option)
|
||||
},
|
||||
setFields,
|
||||
}
|
||||
}, [form, getFormValues])
|
||||
}, [form, getFormValues, setFields])
|
||||
|
||||
const renderField = useCallback((field: AnyFieldApi) => {
|
||||
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
||||
|
|
@ -110,12 +145,13 @@ const BaseForm = ({
|
|||
inputClassName={inputClassName}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
fieldState={fieldStates[field.name]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange])
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, fieldStates])
|
||||
|
||||
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
||||
const validators = getValidators(formSchema)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,13 @@ export type FormOption = {
|
|||
|
||||
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
|
||||
|
||||
export enum FormItemValidateStatusEnum {
|
||||
Success = 'success',
|
||||
Warning = 'warning',
|
||||
Error = 'error',
|
||||
Validating = 'validating',
|
||||
}
|
||||
|
||||
export type FormSchema = {
|
||||
type: FormTypeEnum
|
||||
name: string
|
||||
|
|
@ -79,11 +86,25 @@ export type GetValuesOptions = {
|
|||
needTransformWhenSecretFieldIsPristine?: boolean
|
||||
needCheckValidatedValues?: boolean
|
||||
}
|
||||
|
||||
export type FieldState = {
|
||||
validateStatus?: FormItemValidateStatusEnum
|
||||
help?: string | ReactNode
|
||||
errors?: string[]
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
export type SetFieldsParam = {
|
||||
name: string
|
||||
value?: any
|
||||
} & FieldState
|
||||
|
||||
export type FormRefObject = {
|
||||
getForm: () => AnyFormApi
|
||||
getFormValues: (obj: GetValuesOptions) => {
|
||||
values: Record<string, any>
|
||||
isCheckValidated: boolean
|
||||
}
|
||||
setFields: (fields: SetFieldsParam[]) => void
|
||||
}
|
||||
export type FormRef = ForwardedRef<FormRefObject>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client'
|
||||
// import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
|
|
@ -15,10 +16,11 @@ import {
|
|||
useUpdateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { debounce } from 'lodash-es'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debounce } from 'lodash-es'
|
||||
import LogViewer from '../log-viewer'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from '../store'
|
||||
|
||||
|
|
@ -68,7 +70,6 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
|
||||
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
|
||||
const [verificationError, setVerificationError] = useState<string>('')
|
||||
const isInitializedRef = useRef(false)
|
||||
|
||||
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
|
||||
|
|
@ -175,7 +176,10 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
return
|
||||
}
|
||||
|
||||
setVerificationError('')
|
||||
apiKeyCredentialsFormRef.current?.setFields([{
|
||||
name: Object.keys(credentials)[0],
|
||||
errors: [],
|
||||
}])
|
||||
|
||||
verifyCredentials(
|
||||
{
|
||||
|
|
@ -191,8 +195,12 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
})
|
||||
setCurrentStep(ApiKeyStep.Configuration)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error'))
|
||||
onError: async (error: any) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
|
||||
apiKeyCredentialsFormRef.current?.setFields([{
|
||||
name: Object.keys(credentials)[0],
|
||||
errors: [errorMessage],
|
||||
}])
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -238,10 +246,11 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
onClose()
|
||||
refresh?.()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: async (error: any) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.errors.createFailed')
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
|
||||
message: errorMessage,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
@ -255,6 +264,13 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
handleCreate()
|
||||
}
|
||||
|
||||
const handleApiKeyCredentialsChange = () => {
|
||||
apiKeyCredentialsFormRef.current?.setFields([{
|
||||
name: apiKeyCredentialsSchema[0].name,
|
||||
errors: [],
|
||||
}])
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`pluginTrigger.modal.${createType === SupportedCreationMethods.APIKEY ? 'apiKey' : createType.toLowerCase()}.title`)}
|
||||
|
|
@ -266,6 +282,8 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
disabled={isVerifyingCredentials || isBuilding}
|
||||
bottomSlot={currentStep === ApiKeyStep.Verify ? <EncryptedBottom /> : null}
|
||||
>
|
||||
{createType === SupportedCreationMethods.APIKEY && <MultiSteps currentStep={currentStep} />}
|
||||
{currentStep === ApiKeyStep.Verify && (
|
||||
|
|
@ -278,16 +296,10 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
|||
labelClassName='system-sm-medium mb-2 block text-text-primary'
|
||||
preventDefaultSubmit={true}
|
||||
formClassName='space-y-4'
|
||||
onChange={handleApiKeyCredentialsChange}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentStep === ApiKeyStep.Configuration && <div className='max-h-[70vh] overflow-y-auto'>
|
||||
|
|
|
|||
|
|
@ -21,30 +21,23 @@ export const SubscriptionList = withErrorBoundary(({
|
|||
selectedId,
|
||||
onSelect,
|
||||
}: SubscriptionListProps) => {
|
||||
const { subscriptions, isLoading, hasSubscriptions } = useSubscriptionList()
|
||||
|
||||
// console.log('detail', detail)
|
||||
const { subscriptions, isLoading } = useSubscriptionList()
|
||||
|
||||
if (mode === SubscriptionListMode.SELECTOR) {
|
||||
return (
|
||||
<SubscriptionSelectorView
|
||||
subscriptions={subscriptions}
|
||||
isLoading={isLoading}
|
||||
hasSubscriptions={hasSubscriptions}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// const showTopBorder = !!(detail?.declaration?.tool || detail?.declaration?.endpoint)
|
||||
|
||||
return (
|
||||
<SubscriptionListView
|
||||
subscriptions={subscriptions}
|
||||
isLoading={isLoading}
|
||||
// showTopBorder={showTopBorder}
|
||||
hasSubscriptions={hasSubscriptions}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@ type SubscriptionListViewProps = {
|
|||
subscriptions?: TriggerSubscription[]
|
||||
isLoading: boolean
|
||||
showTopBorder?: boolean
|
||||
hasSubscriptions: boolean
|
||||
}
|
||||
|
||||
export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||
subscriptions,
|
||||
isLoading,
|
||||
showTopBorder = false,
|
||||
hasSubscriptions,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
@ -35,7 +33,7 @@ export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
|||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='relative mb-3 flex items-center justify-between'>
|
||||
{hasSubscriptions && (
|
||||
{subscriptions?.length && (
|
||||
<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 })}
|
||||
|
|
@ -44,11 +42,11 @@ export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
|||
</div>
|
||||
)}
|
||||
<CreateSubscriptionButton
|
||||
buttonType={hasSubscriptions ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON}
|
||||
buttonType={subscriptions?.length ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasSubscriptions && (
|
||||
{subscriptions?.length && (
|
||||
<div className='flex flex-col gap-1'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiCheckLine, RiDeleteBinLine } from '@remixicon/react'
|
||||
import { RiCheckLine, RiDeleteBinLine, RiWebhookLine } from '@remixicon/react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from './create'
|
||||
|
|
@ -13,7 +12,6 @@ import { DeleteConfirm } from './delete-confirm'
|
|||
type SubscriptionSelectorProps = {
|
||||
subscriptions?: TriggerSubscription[]
|
||||
isLoading: boolean
|
||||
hasSubscriptions: boolean
|
||||
selectedId?: string
|
||||
onSelect?: ({ id, name }: { id: string, name: string }) => void
|
||||
}
|
||||
|
|
@ -21,7 +19,6 @@ type SubscriptionSelectorProps = {
|
|||
export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
|
||||
subscriptions,
|
||||
isLoading,
|
||||
hasSubscriptions,
|
||||
selectedId,
|
||||
onSelect,
|
||||
}) => {
|
||||
|
|
@ -38,7 +35,7 @@ export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
|
|||
|
||||
return (
|
||||
<div className='w-[320px] p-1'>
|
||||
{hasSubscriptions && <div className='ml-7 mr-1.5 mt-0.5 flex items-center justify-between'>
|
||||
{subscriptions?.length && <div className='ml-7 mr-1.5 mt-0.5 flex items-center justify-between'>
|
||||
<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 })}
|
||||
|
|
@ -50,7 +47,7 @@ export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
|
|||
/>
|
||||
</div>}
|
||||
<div className='max-h-[320px] overflow-y-auto'>
|
||||
{hasSubscriptions ? (
|
||||
{subscriptions?.length ? (
|
||||
<>
|
||||
{subscriptions?.map(subscription => (
|
||||
<button
|
||||
|
|
@ -66,10 +63,7 @@ export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
|
|||
{selectedId === subscription.id && (
|
||||
<RiCheckLine className='mr-2 h-4 w-4 shrink-0 text-text-accent' />
|
||||
)}
|
||||
<Indicator
|
||||
color={subscription.properties?.active !== false ? 'green' : 'red'}
|
||||
className={cn('mr-1.5', selectedId !== subscription.id && 'ml-6')}
|
||||
/>
|
||||
<RiWebhookLine className={cn('mr-1.5 h-3.5 w-3.5 text-text-secondary', selectedId !== subscription.id && 'ml-6')} />
|
||||
<span className='system-md-regular leading-6 text-text-secondary'>
|
||||
{subscription.name}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
'use client'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
|
|
@ -22,8 +21,6 @@ const SubscriptionCard = ({ data }: Props) => {
|
|||
setFalse: hideDeleteModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const isActive = data.properties?.active !== false
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -35,28 +32,19 @@ const SubscriptionCard = ({ data }: Props) => {
|
|||
)}
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='flex h-6 items-center gap-1'>
|
||||
<RiWebhookLine className='h-4 w-4 text-text-secondary' />
|
||||
<span className='system-md-semibold text-text-secondary'>
|
||||
{data.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex h-[26px] w-6 items-center justify-center group-hover:hidden'>
|
||||
<Indicator
|
||||
color={isActive ? 'green' : 'red'}
|
||||
className=''
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='hidden group-hover:block'>
|
||||
<ActionButton
|
||||
onClick={showDeleteModal}
|
||||
className='subscription-delete-btn transition-colors hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={showDeleteModal}
|
||||
className='subscription-delete-btn hidden transition-colors hover:bg-state-destructive-hover hover:text-text-destructive group-hover:block'
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div className='mt-1 flex items-center justify-between'>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,5 @@ export const useSubscriptionList = () => {
|
|||
subscriptions,
|
||||
isLoading,
|
||||
refetch,
|
||||
hasSubscriptions: !!(subscriptions && subscriptions.length > 0),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ export const useVerifyTriggerSubscriptionBuilder = () => {
|
|||
return post<{ verified: boolean }>(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
|
||||
{ body },
|
||||
{ silent: true },
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Parse plugin error message from nested error structure
|
||||
* Extracts the real error message from PluginInvokeError JSON string
|
||||
*
|
||||
* @example
|
||||
* Input: { message: "req_id: xxx PluginInvokeError: {\"message\":\"Bad credentials\"}" }
|
||||
* Output: "Bad credentials"
|
||||
*
|
||||
* @param error - Error object (can be Response object or error with message property)
|
||||
* @returns Promise<string> or string - Parsed error message
|
||||
*/
|
||||
export const parsePluginErrorMessage = async (error: any): Promise<string> => {
|
||||
let rawMessage = ''
|
||||
|
||||
// Handle Response object from fetch/ky
|
||||
if (error instanceof Response) {
|
||||
try {
|
||||
const body = await error.clone().json()
|
||||
rawMessage = body?.message || error.statusText || 'Unknown error'
|
||||
}
|
||||
catch {
|
||||
rawMessage = error.statusText || 'Unknown error'
|
||||
}
|
||||
}
|
||||
else {
|
||||
rawMessage = error?.message || error?.toString() || 'Unknown error'
|
||||
}
|
||||
|
||||
console.log('rawMessage', rawMessage)
|
||||
|
||||
// Try to extract nested JSON from PluginInvokeError
|
||||
// Use greedy match .+ to capture the complete JSON object with nested braces
|
||||
const pluginErrorPattern = /PluginInvokeError:\s*(\{.+\})/
|
||||
const match = rawMessage.match(pluginErrorPattern)
|
||||
|
||||
if (match) {
|
||||
try {
|
||||
const errorData = JSON.parse(match[1])
|
||||
// Return the inner message if exists
|
||||
if (errorData.message)
|
||||
return errorData.message
|
||||
// Fallback to error_type if message not available
|
||||
if (errorData.error_type)
|
||||
return errorData.error_type
|
||||
}
|
||||
catch (parseError) {
|
||||
console.warn('Failed to parse plugin error JSON:', parseError)
|
||||
}
|
||||
}
|
||||
|
||||
return rawMessage
|
||||
}
|
||||
Loading…
Reference in New Issue