mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:06:51 +08:00
feat(web): support snippet nav
This commit is contained in:
parent
42889d23e5
commit
70fd4a5c88
@ -2,13 +2,16 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { useParams, usePathname, useRouter } from '@/next/navigation'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import { useCreateSnippetMutation, useInfiniteSnippetList, useSnippetApiDetail } from '@/service/use-snippets'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppNav from '../index'
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: vi.fn(),
|
||||
usePathname: vi.fn(),
|
||||
useRouter: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@ -29,6 +32,19 @@ vi.mock('@/service/use-apps', () => ({
|
||||
useInfiniteAppList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-snippets', () => ({
|
||||
useCreateSnippetMutation: vi.fn(),
|
||||
useInfiniteSnippetList: vi.fn(),
|
||||
useSnippetApiDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/create-app-dialog', () => ({
|
||||
default: ({ show, onClose, onSuccess }: { show: boolean, onClose: () => void, onSuccess: () => void }) =>
|
||||
show
|
||||
@ -83,17 +99,67 @@ vi.mock('@/app/components/app/create-from-dsl-modal', () => ({
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/create-snippet-dialog', () => ({
|
||||
default: ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (payload: {
|
||||
name: string
|
||||
description: string
|
||||
icon: { type: 'emoji', icon: string, background: string }
|
||||
}) => void
|
||||
}) =>
|
||||
isOpen
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="create-snippet-dialog"
|
||||
onClick={() => {
|
||||
onConfirm({
|
||||
name: 'Created Snippet',
|
||||
description: '',
|
||||
icon: {
|
||||
type: 'emoji',
|
||||
icon: '🤖',
|
||||
background: '#fff',
|
||||
},
|
||||
})
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Create Snippet
|
||||
</button>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('../../nav', () => ({
|
||||
default: ({
|
||||
createText,
|
||||
curNav,
|
||||
isApp,
|
||||
link,
|
||||
onCreate,
|
||||
onLoadMore,
|
||||
navigationItems,
|
||||
}: {
|
||||
createText: string
|
||||
curNav?: { id: string, name: string }
|
||||
isApp?: boolean
|
||||
link: string
|
||||
onCreate: (state: string) => void
|
||||
onLoadMore?: () => void
|
||||
navigationItems?: Array<{ id: string, name: string, link: string }>
|
||||
}) => (
|
||||
<div data-testid="nav">
|
||||
<div data-testid="nav-link">{link}</div>
|
||||
<div data-testid="nav-is-app">{String(isApp)}</div>
|
||||
<div data-testid="nav-create-text">{createText}</div>
|
||||
<div data-testid="nav-current">{curNav ? `${curNav.id}:${curNav.name}` : ''}</div>
|
||||
<ul data-testid="nav-items">
|
||||
{(navigationItems ?? []).map(item => (
|
||||
<li key={item.id}>{`${item.name} -> ${item.link}`}</li>
|
||||
@ -127,10 +193,52 @@ const mockAppData = [
|
||||
},
|
||||
]
|
||||
|
||||
const mockSnippetData = [
|
||||
{
|
||||
id: 'snippet-1',
|
||||
name: 'Snippet 1',
|
||||
description: '',
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '🧩',
|
||||
icon_background: '#fff',
|
||||
icon_url: null,
|
||||
},
|
||||
is_published: true,
|
||||
use_count: 0,
|
||||
created_at: 1,
|
||||
created_by: 'user-1',
|
||||
updated_at: 1,
|
||||
updated_by: 'user-1',
|
||||
},
|
||||
{
|
||||
id: 'snippet-2',
|
||||
name: 'Snippet 2',
|
||||
description: '',
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '⚙️',
|
||||
icon_background: '#000',
|
||||
icon_url: null,
|
||||
},
|
||||
is_published: true,
|
||||
use_count: 1,
|
||||
created_at: 1,
|
||||
created_by: 'user-1',
|
||||
updated_at: 1,
|
||||
updated_by: 'user-1',
|
||||
},
|
||||
]
|
||||
|
||||
const mockUseParams = vi.mocked(useParams)
|
||||
const mockUsePathname = vi.mocked(usePathname)
|
||||
const mockUseRouter = vi.mocked(useRouter)
|
||||
const mockUseAppContext = vi.mocked(useAppContext)
|
||||
const mockUseAppStore = vi.mocked(useAppStore)
|
||||
const mockUseInfiniteAppList = vi.mocked(useInfiniteAppList)
|
||||
const mockUseInfiniteSnippetList = vi.mocked(useInfiniteSnippetList)
|
||||
const mockUseSnippetApiDetail = vi.mocked(useSnippetApiDetail)
|
||||
const mockUseCreateSnippetMutation = vi.mocked(useCreateSnippetMutation)
|
||||
let mockAppDetail: { id: string, name: string } | null = null
|
||||
|
||||
const setupDefaultMocks = (options?: {
|
||||
@ -144,6 +252,8 @@ const setupDefaultMocks = (options?: {
|
||||
const fetchNextPage = options?.fetchNextPage ?? vi.fn()
|
||||
|
||||
mockUseParams.mockReturnValue({ appId: 'app-1' } as ReturnType<typeof useParams>)
|
||||
mockUsePathname.mockReturnValue('/app/app-1/workflow')
|
||||
mockUseRouter.mockReturnValue({ push: vi.fn() } as unknown as ReturnType<typeof useRouter>)
|
||||
mockUseAppContext.mockReturnValue({ isCurrentWorkspaceEditor: options?.isEditor ?? false } as ReturnType<typeof useAppContext>)
|
||||
mockUseAppStore.mockImplementation((selector: unknown) => (selector as (state: { appDetail: { id: string, name: string } | null }) => unknown)({ appDetail: mockAppDetail }))
|
||||
mockUseInfiniteAppList.mockReturnValue({
|
||||
@ -153,10 +263,51 @@ const setupDefaultMocks = (options?: {
|
||||
isFetchingNextPage: false,
|
||||
refetch,
|
||||
} as ReturnType<typeof useInfiniteAppList>)
|
||||
mockUseInfiniteSnippetList.mockReturnValue({
|
||||
data: undefined,
|
||||
fetchNextPage: vi.fn(),
|
||||
hasNextPage: false,
|
||||
isFetchingNextPage: false,
|
||||
} as unknown as ReturnType<typeof useInfiniteSnippetList>)
|
||||
mockUseSnippetApiDetail.mockReturnValue({
|
||||
data: undefined,
|
||||
} as ReturnType<typeof useSnippetApiDetail>)
|
||||
mockUseCreateSnippetMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useCreateSnippetMutation>)
|
||||
|
||||
return { refetch, fetchNextPage }
|
||||
}
|
||||
|
||||
const setupSnippetMocks = (options?: {
|
||||
fetchNextPage?: () => void
|
||||
hasNextPage?: boolean
|
||||
mutate?: ReturnType<typeof vi.fn>
|
||||
}) => {
|
||||
const fetchNextPage = options?.fetchNextPage ?? vi.fn()
|
||||
const mutate = options?.mutate ?? vi.fn()
|
||||
|
||||
setupDefaultMocks({ isEditor: true })
|
||||
mockUseParams.mockReturnValue({ snippetId: 'snippet-1' } as ReturnType<typeof useParams>)
|
||||
mockUsePathname.mockReturnValue('/snippets/snippet-1/orchestrate')
|
||||
mockUseInfiniteSnippetList.mockReturnValue({
|
||||
data: { pages: [{ data: mockSnippetData }] },
|
||||
fetchNextPage,
|
||||
hasNextPage: options?.hasNextPage ?? false,
|
||||
isFetchingNextPage: false,
|
||||
} as unknown as ReturnType<typeof useInfiniteSnippetList>)
|
||||
mockUseSnippetApiDetail.mockReturnValue({
|
||||
data: mockSnippetData[0],
|
||||
} as ReturnType<typeof useSnippetApiDetail>)
|
||||
mockUseCreateSnippetMutation.mockReturnValue({
|
||||
isPending: false,
|
||||
mutate,
|
||||
} as unknown as ReturnType<typeof useCreateSnippetMutation>)
|
||||
|
||||
return { fetchNextPage, mutate }
|
||||
}
|
||||
|
||||
describe('AppNav', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -338,4 +489,67 @@ describe('AppNav', () => {
|
||||
expect(screen.getByText('App 1 -> /app/app-1/configuration')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch the main nav to snippet list and render snippet items on snippet detail routes', () => {
|
||||
setupSnippetMocks()
|
||||
|
||||
render(<AppNav />)
|
||||
|
||||
expect(screen.getByTestId('nav-link')).toHaveTextContent('/snippets')
|
||||
expect(screen.getByTestId('nav-is-app')).toHaveTextContent('false')
|
||||
expect(screen.getByTestId('nav-current')).toHaveTextContent('snippet-1:Snippet 1')
|
||||
expect(screen.getByTestId('nav-create-text')).toHaveTextContent('createFromBlank')
|
||||
expect(screen.getByText('Snippet 1 -> /snippets/snippet-1/orchestrate')).toBeInTheDocument()
|
||||
expect(screen.getByText('Snippet 2 -> /snippets/snippet-2/orchestrate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show stale snippet detail as the current nav while switching snippets', () => {
|
||||
setupSnippetMocks()
|
||||
mockUseParams.mockReturnValue({ snippetId: 'snippet-2' } as ReturnType<typeof useParams>)
|
||||
mockUseSnippetApiDetail.mockReturnValue({
|
||||
data: mockSnippetData[0],
|
||||
} as ReturnType<typeof useSnippetApiDetail>)
|
||||
|
||||
render(<AppNav />)
|
||||
|
||||
expect(screen.getByTestId('nav-current')).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('should load more snippets from the snippet selector when more data is available', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { fetchNextPage } = setupSnippetMocks({ hasNextPage: true })
|
||||
|
||||
render(<AppNav />)
|
||||
|
||||
await user.click(screen.getByTestId('load-more'))
|
||||
expect(fetchNextPage).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should open the create snippet dialog from snippet nav create action', async () => {
|
||||
const user = userEvent.setup()
|
||||
const mutate = vi.fn()
|
||||
setupSnippetMocks({ mutate })
|
||||
|
||||
render(<AppNav />)
|
||||
|
||||
await user.click(screen.getByTestId('create-blank'))
|
||||
expect(screen.getByTestId('create-snippet-dialog')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('create-snippet-dialog'))
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
body: {
|
||||
name: 'Created Snippet',
|
||||
description: undefined,
|
||||
icon_info: {
|
||||
icon: '🤖',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#fff',
|
||||
icon_url: undefined,
|
||||
},
|
||||
},
|
||||
}, expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import type { NavItem } from '../nav/nav-selector'
|
||||
import {
|
||||
RiRobot2Fill,
|
||||
RiRobot2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { flatten } from 'es-toolkit/compat'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import CreateSnippetDialog from '@/app/components/workflow/create-snippet-dialog'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useParams } from '@/next/navigation'
|
||||
import { useParams, usePathname, useRouter } from '@/next/navigation'
|
||||
import { useInfiniteAppList } from '@/service/use-apps'
|
||||
import {
|
||||
useCreateSnippetMutation,
|
||||
useInfiniteSnippetList,
|
||||
useSnippetApiDetail,
|
||||
} from '@/service/use-snippets'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import Nav from '../nav'
|
||||
|
||||
@ -23,13 +26,18 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
|
||||
|
||||
const AppNav = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appId } = useParams()
|
||||
const { appId, snippetId } = useParams()
|
||||
const { push } = useRouter()
|
||||
const pathname = usePathname()
|
||||
const isSnippetSegment = pathname === '/snippets' || pathname.startsWith('/snippets/')
|
||||
const currentSnippetId = typeof snippetId === 'string' ? snippetId : ''
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
|
||||
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||
const [navItems, setNavItems] = useState<NavItem[]>([])
|
||||
const [showCreateSnippetDialog, setShowCreateSnippetDialog] = useState(false)
|
||||
const createSnippetMutation = useCreateSnippetMutation()
|
||||
|
||||
const {
|
||||
data: appsData,
|
||||
@ -41,14 +49,36 @@ const AppNav = () => {
|
||||
page: 1,
|
||||
limit: 30,
|
||||
name: '',
|
||||
}, { enabled: !!appId })
|
||||
}, { enabled: !!appId && !isSnippetSegment })
|
||||
|
||||
const {
|
||||
data: snippetsData,
|
||||
fetchNextPage: fetchNextSnippetPage,
|
||||
hasNextPage: hasNextSnippetPage,
|
||||
isFetchingNextPage: isFetchingNextSnippetPage,
|
||||
} = useInfiniteSnippetList({
|
||||
page: 1,
|
||||
limit: 30,
|
||||
}, { enabled: !!currentSnippetId })
|
||||
|
||||
const { data: snippetDetail } = useSnippetApiDetail(currentSnippetId)
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (hasNextPage)
|
||||
fetchNextPage()
|
||||
}, [fetchNextPage, hasNextPage])
|
||||
|
||||
const handleLoadMoreSnippet = useCallback(() => {
|
||||
if (hasNextSnippetPage)
|
||||
fetchNextSnippetPage()
|
||||
}, [fetchNextSnippetPage, hasNextSnippetPage])
|
||||
|
||||
const openModal = (state: string) => {
|
||||
if (isSnippetSegment) {
|
||||
setShowCreateSnippetDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (state === 'blank')
|
||||
setShowNewAppDialog(true)
|
||||
if (state === 'template')
|
||||
@ -57,64 +87,125 @@ const AppNav = () => {
|
||||
setShowCreateFromDSLModal(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (appsData) {
|
||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
||||
const navItems = appItems.map((app) => {
|
||||
const link = ((isCurrentWorkspaceEditor, app) => {
|
||||
if (!isCurrentWorkspaceEditor) {
|
||||
return `/app/${app.id}/overview`
|
||||
}
|
||||
else {
|
||||
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
return `/app/${app.id}/workflow`
|
||||
else
|
||||
return `/app/${app.id}/configuration`
|
||||
}
|
||||
})(isCurrentWorkspaceEditor, app)
|
||||
return {
|
||||
id: app.id,
|
||||
icon_type: app.icon_type,
|
||||
icon: app.icon,
|
||||
icon_background: app.icon_background,
|
||||
icon_url: app.icon_url,
|
||||
name: app.name,
|
||||
mode: app.mode,
|
||||
link,
|
||||
}
|
||||
})
|
||||
setNavItems(navItems as any)
|
||||
}
|
||||
}, [appsData, isCurrentWorkspaceEditor, setNavItems])
|
||||
const appNavItems = useMemo<NavItem[]>(() => {
|
||||
if (!appsData)
|
||||
return []
|
||||
|
||||
// update current app name
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
const newNavItems = produce(navItems, (draft: NavItem[]) => {
|
||||
navItems.forEach((app, index) => {
|
||||
if (app.id === appDetail.id)
|
||||
draft[index]!.name = appDetail.name
|
||||
})
|
||||
})
|
||||
setNavItems(newNavItems)
|
||||
const appItems = flatten((appsData.pages ?? []).map(appData => appData.data))
|
||||
|
||||
return appItems.map((app) => {
|
||||
const link = (() => {
|
||||
if (!isCurrentWorkspaceEditor)
|
||||
return `/app/${app.id}/overview`
|
||||
|
||||
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
return `/app/${app.id}/workflow`
|
||||
|
||||
return `/app/${app.id}/configuration`
|
||||
})()
|
||||
|
||||
return {
|
||||
id: app.id,
|
||||
icon_type: app.icon_type,
|
||||
icon: app.icon,
|
||||
icon_background: app.icon_background,
|
||||
icon_url: app.icon_url,
|
||||
name: appDetail?.id === app.id ? appDetail.name : app.name,
|
||||
mode: app.mode,
|
||||
link,
|
||||
}
|
||||
})
|
||||
}, [appDetail?.id, appDetail?.name, appsData, isCurrentWorkspaceEditor])
|
||||
|
||||
const snippetNavItems = useMemo<NavItem[]>(() => {
|
||||
if (!snippetsData)
|
||||
return []
|
||||
|
||||
const snippetItems = flatten((snippetsData.pages ?? []).map(snippetData => snippetData.data))
|
||||
|
||||
return snippetItems.map(snippet => ({
|
||||
id: snippet.id,
|
||||
icon_type: snippet.icon_info.icon_type,
|
||||
icon: snippet.icon_info.icon,
|
||||
icon_background: snippet.icon_info.icon_background ?? null,
|
||||
icon_url: snippet.icon_info.icon_url ?? null,
|
||||
name: snippet.name,
|
||||
link: `/snippets/${snippet.id}/orchestrate`,
|
||||
}))
|
||||
}, [snippetsData])
|
||||
|
||||
const currentSnippetNav = useMemo(() => {
|
||||
if (!snippetDetail)
|
||||
return
|
||||
|
||||
if (snippetDetail.id !== currentSnippetId)
|
||||
return
|
||||
|
||||
return {
|
||||
id: snippetDetail.id,
|
||||
icon_type: snippetDetail.icon_info.icon_type,
|
||||
icon: snippetDetail.icon_info.icon,
|
||||
icon_background: snippetDetail.icon_info.icon_background ?? null,
|
||||
icon_url: snippetDetail.icon_info.icon_url ?? null,
|
||||
name: snippetDetail.name,
|
||||
}
|
||||
}, [appDetail, navItems])
|
||||
}, [currentSnippetId, snippetDetail])
|
||||
|
||||
const handleCreateSnippet = useCallback(({
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
icon: AppIconSelection
|
||||
}) => {
|
||||
createSnippetMutation.mutate({
|
||||
body: {
|
||||
name,
|
||||
description: description || undefined,
|
||||
icon_info: {
|
||||
icon: icon.type === 'emoji' ? icon.icon : icon.fileId,
|
||||
icon_type: icon.type,
|
||||
icon_background: icon.type === 'emoji' ? icon.background : undefined,
|
||||
icon_url: icon.type === 'image' ? icon.url : undefined,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
onSuccess: (snippet) => {
|
||||
toast.success(t('snippet.createSuccess', { ns: 'workflow' }))
|
||||
setShowCreateSnippetDialog(false)
|
||||
push(`/snippets/${snippet.id}/orchestrate`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : t('createFailed', { ns: 'snippet' }))
|
||||
},
|
||||
})
|
||||
}, [createSnippetMutation, push, t])
|
||||
|
||||
const currentNav = isSnippetSegment ? currentSnippetNav : appDetail
|
||||
const currentNavigationItems = isSnippetSegment ? snippetNavItems : appNavItems
|
||||
const currentCreateText = isSnippetSegment
|
||||
? t('createFromBlank', { ns: 'snippet' })
|
||||
: t('menus.newApp', { ns: 'common' })
|
||||
const currentLoadMore = isSnippetSegment ? handleLoadMoreSnippet : handleLoadMore
|
||||
const currentIsLoadingMore = isSnippetSegment ? isFetchingNextSnippetPage : isFetchingNextPage
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav
|
||||
isApp
|
||||
icon={<RiRobot2Line className="h-4 w-4" />}
|
||||
activeIcon={<RiRobot2Fill className="h-4 w-4" />}
|
||||
isApp={!isSnippetSegment}
|
||||
icon={<span className="i-ri-robot-2-line h-4 w-4" />}
|
||||
activeIcon={<span className="i-ri-robot-2-fill h-4 w-4" />}
|
||||
text={t('menus.apps', { ns: 'common' })}
|
||||
activeSegment={['apps', 'app', 'snippets']}
|
||||
link="/apps"
|
||||
curNav={appDetail}
|
||||
navigationItems={navItems}
|
||||
createText={t('menus.newApp', { ns: 'common' })}
|
||||
link={isSnippetSegment ? '/snippets' : '/apps'}
|
||||
curNav={currentNav ?? undefined}
|
||||
navigationItems={currentNavigationItems}
|
||||
createText={currentCreateText}
|
||||
onCreate={openModal}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
onLoadMore={currentLoadMore}
|
||||
isLoadingMore={currentIsLoadingMore}
|
||||
/>
|
||||
<CreateAppModal
|
||||
show={showNewAppDialog}
|
||||
@ -131,6 +222,14 @@ const AppNav = () => {
|
||||
onClose={() => setShowCreateFromDSLModal(false)}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
{showCreateSnippetDialog && (
|
||||
<CreateSnippetDialog
|
||||
isOpen={showCreateSnippetDialog}
|
||||
isSubmitting={createSnippetMutation.isPending}
|
||||
onClose={() => setShowCreateSnippetDialog(false)}
|
||||
onConfirm={handleCreateSnippet}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Link from '@/next/link'
|
||||
import { useSelectedLayoutSegment } from '@/next/navigation'
|
||||
import NavSelector from './nav-selector'
|
||||
@ -62,7 +61,7 @@ const Nav = ({
|
||||
<div>
|
||||
{
|
||||
(hovered && curNav)
|
||||
? <ArrowNarrowLeft className="h-4 w-4" />
|
||||
? <span className="i-custom-vender-line-arrows-arrow-narrow-left h-4 w-4" />
|
||||
: isActivated
|
||||
? activeIcon
|
||||
: icon
|
||||
|
||||
@ -50,7 +50,7 @@ const SnippetCard = ({ snippet }: Props) => {
|
||||
background={snippet.icon_info.icon_background}
|
||||
imageUrl={snippet.icon_info.icon_url}
|
||||
/>
|
||||
<div className="w-0 grow py-[1px]">
|
||||
<div className="w-0 grow py-px">
|
||||
<div className="truncate text-sm leading-5 font-semibold text-text-secondary" title={snippet.name}>
|
||||
{snippet.name}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user