feat(ui): unify tag editing in app sidebar and add management entry to TagFilter (#23325)
This commit is contained in:
@@ -20,12 +20,18 @@ import cn from '@/utils/classnames'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
import { fetchAppDetail, fetchAppWithTags } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { App } from '@/types/app'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@@ -48,6 +54,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
setAppDetail: state.setAppDetail,
|
||||
setAppSiderbarExpand: state.setAppSiderbarExpand,
|
||||
})))
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
||||
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
||||
const [navigation, setNavigation] = useState<Array<{
|
||||
@@ -111,7 +118,17 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
useEffect(() => {
|
||||
setAppDetail()
|
||||
setIsLoadingAppDetail(true)
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => {
|
||||
if (!res.tags || res.tags.length === 0) {
|
||||
try {
|
||||
const appWithTags = await fetchAppWithTags(appId)
|
||||
if (appWithTags?.tags)
|
||||
res.tags = appWithTags.tags
|
||||
}
|
||||
catch (error) {
|
||||
// Fallback failed, continue with empty tags
|
||||
}
|
||||
}
|
||||
setAppDetailRes(res)
|
||||
}).catch((e: any) => {
|
||||
if (e.status === 404)
|
||||
@@ -163,6 +180,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
<div className="grow overflow-hidden bg-components-panel-bg">
|
||||
{children}
|
||||
</div>
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='app' show={showTagManagementModal} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
@@ -18,6 +18,8 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
@@ -73,6 +75,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
|
||||
const [tags, setTags] = useState<Tag[]>(appDetail?.tags || [])
|
||||
useEffect(() => {
|
||||
setTags(appDetail?.tags || [])
|
||||
}, [appDetail?.tags])
|
||||
|
||||
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
icon_type,
|
||||
@@ -303,8 +310,35 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<div className='flex w-full grow flex-col items-start justify-center'>
|
||||
<div className='system-md-semibold w-full truncate text-text-secondary'>{appDetail.name}</div>
|
||||
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className='flex min-w-0 flex-1 flex-col'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
|
||||
{isCurrentWorkspaceEditor && (
|
||||
<div className='flex w-0 grow items-center' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}>
|
||||
<div className='w-full'>
|
||||
<TagSelector
|
||||
position='br'
|
||||
type='app'
|
||||
targetID={appDetail.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onChange={() => {
|
||||
// Optional: could trigger a refresh if needed
|
||||
}}
|
||||
minWidth='true'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* description */}
|
||||
|
@@ -33,6 +33,7 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||
|
||||
const tagList = useTagStore(s => s.tagList)
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
const setShowTagManagementModal = useTagStore(s => s.setShowTagManagementModal)
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
@@ -136,6 +137,15 @@ const TagFilter: FC<TagFilterProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-divider-regular' />
|
||||
<div className='p-1'>
|
||||
<div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => setShowTagManagementModal(true)}>
|
||||
<Tag03 className='h-4 w-4 text-text-tertiary' />
|
||||
<div className='grow truncate text-sm leading-5 text-text-secondary'>
|
||||
{t('common.tag.manageTags')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUnmount } from 'ahooks'
|
||||
@@ -26,6 +26,7 @@ type TagSelectorProps = {
|
||||
selectedTags: Tag[]
|
||||
onCacheUpdate: (tags: Tag[]) => void
|
||||
onChange?: () => void
|
||||
minWidth?: string
|
||||
}
|
||||
|
||||
type PanelProps = {
|
||||
@@ -213,6 +214,7 @@ const TagSelector: FC<TagSelectorProps> = ({
|
||||
selectedTags,
|
||||
onCacheUpdate,
|
||||
onChange,
|
||||
minWidth,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -220,10 +222,20 @@ const TagSelector: FC<TagSelectorProps> = ({
|
||||
const setTagList = useTagStore(s => s.setTagList)
|
||||
|
||||
const getTagList = async () => {
|
||||
const res = await fetchTagList(type)
|
||||
setTagList(res)
|
||||
try {
|
||||
const res = await fetchTagList(type)
|
||||
setTagList(res)
|
||||
}
|
||||
catch (error) {
|
||||
setTagList([])
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (tagList.length === 0)
|
||||
getTagList()
|
||||
}, [type])
|
||||
|
||||
const triggerContent = useMemo(() => {
|
||||
if (selectedTags?.length)
|
||||
return selectedTags.filter(selectedTag => tagList.find(tag => tag.id === selectedTag.id)).map(tag => tag.name).join(', ')
|
||||
@@ -266,7 +278,7 @@ const TagSelector: FC<TagSelectorProps> = ({
|
||||
'!w-full !border-0 !p-0 !text-text-tertiary hover:!bg-state-base-hover hover:!text-text-secondary',
|
||||
)
|
||||
}
|
||||
popupClassName='!w-full !ring-0'
|
||||
popupClassName={cn('!w-full !ring-0', minWidth && '!min-w-80')}
|
||||
className={'!z-20 h-fit !w-full'}
|
||||
/>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user