feat(web): input fields in snippet

This commit is contained in:
JzoNg 2026-03-29 16:31:38 +08:00
parent 1e76ef5ccb
commit 0f13aabea8
12 changed files with 395 additions and 114 deletions

View File

@ -22,6 +22,7 @@ vi.mock('../hooks/use-configs-map', () => ({
vi.mock('../hooks/use-nodes-sync-draft', () => ({
useNodesSyncDraft: () => ({
doSyncWorkflowDraft: vi.fn(),
syncInputFieldsDraft: vi.fn(),
syncWorkflowDraftWhenPageClose: vi.fn(),
}),
}))

View File

@ -0,0 +1,216 @@
import type { WorkflowProps } from '@/app/components/workflow'
import type { SnippetDetailPayload, SnippetInputField, SnippetSection } 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()
const mockReset = vi.fn()
const mockSetInputPanelOpen = vi.fn()
const mockToggleInputPanel = vi.fn()
const mockTogglePublishMenu = 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/snippets/store', () => ({
useSnippetDetailStore: (selector: (state: {
editingField: SnippetInputField | null
isEditorOpen: boolean
isInputPanelOpen: boolean
isPublishMenuOpen: boolean
closeEditor: typeof mockCloseEditor
openEditor: typeof mockOpenEditor
reset: typeof mockReset
setInputPanelOpen: typeof mockSetInputPanelOpen
toggleInputPanel: typeof mockToggleInputPanel
togglePublishMenu: typeof mockTogglePublishMenu
}) => unknown) => selector({
editingField: null,
isEditorOpen: false,
isInputPanelOpen: true,
isPublishMenuOpen: false,
closeEditor: mockCloseEditor,
openEditor: mockOpenEditor,
reset: mockReset,
setInputPanelOpen: mockSetInputPanelOpen,
toggleInputPanel: mockToggleInputPanel,
togglePublishMenu: mockTogglePublishMenu,
}),
}))
vi.mock('@/app/components/snippets/hooks/use-configs-map', () => ({
useConfigsMap: () => ({
flowId: 'snippet-1',
flowType: 'snippet',
fileSettings: {},
}),
}))
vi.mock('@/app/components/snippets/hooks/use-nodes-sync-draft', () => ({
useNodesSyncDraft: () => ({
doSyncWorkflowDraft: vi.fn(),
syncInputFieldsDraft: mockSyncInputFieldsDraft,
syncWorkflowDraftWhenPageClose: vi.fn(),
}),
}))
vi.mock('@/app/components/snippets/hooks/use-snippet-refresh-draft', () => ({
useSnippetRefreshDraft: () => ({
handleRefreshWorkflowDraft: vi.fn(),
}),
}))
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 }: { children: React.ReactNode }) => (
<div data-testid="workflow-inner-context">{children}</div>
),
}))
vi.mock('@/app/components/snippets/components/snippet-children', () => ({
default: ({
onRemoveField,
onSubmitField,
}: {
onRemoveField: (index: number) => void
onSubmitField: (field: SnippetInputField) => void
}) => (
<div>
<button type="button" onClick={() => onRemoveField(0)}>remove</button>
<button
type="button"
onClick={() => onSubmitField({
type: PipelineInputVarType.textInput,
label: 'New Field',
variable: 'new_field',
required: true,
})}
>
submit
</button>
</div>
),
}))
const payload: SnippetDetailPayload = {
snippet: {
id: 'snippet-1',
name: 'Snippet',
description: 'desc',
author: '',
updatedAt: '2026-03-29 10:00',
usage: '0',
icon: '',
iconBackground: '',
},
graph: {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
inputFields: [
{
type: PipelineInputVarType.textInput,
label: 'Blog URL',
variable: 'blog_url',
required: true,
},
],
uiMeta: {
inputFieldCount: 1,
checklistCount: 0,
autoSavedAt: '2026-03-29 10:00',
},
}
const renderSnippetMain = (section: SnippetSection = 'orchestrate') => {
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 }}
/>,
)
}
describe('SnippetMain', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSyncInputFieldsDraft.mockResolvedValue(undefined)
})
describe('Input Fields Sync', () => {
it('should sync draft input_fields when removing a field from the panel', async () => {
renderSnippetMain()
fireEvent.click(screen.getByRole('button', { name: 'remove' }))
await waitFor(() => {
expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([], {
onRefresh: expect.any(Function),
})
})
})
it('should sync draft input_fields when submitting a field from the editor', async () => {
renderSnippetMain()
fireEvent.click(screen.getByRole('button', { name: 'submit' }))
await waitFor(() => {
expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith([
payload.inputFields[0],
{
type: PipelineInputVarType.textInput,
label: 'New Field',
variable: 'new_field',
required: true,
},
], {
onRefresh: expect.any(Function),
})
})
})
})
})

View File

@ -35,8 +35,7 @@ describe('SnippetWorkflowPanel', () => {
onCloseEditor={vi.fn()}
onSubmitField={vi.fn()}
onRemoveField={vi.fn()}
onPrimarySortChange={vi.fn()}
onSecondarySortChange={vi.fn()}
onSortChange={vi.fn()}
/>,
)

View File

@ -5,7 +5,6 @@ import type { SnippetInputField } from '@/models/snippet'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import FieldListContainer from '@/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container'
type SnippetInputFieldPanelProps = {
@ -14,8 +13,7 @@ type SnippetInputFieldPanelProps = {
onAdd: () => void
onEdit: (field: SnippetInputField) => void
onRemove: (index: number) => void
onPrimarySortChange: (fields: SnippetInputField[]) => void
onSecondarySortChange: (fields: SnippetInputField[]) => void
onSortChange: (fields: SnippetInputField[]) => void
}
const toInputFields = (list: SortableItem[]) => {
@ -31,32 +29,19 @@ const SnippetInputFieldPanel = ({
onAdd,
onEdit,
onRemove,
onPrimarySortChange,
onSecondarySortChange,
onSortChange,
}: SnippetInputFieldPanelProps) => {
const { t } = useTranslation('snippet')
const primaryFields = fields.slice(0, 2)
const secondaryFields = fields.slice(2)
const handlePrimaryRemove = useCallback((index: number) => {
const handleRemove = useCallback((index: number) => {
onRemove(index)
}, [onRemove])
const handleSecondaryRemove = useCallback((index: number) => {
onRemove(index + primaryFields.length)
}, [onRemove, primaryFields.length])
const handlePrimaryEdit = useCallback((id: string) => {
const field = primaryFields.find(item => item.variable === id)
const handleEdit = useCallback((id: string) => {
const field = fields.find(item => item.variable === id)
if (field)
onEdit(field)
}, [onEdit, primaryFields])
const handleSecondaryEdit = useCallback((id: string) => {
const field = secondaryFields.find(item => item.variable === id)
if (field)
onEdit(field)
}, [onEdit, secondaryFields])
}, [fields, onEdit])
return (
<div className="mr-1 flex h-full w-[min(400px,calc(100vw-24px))] flex-col rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
@ -79,37 +64,19 @@ const SnippetInputFieldPanel = ({
</div>
<div className="px-4 pb-2">
<Button variant="secondary" size="small" className="w-full justify-center gap-1" onClick={onAdd}>
<Button variant="primary" size="medium" className="gap-0.5 px-3" onClick={onAdd}>
<span aria-hidden className="i-ri-add-line h-4 w-4" />
{t('inputFieldPanel.addInputField', { ns: 'datasetPipeline' })}
</Button>
</div>
<div className="flex grow flex-col overflow-y-auto">
<div className="px-4 pb-1 pt-2 text-text-secondary system-xs-semibold-uppercase">
{t('panelPrimaryGroup')}
</div>
<FieldListContainer
className="flex flex-col gap-y-1 px-4 pb-2"
inputFields={primaryFields}
onListSortChange={list => onPrimarySortChange(toInputFields(list))}
onRemoveField={handlePrimaryRemove}
onEditField={handlePrimaryEdit}
/>
<div className="px-4 py-2">
<Divider type="horizontal" className="bg-divider-subtle" />
</div>
<div className="px-4 pb-1 text-text-secondary system-xs-semibold-uppercase">
{t('panelSecondaryGroup')}
</div>
<FieldListContainer
className="flex flex-col gap-y-1 px-4 pb-4"
inputFields={secondaryFields}
onListSortChange={list => onSecondarySortChange(toInputFields(list))}
onRemoveField={handleSecondaryRemove}
onEditField={handleSecondaryEdit}
className="flex flex-col gap-y-1 px-4 py-4"
inputFields={fields}
onListSortChange={list => onSortChange(toInputFields(list))}
onRemoveField={handleRemove}
onEditField={handleEdit}
/>
</div>
</div>

View File

@ -22,8 +22,7 @@ type SnippetChildrenProps = {
onCloseEditor: () => void
onSubmitField: (field: SnippetInputField) => void
onRemoveField: (index: number) => void
onPrimarySortChange: (fields: SnippetInputField[]) => void
onSecondarySortChange: (fields: SnippetInputField[]) => void
onSortChange: (fields: SnippetInputField[]) => void
}
const SnippetChildren = ({
@ -41,8 +40,7 @@ const SnippetChildren = ({
onCloseEditor,
onSubmitField,
onRemoveField,
onPrimarySortChange,
onSecondarySortChange,
onSortChange,
}: SnippetChildrenProps) => {
return (
<>
@ -66,8 +64,7 @@ const SnippetChildren = ({
onCloseEditor={onCloseEditor}
onSubmitField={onSubmitField}
onRemoveField={onRemoveField}
onPrimarySortChange={onPrimarySortChange}
onSecondarySortChange={onSecondarySortChange}
onSortChange={onSortChange}
/>
{isPublishMenuOpen && (
@ -85,8 +82,7 @@ const SnippetChildren = ({
onAdd={() => onOpenEditor()}
onEdit={onOpenEditor}
onRemove={onRemoveField}
onPrimarySortChange={onPrimarySortChange}
onSecondarySortChange={onSecondarySortChange}
onSortChange={onSortChange}
/>
</div>
</div>

View File

@ -61,6 +61,7 @@ const SnippetMain = ({
const [fields, setFields] = useState<SnippetInputField[]>(payload.inputFields)
const {
doSyncWorkflowDraft,
syncInputFieldsDraft,
syncWorkflowDraftWhenPageClose,
} = useNodesSyncDraft(snippetId)
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
@ -100,19 +101,16 @@ const SnippetMain = ({
setAppSidebarExpand(isMobile ? mode : localeMode)
}, [isMobile, setAppSidebarExpand])
const primaryFields = useMemo(() => fields.slice(0, 2), [fields])
const secondaryFields = useMemo(() => fields.slice(2), [fields])
const handlePrimarySortChange = (newFields: SnippetInputField[]) => {
setFields([...newFields, ...secondaryFields])
}
const handleSecondarySortChange = (newFields: SnippetInputField[]) => {
setFields([...primaryFields, ...newFields])
const handleSortChange = (newFields: SnippetInputField[]) => {
setFields(newFields)
}
const handleRemoveField = (index: number) => {
setFields(current => current.filter((_, currentIndex) => currentIndex !== index))
const nextFields = fields.filter((_, currentIndex) => currentIndex !== index)
setFields(nextFields)
void syncInputFieldsDraft(nextFields, {
onRefresh: setFields,
})
}
const handleSubmitField = (field: SnippetInputField) => {
@ -124,10 +122,14 @@ const SnippetMain = ({
return
}
if (originalVariable)
setFields(current => current.map(item => item.variable === originalVariable ? field : item))
else
setFields(current => [...current, field])
const nextFields = originalVariable
? fields.map(item => item.variable === originalVariable ? field : item)
: [...fields, field]
setFields(nextFields)
void syncInputFieldsDraft(nextFields, {
onRefresh: setFields,
})
closeEditor()
}
@ -205,8 +207,7 @@ const SnippetMain = ({
onCloseEditor={closeEditor}
onSubmitField={handleSubmitField}
onRemoveField={handleRemoveField}
onPrimarySortChange={handlePrimarySortChange}
onSecondarySortChange={handleSecondarySortChange}
onSortChange={handleSortChange}
/>
</WorkflowWithInnerContext>
)}

View File

@ -18,8 +18,7 @@ type SnippetWorkflowPanelProps = {
onCloseEditor: () => void
onSubmitField: (field: SnippetInputField) => void
onRemoveField: (index: number) => void
onPrimarySortChange: (fields: SnippetInputField[]) => void
onSecondarySortChange: (fields: SnippetInputField[]) => void
onSortChange: (fields: SnippetInputField[]) => void
}
const SnippetPanelOnLeft = ({
@ -32,8 +31,7 @@ const SnippetPanelOnLeft = ({
onCloseEditor,
onSubmitField,
onRemoveField,
onPrimarySortChange,
onSecondarySortChange,
onSortChange,
}: SnippetWorkflowPanelProps) => {
return (
<div className="hidden xl:flex">
@ -51,8 +49,7 @@ const SnippetPanelOnLeft = ({
onAdd={() => onOpenEditor()}
onEdit={onOpenEditor}
onRemove={onRemoveField}
onPrimarySortChange={onPrimarySortChange}
onSecondarySortChange={onSecondarySortChange}
onSortChange={onSortChange}
/>
)}
</div>
@ -70,8 +67,7 @@ const SnippetWorkflowPanel = ({
onCloseEditor,
onSubmitField,
onRemoveField,
onPrimarySortChange,
onSecondarySortChange,
onSortChange,
}: SnippetWorkflowPanelProps) => {
const versionHistoryPanelProps = useMemo(() => {
return {
@ -98,8 +94,7 @@ const SnippetWorkflowPanel = ({
onCloseEditor={onCloseEditor}
onSubmitField={onSubmitField}
onRemoveField={onRemoveField}
onPrimarySortChange={onPrimarySortChange}
onSecondarySortChange={onSecondarySortChange}
onSortChange={onSortChange}
/>
),
},
@ -113,10 +108,10 @@ const SnippetWorkflowPanel = ({
onCloseEditor,
onCloseInputPanel,
onOpenEditor,
onPrimarySortChange,
onRemoveField,
onSecondarySortChange,
onSortChange,
onSubmitField,
snippetId,
versionHistoryPanelProps,
])

View File

@ -84,6 +84,68 @@ describe('useSnippetInit', () => {
expect(result.current.isLoading).toBe(false)
})
it('should use draft input_fields for snippet inputs', () => {
mockUseSnippetApiDetail.mockReturnValue({
data: {
id: 'snippet-1',
name: 'Tone Rewriter',
description: 'A static snippet mock.',
type: 'node',
is_published: false,
version: '1',
use_count: 0,
icon_info: {
icon_type: null,
icon: '🪄',
icon_background: '#E0EAFF',
},
input_fields: [
{
label: 'Published field',
variable: 'published_field',
type: 'text-input',
required: true,
},
],
created_at: 1_712_300_000,
updated_at: 1_712_300_000,
author: 'Evan',
},
error: null,
isLoading: false,
})
mockUseSnippetDraftWorkflow.mockReturnValue({
data: {
id: 'draft-1',
graph: {},
features: {},
input_fields: [
{
label: 'Draft field',
variable: 'draft_field',
type: 'text-input',
required: true,
},
],
hash: 'draft-hash',
created_at: 1_712_300_000,
updated_at: 1_712_345_678,
},
isLoading: false,
})
const { result } = renderHook(() => useSnippetInit('snippet-1'))
expect(result.current.data?.inputFields).toEqual([
{
label: 'Draft field',
variable: 'draft_field',
type: 'text-input',
required: true,
},
])
})
it('should sync draft metadata into workflow store', () => {
mockUseSnippetDraftWorkflow.mockImplementation((_snippetId: string, onSuccess?: (data: { updated_at: number, hash: string }) => void) => {
onSuccess?.({

View File

@ -1,4 +1,6 @@
import type { SyncDraftCallback } from '@/app/components/workflow/hooks-store'
import type { SnippetInputField } from '@/models/snippet'
import type { SnippetDraftSyncPayload, SnippetWorkflow } from '@/types/snippet'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
@ -18,13 +20,17 @@ const isSyncConflictError = (error: unknown): error is { bodyUsed: boolean, json
&& typeof error.json === 'function'
}
type SyncInputFieldsDraftCallback = SyncDraftCallback & {
onRefresh?: (inputFields: SnippetInputField[]) => void
}
export const useNodesSyncDraft = (snippetId: string) => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const { getNodesReadOnly } = useNodesReadOnly()
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
const getPostParams = useCallback(() => {
const getGraphSyncPayload = useCallback(() => {
const {
getNodes,
edges,
@ -32,8 +38,6 @@ export const useNodesSyncDraft = (snippetId: string) => {
} = store.getState()
const nodes = getNodes().filter(node => !node.data?._isTempNode)
const [x, y, zoom] = transform
const { syncWorkflowDraftHash } = workflowStore.getState()
if (!snippetId)
return null
@ -55,47 +59,39 @@ export const useNodesSyncDraft = (snippetId: string) => {
})
return {
url: `/snippets/${snippetId}/workflows/draft`,
params: {
graph: {
nodes: producedNodes,
edges: producedEdges,
viewport: { x, y, zoom },
},
hash: syncWorkflowDraftHash,
graph: {
nodes: producedNodes,
edges: producedEdges,
viewport: { x, y, zoom },
},
}
}, [snippetId, store, workflowStore])
}, [snippetId, store])
const syncWorkflowDraftWhenPageClose = useCallback(() => {
if (getNodesReadOnly())
return
const postParams = getPostParams()
if (postParams)
postWithKeepalive(`${API_PREFIX}${postParams.url}`, postParams.params)
}, [getNodesReadOnly, getPostParams])
const performSync = useCallback(async (
const syncDraft = useCallback(async (
payload: Omit<SnippetDraftSyncPayload, 'hash'>,
notRefreshWhenSyncError?: boolean,
callback?: SyncDraftCallback,
onRefresh?: (draftWorkflow: SnippetWorkflow) => void,
) => {
if (getNodesReadOnly())
return
const postParams = getPostParams()
if (!postParams)
if (!snippetId)
return
const {
setDraftUpdatedAt,
setSyncWorkflowDraftHash,
syncWorkflowDraftHash,
} = workflowStore.getState()
try {
const response = await consoleClient.snippets.syncDraftWorkflow({
params: { snippetId },
body: postParams.params,
body: {
...payload,
hash: syncWorkflowDraftHash || undefined,
},
})
setSyncWorkflowDraftHash(response.hash)
@ -106,7 +102,7 @@ export const useNodesSyncDraft = (snippetId: string) => {
if (isSyncConflictError(error) && !error.bodyUsed) {
error.json().then((err) => {
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft()
handleRefreshWorkflowDraft(onRefresh)
})
}
callback?.onError?.()
@ -114,12 +110,57 @@ export const useNodesSyncDraft = (snippetId: string) => {
finally {
callback?.onSettled?.()
}
}, [getNodesReadOnly, getPostParams, handleRefreshWorkflowDraft, snippetId, workflowStore])
}, [getNodesReadOnly, handleRefreshWorkflowDraft, snippetId, workflowStore])
const syncWorkflowDraftWhenPageClose = useCallback(() => {
if (getNodesReadOnly())
return
const graphPayload = getGraphSyncPayload()
if (!graphPayload)
return
const { syncWorkflowDraftHash } = workflowStore.getState()
postWithKeepalive(`${API_PREFIX}/snippets/${snippetId}/workflows/draft`, {
...graphPayload,
hash: syncWorkflowDraftHash,
})
}, [getGraphSyncPayload, getNodesReadOnly, snippetId, workflowStore])
const performSync = useCallback(async (
notRefreshWhenSyncError?: boolean,
callback?: SyncDraftCallback,
) => {
const graphPayload = getGraphSyncPayload()
if (!graphPayload)
return
await syncDraft(graphPayload, notRefreshWhenSyncError, callback)
}, [getGraphSyncPayload, syncDraft])
const performInputFieldsSync = useCallback(async (
inputFields: SnippetInputField[],
callback?: SyncInputFieldsDraftCallback,
) => {
await syncDraft(
{ input_fields: inputFields },
false,
callback,
(draftWorkflow) => {
const refreshedInputFields = Array.isArray(draftWorkflow.input_fields)
? draftWorkflow.input_fields as SnippetInputField[]
: []
callback?.onRefresh?.(refreshedInputFields)
},
)
}, [syncDraft])
const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly)
const syncInputFieldsDraft = useSerialAsyncCallback(performInputFieldsSync)
return {
doSyncWorkflowDraft,
syncInputFieldsDraft,
syncWorkflowDraftWhenPageClose,
}
}

View File

@ -1,4 +1,5 @@
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
import type { SnippetWorkflow } from '@/types/snippet'
import { useCallback } from 'react'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
@ -8,7 +9,7 @@ export const useSnippetRefreshDraft = (snippetId: string) => {
const workflowStore = useWorkflowStore()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const handleRefreshWorkflowDraft = useCallback(() => {
const handleRefreshWorkflowDraft = useCallback((onSuccess?: (draftWorkflow: SnippetWorkflow) => void) => {
const {
setDraftUpdatedAt,
setIsSyncingWorkflowDraft,
@ -30,6 +31,7 @@ export const useSnippetRefreshDraft = (snippetId: string) => {
} as WorkflowDataUpdater)
setSyncWorkflowDraftHash(response.hash)
setDraftUpdatedAt(response.updated_at)
onSuccess?.(response)
}).finally(() => {
setIsSyncingWorkflowDraft(false)
})

View File

@ -102,8 +102,8 @@ const toSnippetCanvasData = (workflow?: SnippetWorkflow): SnippetCanvasData => {
}
export const buildSnippetDetailPayload = (snippet: SnippetContract, workflow?: SnippetWorkflow): SnippetDetailPayload => {
const inputFields = Array.isArray(snippet.input_fields)
? snippet.input_fields as SnippetInputFieldUIModel[]
const inputFields = Array.isArray(workflow?.input_fields)
? workflow.input_fields as SnippetInputFieldUIModel[]
: []
return {

View File

@ -77,6 +77,7 @@ export type SnippetWorkflow = {
id: string
graph: Record<string, unknown>
features: Record<string, unknown>
input_fields?: SnippetInputField[]
hash: string
created_at: number
updated_at: number
@ -87,7 +88,7 @@ export type SnippetDraftSyncPayload = {
hash?: string
environment_variables?: Record<string, unknown>[]
conversation_variables?: Record<string, unknown>[]
input_variables?: Record<string, unknown>[]
input_fields?: SnippetInputField[]
}
export type SnippetDraftSyncResponse = {