diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx new file mode 100644 index 0000000000..5ec0e4fbe2 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -0,0 +1,152 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { useBoolean } from 'ahooks' +import { + RiBugLine, + RiCloseLine, + RiHardDrive3Line, + RiVerifiedBadgeLine, +} from '@remixicon/react' +import type { PluginDetail } from '../types' +import { PluginSource } from '../types' +import Description from '../card/base/description' +import Icon from '../card/base/card-icon' +import Title from '../card/base/title' +import OrgInfo from '../card/base/org-info' +import OperationDropdown from './operation-dropdown' +import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info' +import ActionButton from '@/app/components/base/action-button' +import Button from '@/app/components/base/button' +import Badge from '@/app/components/base/badge' +import Confirm from '@/app/components/base/confirm' +import Tooltip from '@/app/components/base/tooltip' +import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' +import { Github } from '@/app/components/base/icons/src/public/common' +import I18n from '@/context/i18n' +import cn from '@/utils/classnames' + +const i18nPrefix = 'plugin.action' + +type Props = { + detail: PluginDetail + onHide: () => void + onDelete: () => void +} + +const DetailHeader = ({ + detail, + onHide, + onDelete, +}: Props) => { + const { t } = useTranslation() + const { locale } = useContext(I18n) + + const hasNewVersion = useMemo(() => { + if (!detail) + return false + return false + // return pluginDetail.latest_version !== pluginDetail.version + }, [detail]) + + const handleUpdate = () => {} + + const [isShowPluginInfo, { + setTrue: showPluginInfo, + setFalse: hidePluginInfo, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const usedInApps = 3 + + return ( +
+
+ +
+
+ + {detail.declaration.verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} + <Badge + className='mx-1' + text={detail.version} + hasRedCornerMark={hasNewVersion} + /> + {hasNewVersion && ( + <Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button> + )} + </div> + <div className='mb-1 flex justify-between items-center h-4'> + <div className='flex items-center'> + <OrgInfo + className="mt-0.5" + packageNameClassName='w-auto' + orgName={detail.declaration.author} + packageName={detail.declaration.name} + /> + <div className='ml-1 mr-0.5 text-text-quaternary system-xs-regular'>·</div> + {detail.source === PluginSource.marketplace && ( + <Tooltip popupContent={t('plugin.detailPanel.categoryTip.marketplace')} > + <BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary hover:text-text-accent' /> + </Tooltip> + )} + {detail.source === PluginSource.github && ( + <Tooltip popupContent={t('plugin.detailPanel.categoryTip.github')} > + <Github className='w-3.5 h-3.5 text-text-secondary hover:text-text-primary' /> + </Tooltip> + )} + {detail.source === PluginSource.local && ( + <Tooltip popupContent={t('plugin.detailPanel.categoryTip.local')} > + <RiHardDrive3Line className='w-3.5 h-3.5 text-text-tertiary' /> + </Tooltip> + )} + {detail.source === PluginSource.debugging && ( + <Tooltip popupContent={t('plugin.detailPanel.categoryTip.debugging')} > + <RiBugLine className='w-3.5 h-3.5 text-text-tertiary hover:text-text-warning' /> + </Tooltip> + )} + </div> + </div> + </div> + <div className='flex gap-1'> + <OperationDropdown + onInfo={showPluginInfo} + onRemove={showDeleteConfirm} + /> + <ActionButton onClick={onHide}> + <RiCloseLine className='w-4 h-4' /> + </ActionButton> + </div> + </div> + <Description className='mt-3' text={detail.declaration.description[locale]} descriptionLineRows={2}></Description> + {isShowPluginInfo && ( + <PluginInfo + repository={detail.meta?.repo} + release={detail.version} + packageName={detail.meta?.package} + onHide={hidePluginInfo} + /> + )} + {isShowDeleteConfirm && ( + <Confirm + isShow + title={t(`${i18nPrefix}.delete`)} + content={ + <div> + {t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{detail.declaration.label[locale]}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br /> + {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} + </div> + } + onCancel={hideDeleteConfirm} + onConfirm={onDelete} + /> + )} + </div> + ) +} + +export default DetailHeader diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index aab5a93f66..0fd9570137 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -1,33 +1,14 @@ 'use client' -import React, { useMemo } from 'react' +import React from 'react' import type { FC } from 'react' -import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' -import { - RiBugLine, - RiCloseLine, - RiHardDrive3Line, - RiVerifiedBadgeLine, -} from '@remixicon/react' import type { PluginDetail } from '../types' -import { PluginSource } from '../types' -import Description from '../card/base/description' -import Icon from '../card/base/card-icon' -import Title from '../card/base/title' -import OrgInfo from '../card/base/org-info' -import OperationDropdown from './operation-dropdown' +import DetailHeader from './detail-header' import EndpointList from './endpoint-list' import ActionList from './action-list' import ModelList from './model-list' -import Badge from '@/app/components/base/badge' -import Tooltip from '@/app/components/base/tooltip' -import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' -import { Github } from '@/app/components/base/icons/src/public/common' -import Button from '@/app/components/base/button' -import ActionButton from '@/app/components/base/action-button' import Drawer from '@/app/components/base/drawer' // import Loading from '@/app/components/base/loading' -import I18n from '@/context/i18n' import cn from '@/utils/classnames' type Props = { @@ -40,16 +21,8 @@ const PluginDetailPanel: FC<Props> = ({ onHide, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) - const hasNewVersion = useMemo(() => { - if (!pluginDetail) - return false - return false // TODO - // return pluginDetail.latest_version !== pluginDetail.version - }, [pluginDetail]) - - const handleUpdate = () => {} + const handleDelete = () => {} if (!pluginDetail) return null @@ -67,63 +40,11 @@ const PluginDetailPanel: FC<Props> = ({ {/* {loading && <Loading type='area' />} */} {pluginDetail && ( <> - <div className={cn('shrink-0 p-4 pb-3 border-b border-divider-subtle bg-components-panel-bg')}> - <div className="flex"> - <Icon src={pluginDetail.declaration.icon} /> - <div className="ml-3 w-0 grow"> - <div className="flex items-center h-5"> - <Title title={pluginDetail.declaration.label[locale]} /> - {pluginDetail.declaration.verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} - <Badge - className='mx-1' - text={pluginDetail.version} - hasRedCornerMark={hasNewVersion} - /> - {hasNewVersion && ( - <Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button> - )} - </div> - <div className='mb-1 flex justify-between items-center h-4'> - <div className='flex items-center'> - <OrgInfo - className="mt-0.5" - packageNameClassName='w-auto' - orgName={pluginDetail.declaration.author} - packageName={pluginDetail.declaration.name} - /> - <div className='ml-1 mr-0.5 text-text-quaternary system-xs-regular'>·</div> - {pluginDetail.source === PluginSource.marketplace && ( - <Tooltip popupContent={t('plugin.detailPanel.categoryTip.marketplace')} > - <BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary hover:text-text-accent' /> - </Tooltip> - )} - {pluginDetail.source === PluginSource.github && ( - <Tooltip popupContent={t('plugin.detailPanel.categoryTip.github')} > - <Github className='w-3.5 h-3.5 text-text-secondary hover:text-text-primary' /> - </Tooltip> - )} - {pluginDetail.source === PluginSource.local && ( - <Tooltip popupContent={t('plugin.detailPanel.categoryTip.local')} > - <RiHardDrive3Line className='w-3.5 h-3.5 text-text-tertiary' /> - </Tooltip> - )} - {pluginDetail.source === PluginSource.debugging && ( - <Tooltip popupContent={t('plugin.detailPanel.categoryTip.debugging')} > - <RiBugLine className='w-3.5 h-3.5 text-text-tertiary hover:text-text-warning' /> - </Tooltip> - )} - </div> - </div> - </div> - <div className='flex gap-1'> - <OperationDropdown /> - <ActionButton onClick={onHide}> - <RiCloseLine className='w-4 h-4' /> - </ActionButton> - </div> - </div> - <Description className='mt-3' text={pluginDetail.declaration.description[locale]} descriptionLineRows={2}></Description> - </div> + <DetailHeader + detail={pluginDetail} + onHide={onHide} + onDelete={handleDelete} + /> <div className='grow overflow-y-auto'> <ActionList /> <EndpointList /> diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index c9be924a65..e8186d1958 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -13,9 +13,14 @@ import { import cn from '@/utils/classnames' type Props = { + onInfo: () => void + onRemove: () => void } -const OperationDropdown: FC<Props> = () => { +const OperationDropdown: FC<Props> = ({ + onInfo, + onRemove, +}) => { const { t } = useTranslation() const [open, doSetOpen] = useState(false) const openRef = useRef(open) @@ -45,14 +50,16 @@ const OperationDropdown: FC<Props> = () => { </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-50'> <div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'> - <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div> + <div onClick={onInfo} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div> + {/* ##plugin TODO## check update */} <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.checkUpdate')}</div> + {/* ##plugin TODO## router action */} <div className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'> <div className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</div> <RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' /> </div> <div className='my-1 h-px bg-divider-subtle'></div> - <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div> + <div onClick={onRemove} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div> </div> </PortalToFollowElemContent> </PortalToFollowElem>