mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 16:26:25 +08:00
refactor(web): split snippet index
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import SnippetPage from '@/app/components/snippets'
|
||||
import SnippetEvaluationPage from '@/app/components/snippets/snippet-evaluation-page'
|
||||
|
||||
const Page = async (props: {
|
||||
params: Promise<{ snippetId: string }>
|
||||
}) => {
|
||||
const { snippetId } = await props.params
|
||||
|
||||
return <SnippetPage snippetId={snippetId} section="evaluation" />
|
||||
return <SnippetEvaluationPage snippetId={snippetId} />
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -5,7 +5,7 @@ const Page = async (props: {
|
||||
}) => {
|
||||
const { snippetId } = await props.params
|
||||
|
||||
return <SnippetPage snippetId={snippetId} section="orchestrate" />
|
||||
return <SnippetPage snippetId={snippetId} />
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -1,44 +1,16 @@
|
||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import type { SnippetDetailPayload } from '@/models/snippet'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import SnippetPage from '..'
|
||||
import { useSnippetDetailStore } from '../store'
|
||||
|
||||
const mockUseSnippetInit = vi.fn()
|
||||
const mockPublishSnippetMutateAsync = vi.fn()
|
||||
const mockSetAppSidebarExpand = vi.fn()
|
||||
|
||||
vi.mock('../hooks/use-snippet-init', () => ({
|
||||
useSnippetInit: (snippetId: string) => mockUseSnippetInit(snippetId),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-configs-map', () => ({
|
||||
useConfigsMap: () => ({
|
||||
flowId: 'snippet-1',
|
||||
flowType: 'snippet',
|
||||
fileSettings: {},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: vi.fn(),
|
||||
syncInputFieldsDraft: vi.fn(),
|
||||
syncWorkflowDraftWhenPageClose: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks/use-snippet-refresh-draft', () => ({
|
||||
useSnippetRefreshDraft: () => ({
|
||||
handleRefreshWorkflowDraft: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-snippet-workflows', () => ({
|
||||
usePublishSnippetWorkflowMutation: () => ({
|
||||
mutateAsync: mockPublishSnippetMutateAsync,
|
||||
isPending: false,
|
||||
}),
|
||||
vi.mock('../components/snippet-main', () => ({
|
||||
default: ({ snippetId }: { snippetId: string }) => <div data-testid="snippet-main">{snippetId}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@@ -48,41 +20,14 @@ vi.mock('@/next/navigation', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-snippets', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/use-snippets')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useUpdateSnippetMutation: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
useExportSnippetMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
useDeleteSnippetMutation: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useFileUploadConfig: () => ({
|
||||
data: undefined,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => 'desktop',
|
||||
MediaType: { mobile: 'mobile', desktop: 'desktop' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/hooks', () => ({
|
||||
useFloatingRight: () => ({
|
||||
floatingRight: false,
|
||||
floatingRightWidth: 400,
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({
|
||||
setAppSidebarExpand: mockSetAppSidebarExpand,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -90,26 +35,6 @@ vi.mock('@/app/components/workflow', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="workflow-default-context">{children}</div>
|
||||
),
|
||||
WorkflowWithInnerContext: ({ children, viewport }: { children: React.ReactNode, viewport?: { zoom?: number } }) => (
|
||||
<div data-testid="workflow-inner-context">
|
||||
<span data-testid="workflow-viewport-zoom">{viewport?.zoom ?? 'none'}</span>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/header', () => ({
|
||||
default: (props: HeaderProps) => {
|
||||
const CustomRunMode = props.normal?.runAndHistoryProps?.components?.RunMode
|
||||
|
||||
return (
|
||||
<div data-testid="workflow-header">
|
||||
{props.normal?.components?.left}
|
||||
{CustomRunMode && <CustomRunMode text={props.normal?.runAndHistoryProps?.runButtonText} />}
|
||||
{props.normal?.components?.middle}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/context', () => ({
|
||||
@@ -118,6 +43,16 @@ vi.mock('@/app/components/workflow/context', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
initialNodes: (nodes: unknown[]) => nodes,
|
||||
initialEdges: (edges: unknown[]) => edges,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/app-sidebar', () => ({
|
||||
default: ({
|
||||
renderHeader,
|
||||
@@ -139,27 +74,12 @@ vi.mock('@/app/components/app-sidebar/nav-link', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/panel', () => ({
|
||||
default: ({ components }: { components?: { left?: React.ReactNode, right?: React.ReactNode } }) => (
|
||||
<div data-testid="workflow-panel">
|
||||
<div data-testid="workflow-panel-left">{components?.left}</div>
|
||||
<div data-testid="workflow-panel-right">{components?.right}</div>
|
||||
</div>
|
||||
),
|
||||
vi.mock('@/app/components/app-sidebar/snippet-info', () => ({
|
||||
default: () => <div data-testid="snippet-info" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
initialNodes: (nodes: unknown[]) => nodes,
|
||||
initialEdges: (edges: unknown[]) => edges,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-sortablejs', () => ({
|
||||
ReactSortable: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
vi.mock('@/app/components/evaluation', () => ({
|
||||
default: ({ resourceId }: { resourceId: string }) => <div data-testid="evaluation">{resourceId}</div>,
|
||||
}))
|
||||
|
||||
const mockSnippetDetail: SnippetDetailPayload = {
|
||||
@@ -179,20 +99,10 @@ const mockSnippetDetail: SnippetDetailPayload = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
inputFields: [
|
||||
{
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Blog URL',
|
||||
variable: 'blog_url',
|
||||
required: true,
|
||||
options: [],
|
||||
placeholder: 'Paste a source article URL',
|
||||
max_length: 256,
|
||||
},
|
||||
],
|
||||
inputFields: [],
|
||||
uiMeta: {
|
||||
inputFieldCount: 1,
|
||||
checklistCount: 2,
|
||||
inputFieldCount: 0,
|
||||
checklistCount: 0,
|
||||
autoSavedAt: 'Auto-saved · a few seconds ago',
|
||||
},
|
||||
}
|
||||
@@ -200,55 +110,23 @@ const mockSnippetDetail: SnippetDetailPayload = {
|
||||
describe('SnippetPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useSnippetDetailStore.getState().reset()
|
||||
mockPublishSnippetMutateAsync.mockResolvedValue(undefined)
|
||||
mockUseSnippetInit.mockReturnValue({
|
||||
data: mockSnippetDetail,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the snippet detail shell', () => {
|
||||
it('should render the orchestrate route shell with independent main content', () => {
|
||||
render(<SnippetPage snippetId="snippet-1" />)
|
||||
|
||||
expect(screen.getByText('Tone Rewriter')).toBeInTheDocument()
|
||||
expect(screen.getByText('A static snippet mock.')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-sidebar')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('snippet-info')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-context-provider')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-default-context')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-inner-context')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-viewport-zoom').textContent).toBe('1')
|
||||
expect(screen.getByTestId('snippet-main')).toHaveTextContent('snippet-1')
|
||||
})
|
||||
|
||||
it('should open the input field panel and editor', () => {
|
||||
render(<SnippetPage snippetId="snippet-1" />)
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /snippet\.inputFieldButton/i })[0])
|
||||
expect(screen.getAllByText('snippet.panelTitle').length).toBeGreaterThan(0)
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /datasetPipeline\.inputFieldPanel\.addInputField/i })[0])
|
||||
expect(screen.getAllByText('datasetPipeline.inputFieldPanel.addInputField').length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('should toggle the publish menu', () => {
|
||||
render(<SnippetPage snippetId="snippet-1" />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /snippet\.publishButton/i }))
|
||||
expect(screen.getByText('snippet.publishMenuCurrentDraft')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should publish the snippet when clicking publish in the menu', async () => {
|
||||
render(<SnippetPage snippetId="snippet-1" />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /snippet\.publishButton/i }))
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /snippet\.publishButton/i })[1])
|
||||
|
||||
expect(mockPublishSnippetMutateAsync).toHaveBeenCalledWith({
|
||||
params: { snippetId: 'snippet-1' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should render loading fallback when snippet data is unavailable', () => {
|
||||
it('should render loading fallback when orchestrate data is unavailable', () => {
|
||||
mockUseSnippetInit.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Snippet } from '@/types/snippet'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import SnippetEvaluationPage from '../snippet-evaluation-page'
|
||||
|
||||
const mockUseSnippetApiDetail = vi.fn()
|
||||
const mockSetAppSidebarExpand = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-snippets', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/use-snippets')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useSnippetApiDetail: (snippetId: string) => mockUseSnippetApiDetail(snippetId),
|
||||
useUpdateSnippetMutation: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
useExportSnippetMutation: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
useDeleteSnippetMutation: () => ({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: vi.fn(),
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => 'desktop',
|
||||
MediaType: { mobile: 'mobile', desktop: 'desktop' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({
|
||||
setAppSidebarExpand: mockSetAppSidebarExpand,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar', () => ({
|
||||
default: ({
|
||||
renderHeader,
|
||||
renderNavigation,
|
||||
}: {
|
||||
renderHeader?: (modeState: string) => React.ReactNode
|
||||
renderNavigation?: (modeState: string) => React.ReactNode
|
||||
}) => (
|
||||
<div data-testid="app-sidebar">
|
||||
<div data-testid="app-sidebar-header">{renderHeader?.('expand')}</div>
|
||||
<div data-testid="app-sidebar-navigation">{renderNavigation?.('expand')}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar/nav-link', () => ({
|
||||
default: ({ name, onClick }: { name: string, onClick?: () => void }) => (
|
||||
<button type="button" onClick={onClick}>{name}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/evaluation', () => ({
|
||||
default: ({ resourceId }: { resourceId: string }) => <div data-testid="evaluation">{resourceId}</div>,
|
||||
}))
|
||||
|
||||
const mockSnippetApiDetail: Snippet = {
|
||||
id: 'snippet-1',
|
||||
name: 'Tone Rewriter',
|
||||
description: 'A static snippet mock.',
|
||||
type: 'node',
|
||||
is_published: false,
|
||||
version: 'draft',
|
||||
use_count: 19,
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '🪄',
|
||||
icon_background: '#E0EAFF',
|
||||
},
|
||||
input_fields: [],
|
||||
created_at: 1_711_609_600,
|
||||
updated_at: 1_711_616_800,
|
||||
author: 'Evan',
|
||||
}
|
||||
|
||||
describe('SnippetEvaluationPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseSnippetApiDetail.mockReturnValue({
|
||||
data: mockSnippetApiDetail,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should fetch evaluation route data independently from snippet init', () => {
|
||||
render(<SnippetEvaluationPage snippetId="snippet-1" />)
|
||||
|
||||
expect(mockUseSnippetApiDetail).toHaveBeenCalledWith('snippet-1')
|
||||
expect(screen.getByTestId('app-sidebar')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('evaluation')).toHaveTextContent('snippet-1')
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import type { SnippetDetailPayload, SnippetInputField, SnippetSection } from '@/models/snippet'
|
||||
import type { SnippetDetailPayload, SnippetInputField } from '@/models/snippet'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import SnippetMain from '../snippet-main'
|
||||
|
||||
const mockSetAppSidebarExpand = vi.fn()
|
||||
const mockSyncInputFieldsDraft = vi.fn()
|
||||
const mockCloseEditor = vi.fn()
|
||||
const mockOpenEditor = vi.fn()
|
||||
@@ -40,17 +39,6 @@ const mockInspectVarsCrud = {
|
||||
}
|
||||
let capturedHooksStore: Record<string, unknown> | undefined
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => 'desktop',
|
||||
MediaType: { mobile: 'mobile', desktop: 'desktop' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/store', () => ({
|
||||
useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({
|
||||
setAppSidebarExpand: mockSetAppSidebarExpand,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/snippets/store', () => ({
|
||||
useSnippetDetailStore: (selector: (state: {
|
||||
editingField: SnippetInputField | null
|
||||
@@ -135,33 +123,6 @@ vi.mock('@/app/components/snippets/hooks/use-snippet-start-run', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar', () => ({
|
||||
default: ({
|
||||
renderHeader,
|
||||
renderNavigation,
|
||||
}: {
|
||||
renderHeader?: (modeState: string) => React.ReactNode
|
||||
renderNavigation?: (modeState: string) => React.ReactNode
|
||||
}) => (
|
||||
<div data-testid="app-sidebar">
|
||||
<div>{renderHeader?.('expand')}</div>
|
||||
<div>{renderNavigation?.('expand')}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar/nav-link', () => ({
|
||||
default: ({ name }: { name: string }) => <div>{name}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app-sidebar/snippet-info', () => ({
|
||||
default: () => <div data-testid="snippet-info" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/evaluation', () => ({
|
||||
default: () => <div data-testid="evaluation" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
WorkflowWithInnerContext: ({
|
||||
children,
|
||||
@@ -237,12 +198,11 @@ const payload: SnippetDetailPayload = {
|
||||
},
|
||||
}
|
||||
|
||||
const renderSnippetMain = (section: SnippetSection = 'orchestrate') => {
|
||||
const renderSnippetMain = () => {
|
||||
return render(
|
||||
<SnippetMain
|
||||
payload={payload}
|
||||
snippetId="snippet-1"
|
||||
section={section}
|
||||
nodes={[] as WorkflowProps['nodes']}
|
||||
edges={[] as WorkflowProps['edges']}
|
||||
viewport={{ x: 0, y: 0, zoom: 1 }}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SnippetSection } from '@/models/snippet'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -10,12 +9,10 @@ import { useSnippetDetailStore } from '../../store'
|
||||
|
||||
type UseSnippetPublishOptions = {
|
||||
snippetId: string
|
||||
section: SnippetSection
|
||||
}
|
||||
|
||||
export const useSnippetPublish = ({
|
||||
snippetId,
|
||||
section,
|
||||
}: UseSnippetPublishOptions) => {
|
||||
const { t } = useTranslation('snippet')
|
||||
const publishSnippetMutation = usePublishSnippetWorkflowMutation(snippetId)
|
||||
@@ -41,7 +38,7 @@ export const useSnippetPublish = ({
|
||||
}, [publishSnippetMutation, setPublishMenuOpen, snippetId, t])
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (event) => {
|
||||
if (section !== 'orchestrate' || publishSnippetMutation.isPending)
|
||||
if (publishSnippetMutation.isPending)
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
88
web/app/components/snippets/components/snippet-layout.tsx
Normal file
88
web/app/components/snippets/components/snippet-layout.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/nav-link'
|
||||
import type { SnippetDetail, SnippetSection } from '@/models/snippet'
|
||||
import {
|
||||
RiFlaskFill,
|
||||
RiFlaskLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import NavLink from '@/app/components/app-sidebar/nav-link'
|
||||
import SnippetInfo from '@/app/components/app-sidebar/snippet-info'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type SnippetLayoutProps = {
|
||||
children: ReactNode
|
||||
section: SnippetSection
|
||||
snippet: SnippetDetail
|
||||
snippetId: string
|
||||
}
|
||||
|
||||
const ORCHESTRATE_ICONS: { normal: NavIcon, selected: NavIcon } = {
|
||||
normal: RiTerminalWindowLine,
|
||||
selected: RiTerminalWindowFill,
|
||||
}
|
||||
|
||||
const EVALUATION_ICONS: { normal: NavIcon, selected: NavIcon } = {
|
||||
normal: RiFlaskLine,
|
||||
selected: RiFlaskFill,
|
||||
}
|
||||
|
||||
const SnippetLayout = ({
|
||||
children,
|
||||
section,
|
||||
snippet,
|
||||
snippetId,
|
||||
}: SnippetLayoutProps) => {
|
||||
const { t } = useTranslation('snippet')
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand)
|
||||
|
||||
useEffect(() => {
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
}, [isMobile, setAppSidebarExpand])
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full overflow-hidden bg-background-body">
|
||||
<AppSideBar
|
||||
navigation={[]}
|
||||
renderHeader={mode => <SnippetInfo expand={mode === 'expand'} snippet={snippet} />}
|
||||
renderNavigation={mode => (
|
||||
<>
|
||||
<NavLink
|
||||
mode={mode}
|
||||
name={t('sectionOrchestrate')}
|
||||
iconMap={ORCHESTRATE_ICONS}
|
||||
href={`/snippets/${snippetId}/orchestrate`}
|
||||
active={section === 'orchestrate'}
|
||||
/>
|
||||
<NavLink
|
||||
mode={mode}
|
||||
name={t('sectionEvaluation')}
|
||||
iconMap={EVALUATION_ICONS}
|
||||
href={`/snippets/${snippetId}/evaluation`}
|
||||
active={section === 'evaluation'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative min-h-0 min-w-0 grow overflow-hidden">
|
||||
<div className="absolute inset-0 min-h-0 min-w-0 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SnippetLayout
|
||||
@@ -1,29 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/nav-link'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import type { SnippetDetailPayload, SnippetSection } from '@/models/snippet'
|
||||
import {
|
||||
RiFlaskFill,
|
||||
RiFlaskLine,
|
||||
RiTerminalWindowFill,
|
||||
RiTerminalWindowLine,
|
||||
} from '@remixicon/react'
|
||||
import type { SnippetDetailPayload } from '@/models/snippet'
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import NavLink from '@/app/components/app-sidebar/nav-link'
|
||||
import SnippetInfo from '@/app/components/app-sidebar/snippet-info'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Evaluation from '@/app/components/evaluation'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useConfigsMap } from '../hooks/use-configs-map'
|
||||
import { useInspectVarsCrud } from '../hooks/use-inspect-vars-crud'
|
||||
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
|
||||
@@ -38,31 +24,16 @@ import SnippetChildren from './snippet-children'
|
||||
type SnippetMainProps = {
|
||||
payload: SnippetDetailPayload
|
||||
snippetId: string
|
||||
section: SnippetSection
|
||||
} & Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
||||
|
||||
const ORCHESTRATE_ICONS: { normal: NavIcon, selected: NavIcon } = {
|
||||
normal: RiTerminalWindowLine,
|
||||
selected: RiTerminalWindowFill,
|
||||
}
|
||||
|
||||
const EVALUATION_ICONS: { normal: NavIcon, selected: NavIcon } = {
|
||||
normal: RiFlaskLine,
|
||||
selected: RiFlaskFill,
|
||||
}
|
||||
|
||||
const SnippetMain = ({
|
||||
payload,
|
||||
snippetId,
|
||||
section,
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
}: SnippetMainProps) => {
|
||||
const { t } = useTranslation('snippet')
|
||||
const { graph, snippet, uiMeta } = payload
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const { graph, uiMeta } = payload
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
@@ -114,7 +85,6 @@ const SnippetMain = ({
|
||||
nodesMap,
|
||||
}
|
||||
}, [workflowAvailableNodesMetaData])
|
||||
const setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand)
|
||||
const reset = useSnippetDetailStore(state => state.reset)
|
||||
const {
|
||||
editingField,
|
||||
@@ -139,7 +109,6 @@ const SnippetMain = ({
|
||||
setPublishMenuOpen,
|
||||
} = useSnippetPublish({
|
||||
snippetId,
|
||||
section,
|
||||
})
|
||||
const {
|
||||
handleStartWorkflowRun,
|
||||
@@ -153,12 +122,6 @@ const SnippetMain = ({
|
||||
reset()
|
||||
}, [reset, snippetId])
|
||||
|
||||
useEffect(() => {
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSidebarExpand(isMobile ? mode : localeMode)
|
||||
}, [isMobile, setAppSidebarExpand])
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
@@ -220,67 +183,32 @@ const SnippetMain = ({
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full overflow-hidden bg-background-body">
|
||||
<AppSideBar
|
||||
navigation={[]}
|
||||
renderHeader={mode => <SnippetInfo expand={mode === 'expand'} snippet={snippet} />}
|
||||
renderNavigation={mode => (
|
||||
<>
|
||||
<NavLink
|
||||
mode={mode}
|
||||
name={t('sectionOrchestrate')}
|
||||
iconMap={ORCHESTRATE_ICONS}
|
||||
href={`/snippets/${snippetId}/orchestrate`}
|
||||
active={section === 'orchestrate'}
|
||||
/>
|
||||
<NavLink
|
||||
mode={mode}
|
||||
name={t('sectionEvaluation')}
|
||||
iconMap={EVALUATION_ICONS}
|
||||
href={`/snippets/${snippetId}/evaluation`}
|
||||
active={section === 'evaluation'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport ?? graph.viewport}
|
||||
hooksStore={hooksStore as any}
|
||||
>
|
||||
<SnippetChildren
|
||||
snippetId={snippetId}
|
||||
fields={fields}
|
||||
uiMeta={uiMeta}
|
||||
editingField={editingField}
|
||||
isEditorOpen={isEditorOpen}
|
||||
isInputPanelOpen={isInputPanelOpen}
|
||||
isPublishMenuOpen={isPublishMenuOpen}
|
||||
isPublishing={isPublishing}
|
||||
onToggleInputPanel={handleToggleInputPanel}
|
||||
onPublishMenuOpenChange={setPublishMenuOpen}
|
||||
onCloseInputPanel={handleCloseInputPanel}
|
||||
onPublish={handlePublish}
|
||||
onOpenEditor={openEditor}
|
||||
onCloseEditor={closeEditor}
|
||||
onSubmitField={handleSubmitField}
|
||||
onRemoveField={handleRemoveField}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
<div className="relative min-h-0 min-w-0 grow overflow-hidden">
|
||||
<div className="absolute inset-0 min-h-0 min-w-0 overflow-hidden">
|
||||
{section === 'evaluation'
|
||||
? (
|
||||
<Evaluation resourceType="snippet" resourceId={snippetId} />
|
||||
)
|
||||
: (
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport ?? graph.viewport}
|
||||
hooksStore={hooksStore as any}
|
||||
>
|
||||
<SnippetChildren
|
||||
snippetId={snippetId}
|
||||
fields={fields}
|
||||
uiMeta={uiMeta}
|
||||
editingField={editingField}
|
||||
isEditorOpen={isEditorOpen}
|
||||
isInputPanelOpen={isInputPanelOpen}
|
||||
isPublishMenuOpen={isPublishMenuOpen}
|
||||
isPublishing={isPublishing}
|
||||
onToggleInputPanel={handleToggleInputPanel}
|
||||
onPublishMenuOpenChange={setPublishMenuOpen}
|
||||
onCloseInputPanel={handleCloseInputPanel}
|
||||
onPublish={handlePublish}
|
||||
onOpenEditor={openEditor}
|
||||
onCloseEditor={closeEditor}
|
||||
onSubmitField={handleSubmitField}
|
||||
onRemoveField={handleRemoveField}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
</WorkflowWithInnerContext>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WorkflowWithInnerContext>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { SnippetSection } from '@/models/snippet'
|
||||
import { useMemo } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
@@ -9,18 +8,23 @@ import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import SnippetLayout from './components/snippet-layout'
|
||||
import SnippetMain from './components/snippet-main'
|
||||
import { useSnippetInit } from './hooks/use-snippet-init'
|
||||
|
||||
type SnippetPageProps = {
|
||||
snippetId: string
|
||||
section?: SnippetSection
|
||||
}
|
||||
|
||||
const SnippetPage = ({
|
||||
snippetId,
|
||||
section = 'orchestrate',
|
||||
}: SnippetPageProps) => {
|
||||
const SnippetPageLoading = () => {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SnippetPage = ({ snippetId }: SnippetPageProps) => {
|
||||
const { data, isLoading } = useSnippetInit(snippetId)
|
||||
const nodesData = useMemo(() => {
|
||||
if (!data)
|
||||
@@ -36,35 +40,36 @@ const SnippetPage = ({
|
||||
}, [data])
|
||||
|
||||
if (!data || isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
return <SnippetPageLoading />
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowWithDefaultContext
|
||||
edges={edgesData}
|
||||
nodes={nodesData}
|
||||
<SnippetLayout
|
||||
snippetId={snippetId}
|
||||
snippet={data.snippet}
|
||||
section="orchestrate"
|
||||
>
|
||||
<SnippetMain
|
||||
key={snippetId}
|
||||
snippetId={snippetId}
|
||||
section={section}
|
||||
payload={data}
|
||||
nodes={nodesData}
|
||||
<WorkflowWithDefaultContext
|
||||
edges={edgesData}
|
||||
viewport={data.graph.viewport}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
nodes={nodesData}
|
||||
>
|
||||
<SnippetMain
|
||||
key={snippetId}
|
||||
snippetId={snippetId}
|
||||
payload={data}
|
||||
nodes={nodesData}
|
||||
edges={edgesData}
|
||||
viewport={data.graph.viewport}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
</SnippetLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const SnippetPageWrapper = (props: SnippetPageProps) => {
|
||||
const SnippetPageWrapper = ({ snippetId }: SnippetPageProps) => {
|
||||
return (
|
||||
<WorkflowContextProvider>
|
||||
<SnippetPage {...props} />
|
||||
<SnippetPage snippetId={snippetId} />
|
||||
</WorkflowContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
46
web/app/components/snippets/snippet-evaluation-page.tsx
Normal file
46
web/app/components/snippets/snippet-evaluation-page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Evaluation from '@/app/components/evaluation'
|
||||
import { buildSnippetDetailPayload, useSnippetApiDetail } from '@/service/use-snippets'
|
||||
import { getSnippetDetailMock } from '@/service/use-snippets.mock'
|
||||
import SnippetLayout from './components/snippet-layout'
|
||||
|
||||
type SnippetEvaluationPageProps = {
|
||||
snippetId: string
|
||||
}
|
||||
|
||||
const SnippetEvaluationPage = ({ snippetId }: SnippetEvaluationPageProps) => {
|
||||
const snippetApiDetail = useSnippetApiDetail(snippetId)
|
||||
const mockSnippet = useMemo(() => getSnippetDetailMock(snippetId)?.snippet, [snippetId])
|
||||
const snippet = useMemo(() => {
|
||||
if (snippetApiDetail.data)
|
||||
return buildSnippetDetailPayload(snippetApiDetail.data).snippet
|
||||
|
||||
if (!snippetApiDetail.isLoading)
|
||||
return mockSnippet
|
||||
|
||||
return undefined
|
||||
}, [mockSnippet, snippetApiDetail.data, snippetApiDetail.isLoading])
|
||||
|
||||
if (!snippet || snippetApiDetail.isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SnippetLayout
|
||||
snippetId={snippetId}
|
||||
snippet={snippet}
|
||||
section="evaluation"
|
||||
>
|
||||
<Evaluation resourceType="snippet" resourceId={snippetId} />
|
||||
</SnippetLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SnippetEvaluationPage
|
||||
Reference in New Issue
Block a user