Merge branch 'feat/plugin-auto-upgrade-fe' into deploy/dev
commit
b8d895a030
@ -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
File diff suppressed because one or more lines are too long
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,136 @@
|
||||
'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 { 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 { dayjsToTimeOfDay, timeOfDayToDayjs } from './utils'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate'
|
||||
|
||||
type Props = {
|
||||
payload: AutoUpdateConfig
|
||||
onChange: (payload: AutoUpdateConfig) => void
|
||||
}
|
||||
|
||||
const AutoUpdateSetting: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
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])
|
||||
return (
|
||||
<div className='self-stretch px-6'>
|
||||
<div className='my-3 flex items-center'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>Updates Settings</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`)} />
|
||||
<TimePicker
|
||||
value={timeOfDayToDayjs(upgrade_time_of_day)}
|
||||
onChange={v => handleChange('upgrade_time_of_day')(dayjsToTimeOfDay(v))}
|
||||
onClear={() => handleChange('upgrade_time_of_day')(0)}
|
||||
popupClassName='z-[99]'
|
||||
title={t(`${i18nPrefix}.updateTime`)}
|
||||
minuteFilter={minuteFilter}
|
||||
/>
|
||||
</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,14 @@
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
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 dayjsToTimeOfDay = (date?: Dayjs): number => {
|
||||
if(!date) return 0
|
||||
return date.hour() * 3600 + date.minute() * 60
|
||||
}
|
||||
@ -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