Compare commits
39 Commits
main
...
feat/plugi
| Author | SHA1 | Date |
|---|---|---|
|
|
7608eb1049 | 9 months ago |
|
|
95ce7b6f47 | 9 months ago |
|
|
784a236280 | 9 months ago |
|
|
1e0426ca6f | 9 months ago |
|
|
fd7396d8f9 | 9 months ago |
|
|
a0af33e945 | 9 months ago |
|
|
8d8220b06c | 9 months ago |
|
|
0625d6a361 | 9 months ago |
|
|
63a1a1077e | 9 months ago |
|
|
0af646d947 | 9 months ago |
|
|
07c99745fa | 9 months ago |
|
|
afd0d31354 | 9 months ago |
|
|
18bbf1165d | 9 months ago |
|
|
5f17edc77f | 9 months ago |
|
|
836027cb33 | 9 months ago |
|
|
f3cbfe2223 | 9 months ago |
|
|
bc1e4c88e0 | 9 months ago |
|
|
d114485abd | 9 months ago |
|
|
3e8a4a66fe | 9 months ago |
|
|
4c583f3d9a | 9 months ago |
|
|
52b845a5bb | 9 months ago |
|
|
38d1c85c57 | 9 months ago |
|
|
c43d992f2b | 9 months ago |
|
|
1ff5969b92 | 9 months ago |
|
|
93a560ee54 | 9 months ago |
|
|
2f241d932c | 9 months ago |
|
|
a0804786fd | 9 months ago |
|
|
c6fa8102eb | 9 months ago |
|
|
7ec5816513 | 9 months ago |
|
|
825fbcc6f8 | 9 months ago |
|
|
ccef71626d | 9 months ago |
|
|
29cac85b12 | 9 months ago |
|
|
8b290ac7a1 | 9 months ago |
|
|
01cdffaa08 | 9 months ago |
|
|
3061280f7a | 9 months ago |
|
|
bc75d810c4 | 9 months ago |
|
|
dc5e974a78 | 9 months ago |
|
|
baff25c160 | 9 months ago |
|
|
42b6524954 | 10 months ago |
@ -0,0 +1,7 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 823 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3308 16H14.2915L13.6249 13.9476H10.3761L9.70846 16H7.66918L10.7759 7H13.2281L16.3308 16ZM10.8595 12.4622H13.1435L12.0378 9.05639H11.9673L10.8595 12.4622Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 772 B |
File diff suppressed because one or more lines are too long
@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "32",
|
||||||
|
"height": "32",
|
||||||
|
"viewBox": "0 0 32 32",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z",
|
||||||
|
"stroke": "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M4.00488 16H6.67155",
|
||||||
|
"stroke": "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M4.00488 9.33334H8.00488",
|
||||||
|
"stroke": "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M4.00488 22.6667H8.00488",
|
||||||
|
"stroke": "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M26 22L29.3333 25.3333",
|
||||||
|
"stroke": "currentColor",
|
||||||
|
"stroke-width": "2",
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "SearchMenu"
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './SearchMenu.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = (
|
||||||
|
{
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}: React.SVGProps<SVGSVGElement> & {
|
||||||
|
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||||
|
},
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||||
|
|
||||||
|
Icon.displayName = 'SearchMenu'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "24",
|
||||||
|
"height": "24",
|
||||||
|
"viewBox": "0 0 24 24",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M16.3308 16H14.2915L13.6249 13.9476H10.3761L9.70846 16H7.66918L10.7759 7H13.2281L16.3308 16ZM10.8595 12.4622H13.1435L12.0378 9.05639H11.9673L10.8595 12.4622Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "AutoUpdateLine"
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './AutoUpdateLine.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = (
|
||||||
|
{
|
||||||
|
ref,
|
||||||
|
...props
|
||||||
|
}: React.SVGProps<SVGSVGElement> & {
|
||||||
|
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
|
||||||
|
},
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||||
|
|
||||||
|
Icon.displayName = 'AutoUpdateLine'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as AutoUpdateLine } from './AutoUpdateLine'
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import type { AutoUpdateConfig } from './types'
|
||||||
|
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from './types'
|
||||||
|
export const defaultValue: AutoUpdateConfig = {
|
||||||
|
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
|
||||||
|
upgrade_time_of_day: 0,
|
||||||
|
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||||
|
exclude_plugins: [],
|
||||||
|
include_plugins: [],
|
||||||
|
}
|
||||||
@ -0,0 +1,185 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY, type AutoUpdateConfig } from './types'
|
||||||
|
import Label from '../label'
|
||||||
|
import StrategyPicker from './strategy-picker'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||||
|
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||||
|
import PluginsPicker from './plugins-picker'
|
||||||
|
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from './utils'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||||
|
import { RiTimeLine } from '@remixicon/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||||
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
|
|
||||||
|
const i18nPrefix = 'plugin.autoUpdate'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
payload: AutoUpdateConfig
|
||||||
|
onChange: (payload: AutoUpdateConfig) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingTimeZone: FC<{
|
||||||
|
children?: React.ReactNode
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||||
|
return (
|
||||||
|
<span className='body-xs-regular cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'language' })} >{children}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const AutoUpdateSetting: FC<Props> = ({
|
||||||
|
payload,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { userProfile: { timezone } } = useAppContext()
|
||||||
|
|
||||||
|
const {
|
||||||
|
strategy_setting,
|
||||||
|
upgrade_time_of_day,
|
||||||
|
upgrade_mode,
|
||||||
|
exclude_plugins,
|
||||||
|
include_plugins,
|
||||||
|
} = payload
|
||||||
|
|
||||||
|
const minuteFilter = useCallback((minutes: string[]) => {
|
||||||
|
return minutes.filter((m) => {
|
||||||
|
const time = Number.parseInt(m, 10)
|
||||||
|
return time % 15 === 0
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const strategyDescription = useMemo(() => {
|
||||||
|
switch (strategy_setting) {
|
||||||
|
case AUTO_UPDATE_STRATEGY.fixOnly:
|
||||||
|
return t(`${i18nPrefix}.strategy.fixOnly.selectedDescription`)
|
||||||
|
case AUTO_UPDATE_STRATEGY.latest:
|
||||||
|
return t(`${i18nPrefix}.strategy.latest.selectedDescription`)
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}, [strategy_setting, t])
|
||||||
|
|
||||||
|
const plugins = useMemo(() => {
|
||||||
|
switch (upgrade_mode) {
|
||||||
|
case AUTO_UPDATE_MODE.partial:
|
||||||
|
return include_plugins
|
||||||
|
case AUTO_UPDATE_MODE.exclude:
|
||||||
|
return exclude_plugins
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}, [upgrade_mode, exclude_plugins, include_plugins])
|
||||||
|
|
||||||
|
const handlePluginsChange = useCallback((newPlugins: string[]) => {
|
||||||
|
if (upgrade_mode === AUTO_UPDATE_MODE.partial) {
|
||||||
|
onChange({
|
||||||
|
...payload,
|
||||||
|
include_plugins: newPlugins,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (upgrade_mode === AUTO_UPDATE_MODE.exclude) {
|
||||||
|
onChange({
|
||||||
|
...payload,
|
||||||
|
exclude_plugins: newPlugins,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [payload, upgrade_mode, onChange])
|
||||||
|
const handleChange = useCallback((key: keyof AutoUpdateConfig) => {
|
||||||
|
return (value: AutoUpdateConfig[keyof AutoUpdateConfig]) => {
|
||||||
|
onChange({
|
||||||
|
...payload,
|
||||||
|
[key]: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [payload, onChange])
|
||||||
|
|
||||||
|
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className='flex w-0 grow items-center gap-x-1'>
|
||||||
|
<RiTimeLine className={cn(
|
||||||
|
'h-4 w-4 shrink-0 text-text-tertiary',
|
||||||
|
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||||
|
)} />
|
||||||
|
{inputElem}
|
||||||
|
</div>
|
||||||
|
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [timezone])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='self-stretch px-6'>
|
||||||
|
<div className='my-3 flex items-center'>
|
||||||
|
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.updateSettings`)}</div>
|
||||||
|
<div className='ml-2 h-px grow bg-divider-subtle'></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<Label label={t(`${i18nPrefix}.automaticUpdates`)} description={strategyDescription} />
|
||||||
|
<StrategyPicker value={strategy_setting} onChange={handleChange('strategy_setting')} />
|
||||||
|
</div>
|
||||||
|
{strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
|
||||||
|
<>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<Label label={t(`${i18nPrefix}.updateTime`)} />
|
||||||
|
<div className='flex flex-col justify-start'>
|
||||||
|
<TimePicker
|
||||||
|
value={timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(upgrade_time_of_day, timezone!))}
|
||||||
|
timezone={timezone}
|
||||||
|
onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))}
|
||||||
|
onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))}
|
||||||
|
popupClassName='z-[99]'
|
||||||
|
title={t(`${i18nPrefix}.updateTime`)}
|
||||||
|
minuteFilter={minuteFilter}
|
||||||
|
renderTrigger={renderTimePickerTrigger}
|
||||||
|
/>
|
||||||
|
<div className='body-xs-regular mt-1 text-right text-text-tertiary'>
|
||||||
|
<Trans
|
||||||
|
i18nKey={`${i18nPrefix}.changeTimezone`}
|
||||||
|
components={{
|
||||||
|
setTimezone: <SettingTimeZone />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label label={t(`${i18nPrefix}.specifyPluginsToUpdate`)} />
|
||||||
|
<div className='mt-1 flex w-full items-start justify-between gap-2'>
|
||||||
|
{[AUTO_UPDATE_MODE.update_all, AUTO_UPDATE_MODE.exclude, AUTO_UPDATE_MODE.partial].map(option => (
|
||||||
|
<OptionCard
|
||||||
|
key={option}
|
||||||
|
title={t(`${i18nPrefix}.upgradeMode.${option}`)}
|
||||||
|
onSelect={() => handleChange('upgrade_mode')(option)}
|
||||||
|
selected={upgrade_mode === option}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
|
||||||
|
<PluginsPicker
|
||||||
|
value={plugins}
|
||||||
|
onChange={handlePluginsChange}
|
||||||
|
updateMode={upgrade_mode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(AutoUpdateSetting)
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||||
|
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className: string
|
||||||
|
noPlugins?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoDataPlaceholder: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
noPlugins,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const icon = noPlugins ? (<Group className='size-6 text-text-quaternary' />) : (<SearchMenu className='size-8 text-text-tertiary' />)
|
||||||
|
const text = t(`plugin.autoUpdate.noPluginPlaceholder.${noPlugins ? 'noInstalled' : 'noFound'}`)
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center justify-center', className)}>
|
||||||
|
<div className='flex flex-col items-center'>
|
||||||
|
{icon}
|
||||||
|
<div className='system-sm-regular mt-2 text-text-tertiary'>{text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(NoDataPlaceholder)
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { AUTO_UPDATE_MODE } from './types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
updateMode: AUTO_UPDATE_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoPluginSelected: FC<Props> = ({
|
||||||
|
updateMode,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const text = `${t(`plugin.autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`)}`
|
||||||
|
return (
|
||||||
|
<div className='system-xs-regular rounded-[10px] border border-[divider-subtle] bg-background-section p-3 text-center text-text-tertiary'>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(NoPluginSelected)
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import NoPluginSelected from './no-plugin-selected'
|
||||||
|
import { AUTO_UPDATE_MODE } from './types'
|
||||||
|
import PluginsSelected from './plugins-selected'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { RiAddLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
|
import ToolPicker from './tool-picker'
|
||||||
|
|
||||||
|
const i18nPrefix = 'plugin.autoUpdate'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
updateMode: AUTO_UPDATE_MODE
|
||||||
|
value: string[] // plugin ids
|
||||||
|
onChange: (value: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginsPicker: FC<Props> = ({
|
||||||
|
updateMode,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const hasSelected = value.length > 0
|
||||||
|
const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude
|
||||||
|
const handleClear = () => {
|
||||||
|
onChange([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isShowToolPicker, {
|
||||||
|
set: setToolPicker,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
return (
|
||||||
|
<div className='mt-2 rounded-[10px] bg-background-section-burn p-2.5'>
|
||||||
|
{hasSelected ? (
|
||||||
|
<div className='flex justify-between text-text-tertiary'>
|
||||||
|
<div className='system-xs-medium'>{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { num: value.length })}</div>
|
||||||
|
<div className='system-xs-medium cursor-pointer' onClick={handleClear}>{t(`${i18nPrefix}.operation.clearAll`)}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NoPluginSelected updateMode={updateMode} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasSelected && (
|
||||||
|
<PluginsSelected
|
||||||
|
className='mt-2'
|
||||||
|
plugins={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ToolPicker
|
||||||
|
trigger={
|
||||||
|
<Button className='mt-2 w-[412px]' size='small' variant='secondary-accent'>
|
||||||
|
<RiAddLine className='size-3.5' />
|
||||||
|
{t(`${i18nPrefix}.operation.select`)}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
isShow={isShowToolPicker}
|
||||||
|
onShowChange={setToolPicker}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(PluginsPicker)
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||||
|
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||||
|
|
||||||
|
const MAX_DISPLAY_COUNT = 14
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
plugins: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginsSelected: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
plugins,
|
||||||
|
}) => {
|
||||||
|
const isShowAll = plugins.length < MAX_DISPLAY_COUNT
|
||||||
|
const displayPlugins = plugins.slice(0, MAX_DISPLAY_COUNT)
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center space-x-1', className)}>
|
||||||
|
{displayPlugins.map(plugin => (
|
||||||
|
<Icon key={plugin} size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} />
|
||||||
|
))}
|
||||||
|
{!isShowAll && <div className='system-xs-medium text-text-tertiary'>+{plugins.length - MAX_DISPLAY_COUNT}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(PluginsSelected)
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiCheckLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { AUTO_UPDATE_STRATEGY } from './types'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
const i18nPrefix = 'plugin.autoUpdate.strategy'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: AUTO_UPDATE_STRATEGY
|
||||||
|
onChange: (value: AUTO_UPDATE_STRATEGY) => void
|
||||||
|
}
|
||||||
|
const StrategyPicker = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
value: AUTO_UPDATE_STRATEGY.disabled,
|
||||||
|
label: t(`${i18nPrefix}.disabled.name`),
|
||||||
|
description: t(`${i18nPrefix}.disabled.description`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||||
|
label: t(`${i18nPrefix}.fixOnly.name`),
|
||||||
|
description: t(`${i18nPrefix}.fixOnly.description`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AUTO_UPDATE_STRATEGY.latest,
|
||||||
|
label: t(`${i18nPrefix}.latest.name`),
|
||||||
|
description: t(`${i18nPrefix}.latest.description`),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const selectedOption = options.find(option => option.value === value)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
placement='top-end'
|
||||||
|
offset={4}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
setOpen(v => !v)
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
>
|
||||||
|
{selectedOption?.label}
|
||||||
|
<RiArrowDownSLine className='h-3.5 w-3.5' />
|
||||||
|
</Button>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[99]'>
|
||||||
|
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||||
|
{
|
||||||
|
options.map(option => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopImmediatePropagation()
|
||||||
|
onChange(option.value)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mr-1 w-4 shrink-0'>
|
||||||
|
{
|
||||||
|
value === option.value && (
|
||||||
|
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='grow'>
|
||||||
|
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
|
||||||
|
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StrategyPicker
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||||
|
import { renderI18nObject } from '@/i18n'
|
||||||
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
|
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||||
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
payload: PluginDetail
|
||||||
|
isChecked?: boolean
|
||||||
|
onCheckChange: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolItem: FC<Props> = ({
|
||||||
|
payload,
|
||||||
|
isChecked,
|
||||||
|
onCheckChange,
|
||||||
|
}) => {
|
||||||
|
const language = useGetLanguage()
|
||||||
|
|
||||||
|
const { plugin_id, declaration } = payload
|
||||||
|
const { label, author: org } = declaration
|
||||||
|
return (
|
||||||
|
<div className='p-1'>
|
||||||
|
<div
|
||||||
|
className='flex w-full select-none items-center rounded-lg pr-2 hover:bg-state-base-hover'
|
||||||
|
>
|
||||||
|
<div className='flex h-8 grow items-center space-x-2 pl-3 pr-2'>
|
||||||
|
<Icon size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} />
|
||||||
|
<div className='system-sm-medium max-w-[150px] shrink-0 truncate text-text-primary'>{renderI18nObject(label, language)}</div>
|
||||||
|
<div className='system-xs-regular max-w-[150px] shrink-0 truncate text-text-quaternary'>{org}</div>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
checked={isChecked}
|
||||||
|
onCheck={onCheckChange}
|
||||||
|
className='shrink-0'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(ToolItem)
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||||
|
import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch'
|
||||||
|
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import ToolItem from './tool-item'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import NoDataPlaceholder from './no-data-placeholder'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
trigger: React.ReactNode
|
||||||
|
value: string[]
|
||||||
|
onChange: (value: string[]) => void
|
||||||
|
isShow: boolean
|
||||||
|
onShowChange: (isShow: boolean) => void
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolPicker: FC<Props> = ({
|
||||||
|
trigger,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
isShow,
|
||||||
|
onShowChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const toggleShowPopup = useCallback(() => {
|
||||||
|
onShowChange(!isShow)
|
||||||
|
}, [onShowChange, isShow])
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.all,
|
||||||
|
name: t('plugin.category.all'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.model,
|
||||||
|
name: t('plugin.category.models'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.tool,
|
||||||
|
name: t('plugin.category.tools'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.agent,
|
||||||
|
name: t('plugin.category.agents'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.extension,
|
||||||
|
name: t('plugin.category.extensions'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: PLUGIN_TYPE_SEARCH_MAP.bundle,
|
||||||
|
name: t('plugin.category.bundles'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [tags, setTags] = useState<string[]>([])
|
||||||
|
const { data, isLoading } = useInstalledPluginList()
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
const list = data ? data.plugins : []
|
||||||
|
return list.filter((plugin) => {
|
||||||
|
return (
|
||||||
|
(pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
|
||||||
|
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
|
||||||
|
&& (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [data, pluginType, query, tags])
|
||||||
|
const handleCheckChange = useCallback((pluginId: string) => {
|
||||||
|
return () => {
|
||||||
|
const newValue = value.includes(pluginId)
|
||||||
|
? value.filter(id => id !== pluginId)
|
||||||
|
: [...value, pluginId]
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
}, [onChange, value])
|
||||||
|
|
||||||
|
const listContent = (
|
||||||
|
<div className='max-h-[396px] overflow-y-auto'>
|
||||||
|
{filteredList.map(item => (
|
||||||
|
<ToolItem
|
||||||
|
key={item.plugin_id}
|
||||||
|
payload={item}
|
||||||
|
isChecked={value.includes(item.plugin_id)}
|
||||||
|
onCheckChange={handleCheckChange(item.plugin_id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadingContent = (
|
||||||
|
<div className='flex h-[396px] items-center justify-center'>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const noData = (
|
||||||
|
<NoDataPlaceholder className='h-[396px]' noPlugins={!query} />
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
placement='top'
|
||||||
|
offset={0}
|
||||||
|
open={isShow}
|
||||||
|
onOpenChange={onShowChange}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger
|
||||||
|
onClick={toggleShowPopup}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-[1000]'>
|
||||||
|
<div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}>
|
||||||
|
<div className='p-2 pb-1'>
|
||||||
|
<SearchBox
|
||||||
|
search={query}
|
||||||
|
onSearchChange={setQuery}
|
||||||
|
tags={tags}
|
||||||
|
onTagsChange={setTags}
|
||||||
|
size='small'
|
||||||
|
placeholder={t('plugin.searchTools')!}
|
||||||
|
inputClassName='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
|
||||||
|
<div className='flex h-8 items-center space-x-1'>
|
||||||
|
{
|
||||||
|
tabs.map(tab => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
|
||||||
|
'text-xs font-medium text-text-secondary',
|
||||||
|
pluginType === tab.key && 'bg-state-base-hover-alt',
|
||||||
|
)}
|
||||||
|
key={tab.key}
|
||||||
|
onClick={() => setPluginType(tab.key)}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isLoading && filteredList.length > 0 && listContent}
|
||||||
|
{!isLoading && filteredList.length === 0 && noData}
|
||||||
|
{isLoading && loadingContent}
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ToolPicker)
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
export enum AUTO_UPDATE_STRATEGY {
|
||||||
|
fixOnly = 'fix_only',
|
||||||
|
disabled = 'disabled',
|
||||||
|
latest = 'latest',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AUTO_UPDATE_MODE {
|
||||||
|
partial = 'partial',
|
||||||
|
exclude = 'exclude',
|
||||||
|
update_all = 'all',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AutoUpdateConfig = {
|
||||||
|
strategy_setting: AUTO_UPDATE_STRATEGY
|
||||||
|
upgrade_time_of_day: number
|
||||||
|
upgrade_mode: AUTO_UPDATE_MODE
|
||||||
|
exclude_plugins: string[]
|
||||||
|
include_plugins: string[]
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
// write unit test for convertLocalSecondsToUTCDaySeconds
|
||||||
|
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds } from './utils'
|
||||||
|
|
||||||
|
describe('convertLocalSecondsToUTCDaySeconds', () => {
|
||||||
|
it('should convert local seconds to UTC day seconds correctly', () => {
|
||||||
|
const localTimezone = 'Asia/Shanghai'
|
||||||
|
const utcSeconds = convertLocalSecondsToUTCDaySeconds(0, localTimezone)
|
||||||
|
expect(utcSeconds).toBe((24 - 8) * 3600)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert local seconds to UTC day seconds for a specific time', () => {
|
||||||
|
const localTimezone = 'Asia/Shanghai'
|
||||||
|
expect(convertUTCDaySecondsToLocalSeconds(convertLocalSecondsToUTCDaySeconds(0, localTimezone), localTimezone)).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
export const timeOfDayToDayjs = (timeOfDay: number): Dayjs => {
|
||||||
|
const hours = Math.floor(timeOfDay / 3600)
|
||||||
|
const minutes = (timeOfDay - hours * 3600) / 60
|
||||||
|
const res = dayjs().startOf('day').hour(hours).minute(minutes)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertLocalSecondsToUTCDaySeconds = (secondsInDay: number, localTimezone: string): number => {
|
||||||
|
const localDayStart = dayjs().tz(localTimezone).startOf('day')
|
||||||
|
const localTargetTime = localDayStart.add(secondsInDay, 'second')
|
||||||
|
const utcTargetTime = localTargetTime.utc()
|
||||||
|
const utcDayStart = utcTargetTime.startOf('day')
|
||||||
|
const secondsFromUTCMidnight = utcTargetTime.diff(utcDayStart, 'second')
|
||||||
|
return secondsFromUTCMidnight
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dayjsToTimeOfDay = (date?: Dayjs): number => {
|
||||||
|
if(!date) return 0
|
||||||
|
return date.hour() * 3600 + date.minute() * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => {
|
||||||
|
const utcDayStart = dayjs().utc().startOf('day')
|
||||||
|
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
|
||||||
|
const localTargetTime = utcTargetTime.tz(localTimezone)
|
||||||
|
const localDayStart = localTargetTime.startOf('day')
|
||||||
|
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
|
||||||
|
return secondsInLocalDay
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Label: FC<Props> = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={cn('flex h-6 items-center', description && 'h-4')}>
|
||||||
|
<span className='system-sm-semibold text-text-secondary'>{label}</span>
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<div className='body-xs-regular mt-1 text-text-tertiary'>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(Label)
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
|
||||||
|
const i18nPrefix = 'plugin.autoUpdate.pluginDowngradeWarning'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCancel: () => void
|
||||||
|
onJustDowngrade: () => void
|
||||||
|
onExcludeAndDowngrade: () => void
|
||||||
|
}
|
||||||
|
const DowngradeWarningModal = ({
|
||||||
|
onCancel,
|
||||||
|
onJustDowngrade,
|
||||||
|
onExcludeAndDowngrade,
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex flex-col items-start gap-2 self-stretch'>
|
||||||
|
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
|
||||||
|
<div className='system-md-regular text-text-secondary'>
|
||||||
|
{t(`${i18nPrefix}.description`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-9 flex items-start justify-end space-x-2 self-stretch'>
|
||||||
|
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||||
|
<Button variant='secondary' destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`)}</Button>
|
||||||
|
<Button variant='primary' onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`)}</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DowngradeWarningModal
|
||||||
Loading…
Reference in New Issue