refactor(web): split snippet index

This commit is contained in:
JzoNg
2026-03-30 10:32:59 +08:00
parent 60e381eff0
commit 871a2a149f
10 changed files with 332 additions and 323 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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')
})
})

View File

@@ -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 }}

View File

@@ -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()

View 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

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View 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