feat(trigger): add formitem desc

This commit is contained in:
yessenia 2025-10-23 10:57:19 +08:00
parent 06d1a2e2fd
commit 949ac9d930
4 changed files with 182 additions and 159 deletions

View File

@ -1,5 +1,5 @@
import CheckboxList from '@/app/components/base/checkbox-list'
import type { FieldState, FormSchema } from '@/app/components/base/form/types'
import type { FieldState, FormSchema, TypeWithI18N } 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'
@ -31,6 +31,19 @@ const getExtraProps = (type: FormTypeEnum) => {
}
}
const getTranslatedContent = ({ content, render }: {
content: React.ReactNode | string | null | undefined | TypeWithI18N<string> | Record<string, string>
render: (content: TypeWithI18N<string> | Record<string, string>) => string
}): string => {
if (isValidElement(content) || typeof content === 'string')
return content as string
if (typeof content === 'object' && content !== null)
return render(content as TypeWithI18N<string>)
return ''
}
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',
@ -91,24 +104,21 @@ const BaseField = ({
multiple = false,
tooltip,
showCopy,
description,
url,
help,
} = formSchema
const disabled = propsDisabled || formSchemaDisabled
const memorizedLabel = useMemo(() => {
if (isValidElement(label) || typeof label === 'string')
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject])
const memorizedPlaceholder = useMemo(() => {
if (typeof placeholder === 'string')
return placeholder
if (typeof placeholder === 'object' && placeholder !== null)
return renderI18nObject(placeholder as Record<string, string>)
}, [placeholder, renderI18nObject])
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
return [
label,
placeholder,
tooltip,
description,
help,
].map(v => getTranslatedContent({ content: v, render: renderI18nObject }))
}, [label, placeholder, tooltip, description, help, renderI18nObject])
const watchedVariables = useMemo(() => {
const variables = new Set<string>()
@ -139,7 +149,7 @@ const BaseField = ({
})
}).map((option) => {
return {
label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
label: getTranslatedContent({ content: option.label, render: renderI18nObject }),
value: option.value,
}
}) || []
@ -162,7 +172,7 @@ const BaseField = ({
if (!dynamicOptionsData?.options)
return []
return dynamicOptionsData.options.map(option => ({
name: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
name: getTranslatedContent({ content: option.label, render: renderI18nObject }),
value: option.value,
}))
}, [dynamicOptionsData, renderI18nObject])
@ -173,150 +183,158 @@ const BaseField = ({
}, [field, onChange])
return (
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName, formLabelClassName)}>
{memorizedLabel}
{
required && !isValidElement(label) && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
{tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>{typeof tooltip === 'string' ? tooltip : renderI18nObject(tooltip as Record<string, string>)}</div>}
triggerClassName='ml-0.5 w-4 h-4'
/>
)}
</div>
<div className={cn(inputContainerClassName)}>
{
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
<Input
id={field.name}
name={field.name}
className={cn(inputClassName, VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus as FormItemValidateStatusEnum]?.componentClassName)}
value={value || ''}
onChange={(e) => {
handleChange(e.target.value)
}}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
{...getExtraProps(formItemType)}
showCopyIcon={showCopy}
<>
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName, formLabelClassName)}>
{translatedLabel}
{
required && !isValidElement(label) && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
{tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>{translatedTooltip}</div>}
triggerClassName='ml-0.5 w-4 h-4'
/>
)
}
{
formItemType === FormTypeEnum.select && !multiple && (
<PureSelect
value={value}
onChange={v => handleChange(v)}
disabled={disabled}
placeholder={memorizedPlaceholder}
options={memorizedOptions}
triggerPopupSameWidth
popupProps={{
className: 'max-h-[320px] overflow-y-auto',
}}
/>
)
}
{
formItemType === FormTypeEnum.checkbox /* && multiple */ && (
<CheckboxList
title={name}
value={value}
onChange={v => field.handleChange(v)}
options={memorizedOptions}
maxHeight='200px'
/>
)
}
{
formItemType === FormTypeEnum.dynamicSelect && (
<PortalSelect
value={value}
onSelect={(item: any) => field.handleChange(item.value)}
readonly={disabled || isDynamicOptionsLoading}
placeholder={
isDynamicOptionsLoading
? 'Loading options...'
: memorizedPlaceholder || 'Select an option'
}
items={dynamicOptions}
popupClassName="z-[9999]"
/>
)
}
{
formItemType === FormTypeEnum.radio && (
)}
</div>
<div className={cn(inputContainerClassName)}>
{
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
<Input
id={field.name}
name={field.name}
className={cn(inputClassName, VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus as FormItemValidateStatusEnum]?.componentClassName)}
value={value || ''}
onChange={(e) => {
handleChange(e.target.value)
}}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={translatedPlaceholder}
{...getExtraProps(formItemType)}
showCopyIcon={showCopy}
/>
)
}
{
formItemType === FormTypeEnum.select && !multiple && (
<PureSelect
value={value}
onChange={v => handleChange(v)}
disabled={disabled}
placeholder={translatedPlaceholder}
options={memorizedOptions}
triggerPopupSameWidth
popupProps={{
className: 'max-h-[320px] overflow-y-auto',
}}
/>
)
}
{
formItemType === FormTypeEnum.checkbox /* && multiple */ && (
<CheckboxList
title={name}
value={value}
onChange={v => field.handleChange(v)}
options={memorizedOptions}
maxHeight='200px'
/>
)
}
{
formItemType === FormTypeEnum.dynamicSelect && (
<PortalSelect
value={value}
onSelect={(item: any) => field.handleChange(item.value)}
readonly={disabled || isDynamicOptionsLoading}
placeholder={
isDynamicOptionsLoading
? 'Loading options...'
: translatedPlaceholder || 'Select an option'
}
items={dynamicOptions}
popupClassName="z-[9999]"
/>
)
}
{
formItemType === FormTypeEnum.radio && (
<div className={cn(
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
)}>
{
memorizedOptions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
inputClassName,
)}
onClick={() => !disabled && handleChange(option.value)}
>
{
formSchema.showRadioUI && (
<RadioE
className='mr-2'
isChecked={value === option.value}
/>
)
}
{option.label}
</div>
))
}
</div>
)
}
{
formItemType === FormTypeEnum.boolean && (
<Radio.Group
className='flex w-fit items-center'
value={value}
onChange={v => field.handleChange(v)}
>
<Radio value={true} className='!mr-1'>True</Radio>
<Radio value={false}>False</Radio>
</Radio.Group>
)
}
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
<div className={cn(
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
'system-xs-regular mt-1 px-0 py-[2px]',
VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].textClassName,
)}>
{
memorizedOptions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
inputClassName,
)}
onClick={() => !disabled && handleChange(option.value)}
>
{
formSchema.showRadioUI && (
<RadioE
className='mr-2'
isChecked={value === option.value}
/>
)
}
{option.label}
</div>
))
}
{fieldState?.[VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].infoFieldName as keyof FieldState]}
</div>
)
}
{
formItemType === FormTypeEnum.boolean && (
<Radio.Group
className='flex w-fit items-center'
value={value}
onChange={v => field.handleChange(v)}
>
<Radio value={true} className='!mr-1'>True</Radio>
<Radio value={false}>False</Radio>
</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
className='system-xs-regular mt-4 flex items-center text-text-accent'
href={formSchema?.url}
target='_blank'
>
<span className='break-all'>
{renderI18nObject(formSchema?.help as any)}
</span>
<RiExternalLinkLine className='ml-1 h-3 w-3 shrink-0' />
</a>
)
}
)}
</div>
</div>
</div>
{description && (
<div className='system-xs-regular mt-4 text-text-tertiary'>
{translatedDescription}
</div>
)}
{
url && (
<a
className='system-xs-regular mt-4 flex items-center text-text-accent'
href={url}
target='_blank'
>
<span className='break-all'>
{translatedHelp}
</span>
<RiExternalLinkLine className='ml-1 h-3 w-3 shrink-0' />
</a>
)
}
</>
)
}

View File

@ -26,6 +26,7 @@ type ModalProps = {
footerSlot?: React.ReactNode
bottomSlot?: React.ReactNode
disabled?: boolean
containerClassName?: string
}
const Modal = ({
onClose,
@ -44,6 +45,7 @@ const Modal = ({
footerSlot,
bottomSlot,
disabled,
containerClassName,
}: ModalProps) => {
const { t } = useTranslation()
@ -55,9 +57,10 @@ const Modal = ({
>
<div
className={cn(
'flex max-h-[80%] min-h-[360px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs',
'flex max-h-[80%] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs',
size === 'sm' && 'w-[480px]',
size === 'md' && 'w-[640px]',
containerClassName,
)}
onClick={e => e.stopPropagation()}
>

View File

@ -370,6 +370,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
disabled={isVerifyingCredentials || isBuilding}
bottomSlot={currentStep === ApiKeyStep.Verify ? <EncryptedBottom /> : null}
size={createType === SupportedCreationMethods.MANUAL ? 'md' : 'sm'}
containerClassName='min-h-[360px]'
>
{createType === SupportedCreationMethods.APIKEY && <MultiSteps currentStep={currentStep} />}
{currentStep === ApiKeyStep.Verify && (
@ -432,6 +433,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
credential_id: subscriptionBuilder?.id || '',
} : undefined,
fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
}
})}
ref={autoCommonParametersFormRef}

View File

@ -189,7 +189,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
onCancel={() => handleSave(false)}
onConfirm={() => handleSave(true)}
footerSlot={
oauthConfig?.custom_enabled && oauthConfig?.params && (
oauthConfig?.custom_enabled && oauthConfig?.params && clientType === ClientTypeEnum.Custom && (
<div className='grow'>
<Button
variant='secondary'