dify/web/app/components/header/account-setting/update-setting-dialog.tsx
Jingyi 9b74df21d0
feat(web): refine onboarding UI (#37433)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: fatelei <fatelei@gmail.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
Co-authored-by: gigglewang <gigglewang@dify.ai>
Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai>
Co-authored-by: chariri <w@chariri.moe>
Co-authored-by: Evan <2869018789@qq.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
2026-06-15 08:47:15 +00:00

284 lines
11 KiB
TypeScript

'use client'
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle, DialogTrigger } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
import { useAppContext } from '@/context/app-context'
import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings } from '@/service/use-plugins'
import UpdateSettingDialogForm from './update-setting-dialog-form'
type Props = {
category: PluginCategoryEnum
disabled?: boolean
}
const UpdateSettingDialog = ({
category,
disabled = false,
}: Props) => {
const { t } = useTranslation()
const { userProfile } = useAppContext()
const timezone = userProfile.timezone || 'UTC'
const {
data: autoUpgradeSetting,
error,
isFetching,
isLoading,
} = usePluginAutoUpgradeSettings(category)
const { mutate: saveAutoUpgrade, isPending: isSavePending } = useMutationPluginAutoUpgradeSettings({
category,
onSuccess: () => {
toast.success(t('api.actionSuccess', { ns: 'common' }))
},
})
const savedAutoUpgrade = autoUpgradeSetting?.auto_upgrade
const [isOpen, setIsOpen] = useState(false)
const [draftAutoUpgrade, setDraftAutoUpgrade] = useState<AutoUpdateConfig>()
const isSettingsLoading = !savedAutoUpgrade && !error && (isLoading || isFetching)
const autoUpgrade = draftAutoUpgrade ?? savedAutoUpgrade
const hasSettings = !!autoUpgrade
const getStrategyLabel = useCallback((strategy: AUTO_UPDATE_STRATEGY) => {
switch (strategy) {
case AUTO_UPDATE_STRATEGY.disabled:
return t('autoUpdate.strategy.disabled.name', { ns: 'plugin' })
case AUTO_UPDATE_STRATEGY.fixOnly:
return t('autoUpdate.strategy.fixOnly.name', { ns: 'plugin' })
case AUTO_UPDATE_STRATEGY.latest:
return t('autoUpdate.strategy.latest.name', { ns: 'plugin' })
default:
return ''
}
}, [t])
const selectedStrategyLabel = autoUpgrade ? getStrategyLabel(autoUpgrade.strategy_setting) : ''
const plugins = useMemo(() => {
if (!autoUpgrade)
return []
switch (autoUpgrade.upgrade_mode) {
case AUTO_UPDATE_MODE.partial:
return autoUpgrade.include_plugins
case AUTO_UPDATE_MODE.exclude:
return autoUpgrade.exclude_plugins
default:
return []
}
}, [autoUpgrade])
const updateTimeValue = useMemo(() => {
if (!autoUpgrade)
return ''
const localSeconds = convertUTCDaySecondsToLocalSeconds(autoUpgrade.upgrade_time_of_day, timezone)
return timeOfDayToDayjs(localSeconds).format('HH:mm')
}, [autoUpgrade, timezone])
const strategyOptions = useMemo(() => [
{
value: AUTO_UPDATE_STRATEGY.disabled,
label: getStrategyLabel(AUTO_UPDATE_STRATEGY.disabled),
},
{
value: AUTO_UPDATE_STRATEGY.fixOnly,
label: getStrategyLabel(AUTO_UPDATE_STRATEGY.fixOnly),
},
{
value: AUTO_UPDATE_STRATEGY.latest,
label: getStrategyLabel(AUTO_UPDATE_STRATEGY.latest),
},
], [getStrategyLabel])
const scopeOptions = useMemo(() => [
{
value: AUTO_UPDATE_MODE.update_all,
label: t('autoUpdate.scopeMode.all', { ns: 'plugin' }),
},
{
value: AUTO_UPDATE_MODE.exclude,
label: t('autoUpdate.scopeMode.exclude', { ns: 'plugin' }),
},
{
value: AUTO_UPDATE_MODE.partial,
label: t('autoUpdate.upgradeMode.partial', { ns: 'plugin' }),
},
], [t])
const updateAutoUpgrade = useCallback((payload: Partial<AutoUpdateConfig>) => {
setDraftAutoUpgrade((currentAutoUpgrade) => {
const baseAutoUpgrade = currentAutoUpgrade ?? savedAutoUpgrade
if (!baseAutoUpgrade)
return undefined
return {
...baseAutoUpgrade,
...payload,
}
})
}, [savedAutoUpgrade])
const handlePluginsChange = useCallback((newPlugins: string[]) => {
if (!autoUpgrade)
return
if (autoUpgrade.upgrade_mode === AUTO_UPDATE_MODE.partial) {
updateAutoUpgrade({
include_plugins: newPlugins,
})
}
else if (autoUpgrade.upgrade_mode === AUTO_UPDATE_MODE.exclude) {
updateAutoUpgrade({
exclude_plugins: newPlugins,
})
}
}, [autoUpgrade, updateAutoUpgrade])
const minuteFilter = useCallback((minutes: string[]) => {
return minutes.filter((m) => {
const time = Number.parseInt(m, 10)
return time % 15 === 0
})
}, [])
const handleUpdateTimeChange = useCallback((value: Parameters<typeof dayjsToTimeOfDay>[0]) => {
updateAutoUpgrade({
upgrade_time_of_day: convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(value), timezone),
})
}, [timezone, updateAutoUpgrade])
const handleOpenChange = useCallback((open: boolean) => {
if (disabled) {
setIsOpen(false)
return
}
setIsOpen(open)
if (open)
setDraftAutoUpgrade(savedAutoUpgrade)
else
setDraftAutoUpgrade(undefined)
}, [disabled, savedAutoUpgrade])
const handleCancel = useCallback(() => {
setDraftAutoUpgrade(undefined)
setIsOpen(false)
}, [])
const handleSave = useCallback(() => {
if (!autoUpgrade)
return
saveAutoUpgrade(autoUpgrade)
setDraftAutoUpgrade(undefined)
setIsOpen(false)
}, [autoUpgrade, saveAutoUpgrade])
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
return (
<button
type="button"
className="group flex h-8 w-full cursor-pointer items-center gap-1 rounded-lg border-none bg-components-input-bg-normal px-2 py-1 text-left shadow-none hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={onClick}
>
<span
aria-hidden
className={cn(
'i-ri-time-line size-4 shrink-0 text-text-tertiary',
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
)}
/>
<span className="min-w-0 flex-1 p-1 system-sm-regular text-components-input-text-filled">
{inputElem}
</span>
<span className="shrink-0 pr-0.5 system-sm-regular text-text-tertiary">
{convertTimezoneToOffsetStr(timezone)}
</span>
</button>
)
}, [timezone])
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogTrigger
render={(
<Button
variant="secondary"
className="h-8 gap-0.5 px-3 system-sm-medium"
disabled={disabled}
>
<span aria-hidden className="i-custom-vender-system-auto-update-line size-4" />
<span className="px-0.5">{t('autoUpdate.autoUpdate', { ns: 'plugin' })}</span>
{selectedStrategyLabel && (
<span className="flex min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{selectedStrategyLabel}
</span>
)}
</Button>
)}
/>
{!disabled && (
<DialogContent
className="flex w-[480px] max-w-[calc(100vw-32px)] flex-col overflow-hidden! rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0! text-left align-middle shadow-xl"
>
<div className="relative flex w-full items-start gap-2 px-6 pt-6 pr-14 pb-3">
<DialogTitle className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
{t('autoUpdate.autoUpdateSettings', { ns: 'plugin' })}
</DialogTitle>
<DialogCloseButton className="top-5 right-5 size-8 rounded-lg" />
</div>
{isSettingsLoading && (
<div
role="status"
className="flex min-h-[260px] items-center justify-center gap-2 px-6 py-6 system-sm-regular text-text-tertiary"
>
<span aria-hidden className="i-ri-loader-2-line size-4 animate-spin motion-reduce:animate-none" />
<span>{t('loading', { ns: 'common' })}</span>
</div>
)}
{!isSettingsLoading && !hasSettings && (
<div className="flex min-h-[260px] items-center justify-center px-6 py-6 text-center system-sm-regular text-text-tertiary">
{t('api.actionFailed', { ns: 'common' })}
</div>
)}
{!isSettingsLoading && hasSettings && autoUpgrade && (
<>
<UpdateSettingDialogForm
autoUpgrade={autoUpgrade}
category={category}
plugins={plugins}
scopeOptions={scopeOptions}
strategyOptions={strategyOptions}
timezone={timezone}
updateTimeValue={updateTimeValue}
minuteFilter={minuteFilter}
onAutoUpgradeChange={updateAutoUpgrade}
onPluginsChange={handlePluginsChange}
onRequestClose={handleCancel}
onUpdateTimeChange={handleUpdateTimeChange}
renderTimePickerTrigger={renderTimePickerTrigger}
/>
<div className="flex h-[76px] items-center justify-end gap-2 px-6 pt-5 pb-6">
<Button
variant="secondary"
className="min-w-[72px]"
onClick={handleCancel}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
className="min-w-[72px]"
onClick={handleSave}
disabled={isSavePending}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</>
)}
</DialogContent>
)}
</Dialog>
)
}
export default UpdateSettingDialog