fix(web): remove unsafe select value casts (#36007)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-05-11 12:01:49 +08:00 committed by GitHub
parent 279b66bc7f
commit a643b05368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 218 additions and 125 deletions

View File

@ -1334,11 +1334,6 @@
"count": 9
}
},
"web/app/components/base/markdown-blocks/form.tsx": {
"erasable-syntax-only/enums": {
"count": 3
}
},
"web/app/components/base/markdown-blocks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 10
@ -4435,11 +4430,6 @@
"count": 1
}
},
"web/app/signin/one-more-step.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/signup/layout.tsx": {
"ts/no-explicit-any": {
"count": 1

View File

@ -55,10 +55,12 @@ export type ConfigParams = {
const prefixSettings = 'overview.appInfo.settings'
type SelectOption = {
value: string
value: Language
name: string
}
const LANGUAGE_OPTIONS: SelectOption[] = languages.filter(item => item.supported)
const createInputInfo = (appInfo: ISettingsModalProps['appInfo']) => {
const {
title,
@ -139,8 +141,13 @@ const SettingsModal: FC<ISettingsModalProps> = ({
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === 'sandbox'
const languageOptions: SelectOption[] = languages.filter(item => item.supported)
const selectedLanguage = languageOptions.find(item => item.value === language)
const selectedLanguage = LANGUAGE_OPTIONS.find(item => item.value === language)
const handleLanguageChange = (nextValue: string | null) => {
const nextLanguage = LANGUAGE_OPTIONS.find(item => item.value === nextValue)
if (nextLanguage)
setLanguage(nextLanguage.value)
}
const handlePlanClick = useCallback(() => {
if (isFreePlan)
setShowPricingModal()
@ -308,17 +315,17 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
<Select
value={selectedLanguage?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
return
setLanguage(nextValue as Language)
}}
onValueChange={handleLanguageChange}
>
<SelectTrigger size="large" className="w-[200px]">
<SelectTrigger
aria-label={t(`${prefixSettings}.language`, { ns: 'appOverview' })}
size="large"
className="w-[200px]"
>
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{languageOptions.map(item => (
{LANGUAGE_OPTIONS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />

View File

@ -12,6 +12,7 @@ import Label from '../../label'
import { useInputTypeOptions } from './hooks'
import Option from './option'
import Trigger from './trigger'
import { InputTypeEnum } from './types'
type InputTypeSelectFieldProps = {
label: string
@ -32,6 +33,12 @@ const InputTypeSelectField = ({
const inputTypeOptions = useInputTypeOptions(supportFile)
const selected = inputTypeOptions.find(option => option.value === field.state.value)
const handleInputTypeChange = (next: string | null) => {
const inputType = InputTypeEnum.safeParse(next)
if (inputType.success)
field.handleChange(inputType.data)
}
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
@ -43,11 +50,7 @@ const InputTypeSelectField = ({
items={inputTypeOptions}
value={field.state.value ?? null}
disabled={disabled}
onValueChange={(next) => {
if (next == null)
return
field.handleChange(next as InputType)
}}
onValueChange={handleInputTypeChange}
>
<SelectTrigger id={field.name} className="gap-x-0.5 px-2">
<Trigger option={selected} />

View File

@ -12,28 +12,32 @@ import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-tim
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
enum DATA_FORMAT {
TEXT = 'text',
JSON = 'json',
}
enum SUPPORTED_TAGS {
LABEL = 'label',
INPUT = 'input',
TEXTAREA = 'textarea',
BUTTON = 'button',
}
enum SUPPORTED_TYPES {
TEXT = 'text',
PASSWORD = 'password',
EMAIL = 'email',
NUMBER = 'number',
DATE = 'date',
TIME = 'time',
DATETIME = 'datetime',
CHECKBOX = 'checkbox',
SELECT = 'select',
HIDDEN = 'hidden',
}
const DATA_FORMAT = {
TEXT: 'text',
JSON: 'json',
} as const
const SUPPORTED_TAGS = {
LABEL: 'label',
INPUT: 'input',
TEXTAREA: 'textarea',
BUTTON: 'button',
} as const
const SUPPORTED_TYPES = {
TEXT: 'text',
PASSWORD: 'password',
EMAIL: 'email',
NUMBER: 'number',
DATE: 'date',
TIME: 'time',
DATETIME: 'datetime',
CHECKBOX: 'checkbox',
SELECT: 'select',
HIDDEN: 'hidden',
} as const
type SupportedType = typeof SUPPORTED_TYPES[keyof typeof SUPPORTED_TYPES]
const SUPPORTED_TYPES_SET = new Set<string>(Object.values(SUPPORTED_TYPES))
@ -253,7 +257,7 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
if (!isSafeName(name))
return null
const type = str(child.properties.type) as SUPPORTED_TYPES
const type = str(child.properties.type) as SupportedType
if (type === SUPPORTED_TYPES.DATE || type === SUPPORTED_TYPES.DATETIME) {
return (
@ -309,7 +313,10 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
<Select
key={key}
defaultValue={formValues[name] as string | undefined}
onValueChange={val => updateValue(name, val as string)}
onValueChange={(val) => {
if (val != null)
updateValue(name, val)
}}
>
<SelectTrigger className="w-full">
<SelectValue />

View File

@ -12,7 +12,12 @@ import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
export type Theme = 'light' | 'dark' | 'system'
const THEMES = ['light', 'dark', 'system'] as const
export type Theme = typeof THEMES[number]
const isTheme = (value: string): value is Theme => {
return (THEMES as readonly string[]).includes(value)
}
export default function ThemeSelector() {
const { t } = useTranslation()
@ -22,6 +27,11 @@ export default function ThemeSelector() {
setTheme(newTheme)
}
const handleThemeValueChange = (value: string) => {
if (isTheme(value))
handleThemeChange(value)
}
const getCurrentIcon = () => {
switch (theme) {
case 'light': return <span className="i-ri-sun-line h-4 w-4 text-text-tertiary" />
@ -43,7 +53,7 @@ export default function ThemeSelector() {
{getCurrentIcon()}
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={6} popupClassName="w-[144px]">
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={value => handleThemeChange(value as Theme)}>
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={handleThemeValueChange}>
<DropdownMenuRadioItem value="light" closeOnClick>
<span className="i-ri-sun-line h-4 w-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">{t('theme.light', { ns: 'common' })}</span>

View File

@ -186,6 +186,15 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
}
}
const handleCreateTypeChange = (value: string | null) => {
const option = visibleOptions.find(item => item.value === value)
if (!option)
return
setIsMenuOpen(false)
void onChooseCreateType(option.value)
}
const onClickCreate = (e: React.MouseEvent<HTMLButtonElement>) => {
if (subscriptionCount >= MAX_COUNT) {
e.stopPropagation()
@ -209,12 +218,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
value={methodType === DEFAULT_METHOD ? null : methodType}
open={shouldAllowSelect ? isMenuOpen : false}
onOpenChange={setIsMenuOpen}
onValueChange={(value) => {
if (!value)
return
setIsMenuOpen(false)
void onChooseCreateType(value as SupportedCreationMethods)
}}
onValueChange={handleCreateTypeChange}
>
<SelectTrigger
render={<div />}

View File

@ -9,8 +9,6 @@ import {
SelectLabel,
SelectTrigger,
} from '@langgenius/dify-ui/select'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
type FrequencyOption = {
@ -27,19 +25,25 @@ const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
const { t } = useTranslation()
const groupLabel = t('nodes.triggerSchedule.frequency.label', { ns: 'workflow' })
const frequencies = useMemo<FrequencyOption[]>(() => [
const frequencies: FrequencyOption[] = [
{ value: 'hourly', name: t('nodes.triggerSchedule.frequency.hourly', { ns: 'workflow' }) },
{ value: 'daily', name: t('nodes.triggerSchedule.frequency.daily', { ns: 'workflow' }) },
{ value: 'weekly', name: t('nodes.triggerSchedule.frequency.weekly', { ns: 'workflow' }) },
{ value: 'monthly', name: t('nodes.triggerSchedule.frequency.monthly', { ns: 'workflow' }) },
], [t])
]
const selectedFrequency = frequencies.find(item => item.value === frequency)
const handleFrequencyChange = (value: string | null) => {
const selected = frequencies.find(item => item.value === value)
if (selected)
onChange(selected.value)
}
return (
<Select
key={`${frequency}-${groupLabel}`}
value={frequency}
onValueChange={value => value && onChange(value as ScheduleFrequency)}
onValueChange={handleFrequencyChange}
>
<SelectTrigger className="w-full py-2">
{selectedFrequency?.name ?? t('nodes.triggerSchedule.selectFrequency', { ns: 'workflow' })}

View File

@ -36,7 +36,7 @@ const HTTP_METHODS = [
{ name: 'DELETE', value: 'DELETE' },
{ name: 'PATCH', value: 'PATCH' },
{ name: 'HEAD', value: 'HEAD' },
]
] satisfies Array<{ name: string, value: HttpMethod }>
const CONTENT_TYPES = [
{ name: 'application/json', value: 'application/json' },
@ -46,6 +46,51 @@ const CONTENT_TYPES = [
{ name: 'multipart/form-data', value: 'multipart/form-data' },
]
type WebhookMethodSelectorProps = {
nodeId: string
label: string
value: HttpMethod
disabled: boolean
onChange: (method: HttpMethod) => void
}
const WebhookMethodSelector = ({
nodeId,
label,
value,
disabled,
onChange,
}: WebhookMethodSelectorProps) => {
const selectedMethod = HTTP_METHODS.find(item => item.value === value) ?? null
const handleMethodChange = (nextValue: string | null) => {
const nextMethod = HTTP_METHODS.find(item => item.value === nextValue)
if (nextMethod)
onChange(nextMethod.value)
}
return (
<Select
key={`${nodeId}-method-${value}`}
value={selectedMethod?.value ?? null}
disabled={disabled}
onValueChange={handleMethodChange}
>
<SelectTrigger aria-label={label} className="h-8 pr-8 text-sm">
{selectedMethod?.name}
</SelectTrigger>
<SelectContent popupClassName="w-26 min-w-26">
{HTTP_METHODS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
)
}
const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
id,
data,
@ -75,7 +120,6 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
}
}, [readOnly, inputs.webhook_url, generateWebhookUrl])
const selectedMethod = HTTP_METHODS.find(item => item.value === inputs.method) ?? null
const selectedContentType = CONTENT_TYPES.find(item => item.value === inputs.content_type) ?? null
return (
@ -86,24 +130,13 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
<div className="space-y-1">
<div className="flex gap-1" style={{ height: '32px' }}>
<div className="w-26 shrink-0">
<Select
key={`${id}-method-${inputs.method}`}
value={selectedMethod?.value ?? null}
<WebhookMethodSelector
nodeId={id}
label={t(`${i18nPrefix}.method`, { ns: 'workflow' })}
value={inputs.method}
disabled={readOnly}
onValueChange={value => value && handleMethodChange(value as HttpMethod)}
>
<SelectTrigger className="h-8 pr-8 text-sm">
{selectedMethod?.name}
</SelectTrigger>
<SelectContent popupClassName="w-26 min-w-26">
{HTTP_METHODS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
onChange={handleMethodChange}
/>
</div>
<div className="flex-1" style={{ width: '284px' }}>
<InputWithCopy

View File

@ -21,11 +21,28 @@ import { useInvitationCheck } from '@/service/use-common'
import { timezones } from '@/utils/timezone'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
type SelectOption = {
type LanguageSelectOption = {
value: Locale
name: string
}
type TimezoneSelectOption = {
value: string
name: string
}
const LANGUAGE_OPTIONS: LanguageSelectOption[] = languages
.filter(item => item.supported)
.map(item => ({
value: item.value,
name: item.name,
}))
const TIMEZONE_OPTIONS: TimezoneSelectOption[] = timezones.map(item => ({
value: String(item.value),
name: item.name,
}))
export default function InviteSettingsPage() {
const { t } = useTranslation()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
@ -35,9 +52,20 @@ export default function InviteSettingsPage() {
const [name, setName] = useState('')
const [language, setLanguage] = useState(LanguagesSupported[0])
const [timezone, setTimezone] = useState(() => Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles')
const languageOptions: SelectOption[] = languages.filter(item => item.supported)
const selectedLanguage = languageOptions.find(item => item.value === language)
const selectedTimezone = timezones.find(item => item.value === timezone)
const selectedLanguage = LANGUAGE_OPTIONS.find(item => item.value === language)
const selectedTimezone = TIMEZONE_OPTIONS.find(item => item.value === timezone)
const handleLanguageChange = (nextValue: string | null) => {
const nextLanguage = LANGUAGE_OPTIONS.find(item => item.value === nextValue)
if (nextLanguage)
setLanguage(nextLanguage.value)
}
const handleTimezoneChange = (nextValue: string | null) => {
const nextTimezone = TIMEZONE_OPTIONS.find(item => item.value === nextValue)
if (nextTimezone)
setTimezone(nextTimezone.value)
}
const checkParams = {
url: '/activate/check',
@ -123,23 +151,19 @@ export default function InviteSettingsPage() {
</div>
</div>
<div className="mb-5">
<label htmlFor="name" className="my-2 system-md-semibold text-text-secondary">
<label htmlFor="interface_language" className="my-2 system-md-semibold text-text-secondary">
{t('interfaceLanguage', { ns: 'login' })}
</label>
<div className="mt-1">
<Select
value={selectedLanguage?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
return
setLanguage(nextValue as Locale)
}}
onValueChange={handleLanguageChange}
>
<SelectTrigger size="large">
<SelectTrigger id="interface_language" size="large">
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{languageOptions.map(item => (
{LANGUAGE_OPTIONS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
@ -156,19 +180,15 @@ export default function InviteSettingsPage() {
</label>
<div className="mt-1">
<Select
value={selectedTimezone ? String(selectedTimezone.value) : null}
onValueChange={(nextValue) => {
if (!nextValue)
return
setTimezone(nextValue as string)
}}
value={selectedTimezone?.value ?? null}
onValueChange={handleTimezoneChange}
>
<SelectTrigger size="large">
<SelectTrigger id="timezone" size="large">
{selectedTimezone?.name ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{timezones.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
{TIMEZONE_OPTIONS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>

View File

@ -1,6 +1,5 @@
'use client'
import type { Reducer } from 'react'
import type { LanguagesSupported } from '@/i18n-config/language'
import { Button } from '@langgenius/dify-ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
@ -51,6 +50,19 @@ type SelectOption = {
name: string
}
const LANGUAGE_OPTIONS: SelectOption[] = languages.filter(item => item.supported)
const TIMEZONE_OPTIONS: SelectOption[] = timezones.map(item => ({
value: String(item.value),
name: item.name,
}))
const hasStatus = (error: unknown): error is { status: number } => {
return typeof error === 'object'
&& error !== null
&& 'status' in error
&& typeof error.status === 'number'
}
const OneMoreStep = () => {
const { t } = useTranslation()
const router = useRouter()
@ -62,9 +74,20 @@ const OneMoreStep = () => {
timezone: 'Asia/Shanghai',
})
const { mutateAsync: submitOneMoreStep, isPending } = useOneMoreStep()
const languageOptions: SelectOption[] = languages.filter(item => item.supported)
const selectedLanguage = languageOptions.find(item => item.value === state.interface_language)
const selectedTimezone = timezones.find(item => item.value === state.timezone)
const selectedLanguage = LANGUAGE_OPTIONS.find(item => item.value === state.interface_language)
const selectedTimezone = TIMEZONE_OPTIONS.find(item => item.value === state.timezone)
const handleLanguageChange = (nextValue: string | null) => {
const nextLanguage = LANGUAGE_OPTIONS.find(item => item.value === nextValue)
if (nextLanguage)
dispatch({ type: 'interface_language', value: nextLanguage.value })
}
const handleTimezoneChange = (nextValue: string | null) => {
const nextTimezone = TIMEZONE_OPTIONS.find(item => item.value === nextValue)
if (nextTimezone)
dispatch({ type: 'timezone', value: nextTimezone.value })
}
const handleSubmit = async () => {
if (isPending)
@ -77,8 +100,8 @@ const OneMoreStep = () => {
})
router.push('/apps')
}
catch (error: any) {
if (error && error.status === 400)
catch (error: unknown) {
if (hasStatus(error) && error.status === 400)
toast.error(t('invalidInvitationCode', { ns: 'login' }))
dispatch({ type: 'failed', payload: null })
}
@ -136,23 +159,19 @@ const OneMoreStep = () => {
</div>
</div>
<div className="mb-5">
<label htmlFor="name" className="my-2 system-md-semibold text-text-secondary">
<label htmlFor="interface_language" className="my-2 system-md-semibold text-text-secondary">
{t('interfaceLanguage', { ns: 'login' })}
</label>
<div className="mt-1">
<Select
value={selectedLanguage?.value ?? null}
onValueChange={(nextValue) => {
if (!nextValue)
return
dispatch({ type: 'interface_language', value: nextValue as typeof LanguagesSupported[number] })
}}
onValueChange={handleLanguageChange}
>
<SelectTrigger size="large">
<SelectTrigger id="interface_language" size="large">
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{languageOptions.map(item => (
{LANGUAGE_OPTIONS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
@ -168,19 +187,15 @@ const OneMoreStep = () => {
</label>
<div className="mt-1">
<Select
value={selectedTimezone ? String(selectedTimezone.value) : null}
onValueChange={(nextValue) => {
if (!nextValue)
return
dispatch({ type: 'timezone', value: nextValue as typeof state.timezone })
}}
value={selectedTimezone?.value ?? null}
onValueChange={handleTimezoneChange}
>
<SelectTrigger size="large">
<SelectTrigger id="timezone" size="large">
{selectedTimezone?.name ?? t('placeholder.select', { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{timezones.map(item => (
<SelectItem key={item.value} value={String(item.value)}>
{TIMEZONE_OPTIONS.map(item => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>