fix(web): input fields form & graph publish

This commit is contained in:
JzoNg
2026-04-16 11:07:26 +08:00
parent 5bfebd371d
commit a9dc57eeef
17 changed files with 465 additions and 23 deletions

View File

@@ -9,7 +9,7 @@ vi.mock('@/app/components/rag-pipeline/components/panel/input-field/hooks', () =
useFloatingRight: (...args: unknown[]) => mockUseFloatingRight(...args),
}))
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/editor/form', () => ({
vi.mock('../input-field-form', () => ({
default: ({ isEditMode }: { isEditMode: boolean }) => (
<div data-testid="snippet-input-field-form">{isEditMode ? 'edit' : 'create'}</div>
),

View File

@@ -5,6 +5,8 @@ describe('PublishMenu', () => {
it('should render the draft summary and publish shortcut', () => {
const { container } = render(
<PublishMenu
draftUpdatedAt={0}
publishedAt={0}
uiMeta={{
inputFieldCount: 1,
checklistCount: 2,
@@ -19,4 +21,22 @@ describe('PublishMenu', () => {
expect(screen.getByRole('button', { name: 'snippet.publishButton' })).toBeInTheDocument()
expect(container.querySelectorAll('.system-kbd')).toHaveLength(3)
})
it('should render published summary when a published version exists', () => {
render(
<PublishMenu
draftUpdatedAt={1_712_300_000_000}
publishedAt={1_712_345_678_000}
uiMeta={{
inputFieldCount: 1,
checklistCount: 2,
autoSavedAt: 'Auto-saved · a few seconds ago',
}}
onPublish={vi.fn()}
/>,
)
expect(screen.getByText('workflow.common.latestPublished')).toBeInTheDocument()
expect(screen.getByText(/workflow\.common\.publishedAt/)).toBeInTheDocument()
})
})

View File

@@ -5,6 +5,7 @@ import { useSnippetPublish } from '../use-snippet-publish'
const mockMutateAsync = vi.fn()
const mockSetPublishMenuOpen = vi.fn()
const mockUseKeyPress = vi.fn()
const mockSetPublishedAt = vi.fn()
let isPublishMenuOpen = false
let isPending = false
@@ -28,6 +29,14 @@ vi.mock('@/service/use-snippet-workflows', () => ({
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useWorkflowStore: () => ({
getState: () => ({
setPublishedAt: mockSetPublishedAt,
}),
}),
}))
vi.mock('../../../store', () => ({
useSnippetDetailStore: (selector: (state: {
isPublishMenuOpen: boolean
@@ -44,7 +53,7 @@ describe('useSnippetPublish', () => {
isPublishMenuOpen = false
isPending = false
shortcutHandler = undefined
mockMutateAsync.mockResolvedValue(undefined)
mockMutateAsync.mockResolvedValue({ created_at: 1_712_345_678 })
mockUseKeyPress.mockImplementation((_key, handler) => {
shortcutHandler = handler
})
@@ -63,6 +72,7 @@ describe('useSnippetPublish', () => {
expect(mockMutateAsync).toHaveBeenCalledWith({
params: { snippetId: 'snippet-1' },
})
expect(mockSetPublishedAt).toHaveBeenCalledWith(1_712_345_678)
expect(mockSetPublishMenuOpen).toHaveBeenCalledWith(false)
expect(toast.success).toHaveBeenCalledWith('snippet.publishSuccess')
})
@@ -104,7 +114,8 @@ describe('useSnippetPublish', () => {
expect(preventDefault).toHaveBeenCalledTimes(1)
})
it('should ignore the shortcut outside the orchestrate section', () => {
it('should ignore the shortcut while publishing is pending', () => {
isPending = true
renderHook(() => useSnippetPublish({
snippetId: 'snippet-1',
}))

View File

@@ -3,6 +3,7 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { toast } from '@/app/components/base/ui/toast'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
import { usePublishSnippetWorkflowMutation } from '@/service/use-snippet-workflows'
import { useSnippetDetailStore } from '../../store'
@@ -15,6 +16,7 @@ export const useSnippetPublish = ({
snippetId,
}: UseSnippetPublishOptions) => {
const { t } = useTranslation('snippet')
const workflowStore = useWorkflowStore()
const publishSnippetMutation = usePublishSnippetWorkflowMutation(snippetId)
const {
isPublishMenuOpen,
@@ -26,16 +28,17 @@ export const useSnippetPublish = ({
const handlePublish = useCallback(async () => {
try {
await publishSnippetMutation.mutateAsync({
const publishedWorkflow = await publishSnippetMutation.mutateAsync({
params: { snippetId },
})
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
setPublishMenuOpen(false)
toast.success(t('publishSuccess'))
}
catch (error) {
toast.error(error instanceof Error ? error.message : t('publishFailed'))
}
}, [publishSnippetMutation, setPublishMenuOpen, snippetId, t])
}, [publishSnippetMutation, setPublishMenuOpen, snippetId, t, workflowStore])
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (event) => {
if (publishSnippetMutation.isPending)

View File

@@ -2,13 +2,12 @@
import type { FormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
import type { SnippetInputField } from '@/models/snippet'
import { RiCloseLine } from '@remixicon/react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import InputFieldForm from '@/app/components/rag-pipeline/components/panel/input-field/editor/form'
import { convertFormDataToINputField, convertToInputFieldFormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/utils'
import { useFloatingRight } from '@/app/components/rag-pipeline/components/panel/input-field/hooks'
import { cn } from '@/utils/classnames'
import InputFieldForm from './input-field-form'
type SnippetInputFieldEditorProps = {
field?: SnippetInputField | null
@@ -43,15 +42,15 @@ const SnippetInputFieldEditor = ({
width: `min(${floatingRightWidth}px, calc(100vw - 24px))`,
}}
>
<div className="flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary system-xl-semibold">
<div className="flex items-center pt-3.5 pr-11 pb-1 pl-4 system-xl-semibold text-text-primary">
{field ? t('inputFieldPanel.editInputField', { ns: 'datasetPipeline' }) : t('inputFieldPanel.addInputField', { ns: 'datasetPipeline' })}
</div>
<button
type="button"
className="absolute right-2.5 top-2.5 flex h-8 w-8 items-center justify-center"
className="absolute top-2.5 right-2.5 flex h-8 w-8 items-center justify-center"
onClick={onClose}
>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
<span aria-hidden className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</button>
<InputFieldForm
initialData={initialData}

View File

@@ -0,0 +1,40 @@
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
import { useSnippetInputFieldConfigurations } from '../hooks'
const mockUseConfigurations = vi.fn()
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks', () => ({
useConfigurations: (...args: unknown[]) => mockUseConfigurations(...args),
}))
describe('useSnippetInputFieldConfigurations', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should make text maxLength configuration optional for snippets only', () => {
mockUseConfigurations.mockReturnValue([
{
variable: 'maxLength',
required: true,
showConditions: [{ variable: 'type', value: PipelineInputVarType.textInput }],
},
{
variable: 'required',
required: true,
showConditions: [],
},
])
const { result } = renderHook(() => useSnippetInputFieldConfigurations({
getFieldValue: vi.fn(),
setFieldValue: vi.fn(),
supportFile: true,
}))
expect(result.current[0].required).toBe(false)
expect(result.current[1].required).toBe(true)
})
})

View File

@@ -0,0 +1,51 @@
import type { TFunction } from 'i18next'
import { describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
import { createSnippetInputFieldSchema, TEXT_MAX_LENGTH } from '../schema'
vi.mock('@/config', () => ({
MAX_VAR_KEY_LENGTH: 30,
}))
const t: TFunction = ((key: string) => key) as unknown as TFunction
describe('createSnippetInputFieldSchema', () => {
const defaultOptions = { maxFileUploadLimit: 10 }
it('should allow text-input maxLength to be omitted', () => {
const schema = createSnippetInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: 'text_var',
label: 'Text',
required: false,
})
expect(result.success).toBe(true)
})
it('should still reject text-input maxLength above the limit', () => {
const schema = createSnippetInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: 'text_var',
label: 'Text',
required: false,
maxLength: TEXT_MAX_LENGTH + 1,
})
expect(result.success).toBe(false)
})
it('should allow paragraph maxLength to be omitted', () => {
const schema = createSnippetInputFieldSchema(PipelineInputVarType.paragraph, t, defaultOptions)
const result = schema.safeParse({
type: 'paragraph',
variable: 'paragraph_var',
label: 'Paragraph',
required: false,
})
expect(result.success).toBe(true)
})
})

View File

@@ -0,0 +1,30 @@
import type { DeepKeys } from '@tanstack/react-form'
import type { FormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
import { useMemo } from 'react'
import { useConfigurations } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks'
import { PipelineInputVarType } from '@/models/pipeline'
type UseSnippetInputFieldConfigurationsProps = {
getFieldValue: (fieldName: DeepKeys<FormData>) => unknown
setFieldValue: (fieldName: DeepKeys<FormData>, value: unknown) => void
supportFile: boolean
}
export const useSnippetInputFieldConfigurations = (props: UseSnippetInputFieldConfigurationsProps) => {
const configurations = useConfigurations(props)
return useMemo(() => {
return configurations.map((configuration) => {
const isTextMaxLengthField = configuration.variable === 'maxLength'
&& configuration.showConditions?.some(condition => condition.value === PipelineInputVarType.textInput)
if (!isTextMaxLengthField)
return configuration
return {
...configuration,
required: false,
}
})
}, [configurations])
}

View File

@@ -0,0 +1,92 @@
import type { FormData, InputFieldFormProps } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
import type { MoreInfo } from '@/app/components/workflow/types'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
import { useAppForm } from '@/app/components/base/form'
import { Button } from '@/app/components/base/ui/button'
import { toast } from '@/app/components/base/ui/toast'
import HiddenFields from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/hidden-fields'
import ShowAllSettings from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/show-all-settings'
import { ChangeType } from '@/app/components/workflow/types'
import { useFileUploadConfig } from '@/service/use-common'
import InitialFields from './initial-fields'
import { createSnippetInputFieldSchema } from './schema'
const SnippetInputFieldForm = ({ initialData, supportFile = false, onCancel, onSubmit, isEditMode = true }: InputFieldFormProps) => {
const { t } = useTranslation()
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const { maxFileUploadLimit } = useFileSizeLimit(fileUploadConfigResponse)
const inputFieldForm = useAppForm({
defaultValues: initialData,
validators: {
onSubmit: ({ value }) => {
const { type } = value
const schema = createSnippetInputFieldSchema(type, t, { maxFileUploadLimit })
const result = schema.safeParse(value)
if (!result.success) {
const issues = result.error.issues
const firstIssue = issues[0]
const errorMessage = `"${firstIssue.path.join('.')}" ${firstIssue.message}`
toast.error(errorMessage)
return errorMessage
}
return undefined
},
},
onSubmit: ({ value }) => {
let moreInfo: MoreInfo | undefined
if (isEditMode && value.variable !== initialData?.variable) {
moreInfo = {
type: ChangeType.changeVarName,
payload: { beforeKey: initialData?.variable || '', afterKey: value.variable },
}
}
onSubmit(value as FormData, moreInfo)
},
})
const [showAllSettings, setShowAllSettings] = useState(false)
const InitialFieldsComp = InitialFields({
initialData,
supportFile,
})
const HiddenFieldsComp = HiddenFields({
initialData,
})
const handleShowAllSettings = useCallback(() => {
setShowAllSettings(true)
}, [])
const ShowAllSettingComp = ShowAllSettings({
initialData,
handleShowAllSettings,
})
return (
<form
className="w-full"
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
inputFieldForm.handleSubmit()
}}
>
<div className="flex flex-col gap-4 px-4 py-2">
<InitialFieldsComp form={inputFieldForm} />
<Divider type="horizontal" />
{!showAllSettings && (<ShowAllSettingComp form={inputFieldForm} />)}
{showAllSettings && (<HiddenFieldsComp form={inputFieldForm} />)}
</div>
<div className="flex items-center justify-end gap-x-2 p-4 pt-2">
<Button variant="secondary" onClick={onCancel}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<inputFieldForm.AppForm>
<inputFieldForm.Actions />
</inputFieldForm.AppForm>
</div>
</form>
)
}
export default SnippetInputFieldForm

View File

@@ -0,0 +1,50 @@
import * as React from 'react'
import { useCallback } from 'react'
import { withForm } from '@/app/components/base/form'
import InputField from '@/app/components/base/form/form-scenarios/input-field/field'
import { useSnippetInputFieldConfigurations } from './hooks'
type InitialFieldsProps = {
initialData?: Record<string, unknown>
supportFile: boolean
}
const InitialFields = ({
initialData,
supportFile,
}: InitialFieldsProps) => withForm({
defaultValues: initialData,
// eslint-disable-next-line react/component-hook-factories
render: function Render({
form,
}) {
const getFieldValue = useCallback((fieldName: string) => {
return form.getFieldValue(fieldName)
}, [form])
const setFieldValue = useCallback((fieldName: string, value: unknown) => {
form.setFieldValue(fieldName, value)
}, [form])
const initialConfigurations = useSnippetInputFieldConfigurations({
getFieldValue,
setFieldValue,
supportFile,
})
return (
<>
{initialConfigurations.map((config) => {
const FieldComponent = InputField({
initialData,
config,
})
const key = `${config.variable}-${config.label}-${config.showConditions.map(condition => String(condition.value)).join('-') || 'default'}`
return <FieldComponent key={key} form={form} />
})}
</>
)
},
})
export default InitialFields

View File

@@ -0,0 +1,95 @@
import type { TFunction } from 'i18next'
import type { SchemaOptions } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
import * as z from 'zod'
import { InputTypeEnum } from '@/app/components/base/form/components/field/input-type-select/types'
import { MAX_VAR_KEY_LENGTH } from '@/config'
import { PipelineInputVarType } from '@/models/pipeline'
export const TEXT_MAX_LENGTH = 256
const TransferMethod = z.enum([
'all',
'local_file',
'remote_url',
])
const SupportedFileTypes = z.enum([
'image',
'document',
'video',
'audio',
'custom',
])
export const createSnippetInputFieldSchema = (type: PipelineInputVarType, t: TFunction, options: SchemaOptions) => {
const { maxFileUploadLimit } = options
const commonSchema = z.object({
type: InputTypeEnum,
variable: z.string().nonempty({
message: t('varKeyError.canNoBeEmpty', { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
}).max(MAX_VAR_KEY_LENGTH, {
message: t('varKeyError.tooLong', { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
}).regex(/^(?!\d)\w+/, {
message: t('varKeyError.notStartWithNumber', { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
}).regex(/^[a-z_]\w{0,29}$/i, {
message: t('varKeyError.notValid', { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }),
}),
label: z.string().nonempty({
message: t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' }),
}),
required: z.boolean(),
tooltips: z.string().optional(),
})
if (type === PipelineInputVarType.textInput || type === PipelineInputVarType.paragraph) {
return z.object({
maxLength: z.number().min(1).max(TEXT_MAX_LENGTH).optional(),
default: z.string().optional(),
}).merge(commonSchema).passthrough()
}
if (type === PipelineInputVarType.number) {
return z.object({
default: z.number().optional(),
unit: z.string().optional(),
placeholder: z.string().optional(),
}).merge(commonSchema).passthrough()
}
if (type === PipelineInputVarType.select) {
return z.object({
options: z.array(z.string()).nonempty({
message: t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }),
}).refine(
arr => new Set(arr).size === arr.length,
{
message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }),
},
),
default: z.string().optional(),
}).merge(commonSchema).passthrough()
}
if (type === PipelineInputVarType.singleFile) {
return z.object({
allowedFileUploadMethods: z.array(TransferMethod),
allowedTypesAndExtensions: z.object({
allowedFileExtensions: z.array(z.string()).optional(),
allowedFileTypes: z.array(SupportedFileTypes),
}),
}).merge(commonSchema).passthrough()
}
if (type === PipelineInputVarType.multiFiles) {
return z.object({
allowedFileUploadMethods: z.array(TransferMethod),
allowedTypesAndExtensions: z.object({
allowedFileExtensions: z.array(z.string()).optional(),
allowedFileTypes: z.array(SupportedFileTypes),
}),
maxLength: z.number().min(1).max(maxFileUploadLimit),
}).merge(commonSchema).passthrough()
}
return commonSchema.passthrough()
}

View File

@@ -4,26 +4,40 @@ import type { SnippetDetailUIModel } from '@/models/snippet'
import { useTranslation } from 'react-i18next'
import { Button } from '@/app/components/base/ui/button'
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
const PublishMenu = ({
draftUpdatedAt,
publishedAt,
uiMeta,
onPublish,
isPublishing = false,
}: {
draftUpdatedAt: number
publishedAt: number
uiMeta: SnippetDetailUIModel
onPublish: () => void
isPublishing?: boolean
}) => {
const { t } = useTranslation('snippet')
const { formatTimeFromNow } = useFormatTimeFromNow()
const hasPublishedVersion = Boolean(publishedAt)
return (
<div className="flex flex-col gap-3 px-4 pb-4 pt-3">
<div className="flex flex-col gap-3 px-4 pt-3 pb-4">
<div className="flex flex-col">
<div className="min-h-6 text-text-tertiary system-xs-medium-uppercase">
{t('publishMenuCurrentDraft')}
<div className="min-h-6 system-xs-medium-uppercase text-text-tertiary">
{hasPublishedVersion
? t('common.latestPublished', { ns: 'workflow' })
: t('publishMenuCurrentDraft')}
</div>
<div className="text-text-secondary system-sm-medium">
{uiMeta.autoSavedAt}
<div className="system-sm-medium text-text-secondary">
{hasPublishedVersion
? `${t('common.publishedAt', { ns: 'workflow' })} ${formatTimeFromNow(publishedAt)}`
: draftUpdatedAt
? `${t('common.autoSaved', { ns: 'workflow' })} · ${formatTimeFromNow(draftUpdatedAt)}`
: uiMeta.autoSavedAt}
</div>
</div>
<Button

View File

@@ -7,6 +7,7 @@ import {
useMemo,
} from 'react'
import Header from '@/app/components/workflow/header'
import { useStore } from '@/app/components/workflow/store'
import InputFieldButton from './input-field-button'
import Publisher from './publisher'
import RunMode from './run-mode'
@@ -32,6 +33,8 @@ const SnippetHeader = ({
onPublishMenuOpenChange,
onPublish,
}: SnippetHeaderProps) => {
const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
const publishedAt = useStore(state => state.publishedAt)
const viewHistoryProps = useMemo(() => {
return {
historyUrl: `/snippets/${snippetId}/workflow-runs`,
@@ -46,10 +49,12 @@ const SnippetHeader = ({
middle: (
<Publisher
uiMeta={uiMeta}
draftUpdatedAt={draftUpdatedAt}
open={isPublishMenuOpen}
isPublishing={isPublishing}
onOpenChange={onPublishMenuOpenChange}
onPublish={onPublish}
publishedAt={publishedAt}
/>
),
},
@@ -69,7 +74,7 @@ const SnippetHeader = ({
viewHistoryProps,
},
}
}, [inputFieldCount, isPublishMenuOpen, isPublishing, onPublish, onPublishMenuOpenChange, onToggleInputPanel, uiMeta, viewHistoryProps])
}, [draftUpdatedAt, inputFieldCount, isPublishMenuOpen, isPublishing, onPublish, onPublishMenuOpenChange, onToggleInputPanel, publishedAt, uiMeta, viewHistoryProps])
return <Header {...headerProps} />
}

View File

@@ -12,25 +12,29 @@ import PublishMenu from '../publish-menu'
type PublisherProps = {
uiMeta: SnippetDetailUIModel
draftUpdatedAt: number
open: boolean
isPublishing: boolean
onOpenChange: (open: boolean) => void
onPublish: () => void
publishedAt: number
}
const Publisher = ({
uiMeta,
draftUpdatedAt,
open,
isPublishing,
onOpenChange,
onPublish,
publishedAt,
}: PublisherProps) => {
const { t } = useTranslation('snippet')
return (
<DropdownMenu open={open} onOpenChange={onOpenChange}>
<DropdownMenuTrigger className="flex items-center gap-1 rounded-lg bg-components-button-primary-bg px-3 py-2 text-white shadow-[0px_2px_2px_-1px_rgba(0,0,0,0.12),0px_1px_1px_-1px_rgba(0,0,0,0.12),0px_0px_0px_0.5px_rgba(9,9,11,0.05)]">
<span className="text-[13px] font-medium leading-4">{t('publishButton')}</span>
<span className="text-[13px] leading-4 font-medium">{t('publishButton')}</span>
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
@@ -39,6 +43,8 @@ const Publisher = ({
popupClassName="w-80 !rounded-2xl !bg-components-panel-bg !p-0 !shadow-[0px_20px_24px_-4px_rgba(9,9,11,0.08),0px_8px_8px_-4px_rgba(9,9,11,0.03)]"
>
<PublishMenu
draftUpdatedAt={draftUpdatedAt}
publishedAt={publishedAt}
uiMeta={uiMeta}
isPublishing={isPublishing}
onPublish={onPublish}

View File

@@ -202,7 +202,7 @@ describe('useSnippetInit', () => {
onSuccess?.({
created_at: 1_712_345_678,
})
return { data: undefined, isLoading: false }
return { data: { created_at: 1_712_345_678 }, isLoading: false }
})
renderHook(() => useSnippetInit('snippet-1'))
@@ -210,6 +210,17 @@ describe('useSnippetInit', () => {
expect(mockSetPublishedAt).toHaveBeenCalledWith(1_712_345_678)
})
it('should reset published metadata when the published workflow is unavailable', () => {
mockUseSnippetPublishedWorkflow.mockReturnValue({
data: undefined,
isLoading: false,
})
renderHook(() => useSnippetInit('snippet-1'))
expect(mockSetPublishedAt).toHaveBeenCalledWith(0)
})
it('should stay loading while draft workflow is still fetching', () => {
mockUseSnippetDraftWorkflow.mockReturnValue({
data: undefined,

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
import {
useSnippetDefaultBlockConfigs,
@@ -54,10 +54,17 @@ export const useSnippetInit = (snippetId: string) => {
nodesDefaultConfigs: normalizeNodesDefaultConfigs(nodesDefaultConfigs),
})
})
useSnippetPublishedWorkflow(snippetId, (publishedWorkflow) => {
const publishedWorkflowQuery = useSnippetPublishedWorkflow(snippetId, (publishedWorkflow) => {
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
})
useEffect(() => {
if (publishedWorkflowQuery.isLoading)
return
workflowStore.getState().setPublishedAt(publishedWorkflowQuery.data?.created_at ?? 0)
}, [publishedWorkflowQuery.data?.created_at, publishedWorkflowQuery.isLoading, workflowStore])
const mockData = useMemo(() => getSnippetDetailMock(snippetId), [snippetId])
const shouldUseMockData = !snippetApiDetail.isLoading && !snippetApiDetail.data && !!mockData

View File

@@ -77,9 +77,17 @@ export const useSnippetPublishedWorkflow = (
return useQuery({
...queryOptions,
queryFn: async (context) => {
const publishedWorkflow = await queryOptions.queryFn(context)
onSuccess?.(publishedWorkflow)
return publishedWorkflow
try {
const publishedWorkflow = await queryOptions.queryFn(context)
onSuccess?.(publishedWorkflow)
return publishedWorkflow
}
catch (error) {
if (isNotFoundError(error))
return undefined
throw error
}
},
})
}