diff --git a/web/app/components/datasets/create/step-one/components/data-source-type-selector.tsx b/web/app/components/datasets/create/step-one/components/data-source-type-selector.tsx
new file mode 100644
index 0000000000..6bdc2ace56
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/components/data-source-type-selector.tsx
@@ -0,0 +1,97 @@
+'use client'
+
+import { useCallback, useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
+import { DataSourceType } from '@/models/datasets'
+import { cn } from '@/utils/classnames'
+import s from '../index.module.css'
+
+type DataSourceTypeSelectorProps = {
+ currentType: DataSourceType
+ disabled: boolean
+ onChange: (type: DataSourceType) => void
+ onClearPreviews: (type: DataSourceType) => void
+}
+
+type DataSourceLabelKey
+ = | 'stepOne.dataSourceType.file'
+ | 'stepOne.dataSourceType.notion'
+ | 'stepOne.dataSourceType.web'
+
+type DataSourceOption = {
+ type: DataSourceType
+ iconClass?: string
+ labelKey: DataSourceLabelKey
+}
+
+const DATA_SOURCE_OPTIONS: DataSourceOption[] = [
+ {
+ type: DataSourceType.FILE,
+ labelKey: 'stepOne.dataSourceType.file',
+ },
+ {
+ type: DataSourceType.NOTION,
+ iconClass: s.notion,
+ labelKey: 'stepOne.dataSourceType.notion',
+ },
+ {
+ type: DataSourceType.WEB,
+ iconClass: s.web,
+ labelKey: 'stepOne.dataSourceType.web',
+ },
+]
+
+/**
+ * Data source type selector component for choosing between file, notion, and web sources.
+ */
+function DataSourceTypeSelector({
+ currentType,
+ disabled,
+ onChange,
+ onClearPreviews,
+}: DataSourceTypeSelectorProps) {
+ const { t } = useTranslation()
+
+ const isWebEnabled = ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL
+
+ const handleTypeChange = useCallback((type: DataSourceType) => {
+ if (disabled)
+ return
+ onChange(type)
+ onClearPreviews(type)
+ }, [disabled, onChange, onClearPreviews])
+
+ const visibleOptions = useMemo(() => DATA_SOURCE_OPTIONS.filter((option) => {
+ if (option.type === DataSourceType.WEB)
+ return isWebEnabled
+ return true
+ }), [isWebEnabled])
+
+ return (
+
+ {visibleOptions.map(option => (
+
handleTypeChange(option.type)}
+ >
+
+
+ {t(option.labelKey, { ns: 'datasetCreation' })}
+
+
+ ))}
+
+ )
+}
+
+export default DataSourceTypeSelector
diff --git a/web/app/components/datasets/create/step-one/components/index.ts b/web/app/components/datasets/create/step-one/components/index.ts
new file mode 100644
index 0000000000..5271835741
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/components/index.ts
@@ -0,0 +1,3 @@
+export { default as DataSourceTypeSelector } from './data-source-type-selector'
+export { default as NextStepButton } from './next-step-button'
+export { default as PreviewPanel } from './preview-panel'
diff --git a/web/app/components/datasets/create/step-one/components/next-step-button.tsx b/web/app/components/datasets/create/step-one/components/next-step-button.tsx
new file mode 100644
index 0000000000..71e4e87fcf
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/components/next-step-button.tsx
@@ -0,0 +1,30 @@
+'use client'
+
+import { RiArrowRightLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import Button from '@/app/components/base/button'
+
+type NextStepButtonProps = {
+ disabled: boolean
+ onClick: () => void
+}
+
+/**
+ * Reusable next step button component for dataset creation flow.
+ */
+function NextStepButton({ disabled, onClick }: NextStepButtonProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t('stepOne.button', { ns: 'datasetCreation' })}
+
+
+
+
+ )
+}
+
+export default NextStepButton
diff --git a/web/app/components/datasets/create/step-one/components/preview-panel.tsx b/web/app/components/datasets/create/step-one/components/preview-panel.tsx
new file mode 100644
index 0000000000..8ae0b7df55
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/components/preview-panel.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+import type { NotionPage } from '@/models/common'
+import type { CrawlResultItem } from '@/models/datasets'
+import { useTranslation } from 'react-i18next'
+import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
+import FilePreview from '../../file-preview'
+import NotionPagePreview from '../../notion-page-preview'
+import WebsitePreview from '../../website/preview'
+
+type PreviewPanelProps = {
+ currentFile: File | undefined
+ currentNotionPage: NotionPage | undefined
+ currentWebsite: CrawlResultItem | undefined
+ notionCredentialId: string
+ isShowPlanUpgradeModal: boolean
+ hideFilePreview: () => void
+ hideNotionPagePreview: () => void
+ hideWebsitePreview: () => void
+ hidePlanUpgradeModal: () => void
+}
+
+/**
+ * Right panel component for displaying file, notion page, or website previews.
+ */
+function PreviewPanel({
+ currentFile,
+ currentNotionPage,
+ currentWebsite,
+ notionCredentialId,
+ isShowPlanUpgradeModal,
+ hideFilePreview,
+ hideNotionPagePreview,
+ hideWebsitePreview,
+ hidePlanUpgradeModal,
+}: PreviewPanelProps) {
+ const { t } = useTranslation()
+
+ return (
+
+ {currentFile &&
}
+ {currentNotionPage && (
+
+ )}
+ {currentWebsite &&
}
+ {isShowPlanUpgradeModal && (
+
+ )}
+
+ )
+}
+
+export default PreviewPanel
diff --git a/web/app/components/datasets/create/step-one/hooks/index.ts b/web/app/components/datasets/create/step-one/hooks/index.ts
new file mode 100644
index 0000000000..bae5ce4fce
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/hooks/index.ts
@@ -0,0 +1,2 @@
+export { default as usePreviewState } from './use-preview-state'
+export type { PreviewActions, PreviewState, UsePreviewStateReturn } from './use-preview-state'
diff --git a/web/app/components/datasets/create/step-one/hooks/use-preview-state.ts b/web/app/components/datasets/create/step-one/hooks/use-preview-state.ts
new file mode 100644
index 0000000000..3984947ab1
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/hooks/use-preview-state.ts
@@ -0,0 +1,70 @@
+'use client'
+
+import type { NotionPage } from '@/models/common'
+import type { CrawlResultItem } from '@/models/datasets'
+import { useCallback, useState } from 'react'
+
+export type PreviewState = {
+ currentFile: File | undefined
+ currentNotionPage: NotionPage | undefined
+ currentWebsite: CrawlResultItem | undefined
+}
+
+export type PreviewActions = {
+ showFilePreview: (file: File) => void
+ hideFilePreview: () => void
+ showNotionPagePreview: (page: NotionPage) => void
+ hideNotionPagePreview: () => void
+ showWebsitePreview: (website: CrawlResultItem) => void
+ hideWebsitePreview: () => void
+}
+
+export type UsePreviewStateReturn = PreviewState & PreviewActions
+
+/**
+ * Custom hook for managing preview state across different data source types.
+ * Handles file, notion page, and website preview visibility.
+ */
+function usePreviewState(): UsePreviewStateReturn {
+ const [currentFile, setCurrentFile] = useState()
+ const [currentNotionPage, setCurrentNotionPage] = useState()
+ const [currentWebsite, setCurrentWebsite] = useState()
+
+ const showFilePreview = useCallback((file: File) => {
+ setCurrentFile(file)
+ }, [])
+
+ const hideFilePreview = useCallback(() => {
+ setCurrentFile(undefined)
+ }, [])
+
+ const showNotionPagePreview = useCallback((page: NotionPage) => {
+ setCurrentNotionPage(page)
+ }, [])
+
+ const hideNotionPagePreview = useCallback(() => {
+ setCurrentNotionPage(undefined)
+ }, [])
+
+ const showWebsitePreview = useCallback((website: CrawlResultItem) => {
+ setCurrentWebsite(website)
+ }, [])
+
+ const hideWebsitePreview = useCallback(() => {
+ setCurrentWebsite(undefined)
+ }, [])
+
+ return {
+ currentFile,
+ currentNotionPage,
+ currentWebsite,
+ showFilePreview,
+ hideFilePreview,
+ showNotionPagePreview,
+ hideNotionPagePreview,
+ showWebsitePreview,
+ hideWebsitePreview,
+ }
+}
+
+export default usePreviewState
diff --git a/web/app/components/datasets/create/step-one/index.spec.tsx b/web/app/components/datasets/create/step-one/index.spec.tsx
new file mode 100644
index 0000000000..1ff77dc1f6
--- /dev/null
+++ b/web/app/components/datasets/create/step-one/index.spec.tsx
@@ -0,0 +1,1204 @@
+import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
+import type { NotionPage } from '@/models/common'
+import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/datasets'
+import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'
+import { Plan } from '@/app/components/billing/type'
+import { DataSourceType } from '@/models/datasets'
+import { DataSourceTypeSelector, NextStepButton, PreviewPanel } from './components'
+import { usePreviewState } from './hooks'
+import StepOne from './index'
+
+// ==========================================
+// Mock External Dependencies
+// ==========================================
+
+// Mock config for website crawl features
+vi.mock('@/config', () => ({
+ ENABLE_WEBSITE_FIRECRAWL: true,
+ ENABLE_WEBSITE_JINAREADER: false,
+ ENABLE_WEBSITE_WATERCRAWL: false,
+}))
+
+// Mock dataset detail context
+let mockDatasetDetail: DataSet | undefined
+vi.mock('@/context/dataset-detail', () => ({
+ useDatasetDetailContextWithSelector: (selector: (state: { dataset: DataSet | undefined }) => DataSet | undefined) => {
+ return selector({ dataset: mockDatasetDetail })
+ },
+}))
+
+// Mock provider context
+let mockPlan = {
+ type: Plan.professional,
+ usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
+ total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
+}
+let mockEnableBilling = false
+
+vi.mock('@/context/provider-context', () => ({
+ useProviderContext: () => ({
+ plan: mockPlan,
+ enableBilling: mockEnableBilling,
+ }),
+}))
+
+// Mock child components
+vi.mock('../file-uploader', () => ({
+ default: ({ onPreview, fileList }: { onPreview: (file: File) => void, fileList: FileItem[] }) => (
+
+ {fileList.length}
+ onPreview(new File(['test'], 'test.txt'))}>
+ Preview
+
+
+ ),
+}))
+
+vi.mock('../website', () => ({
+ default: ({ onPreview }: { onPreview: (item: CrawlResultItem) => void }) => (
+
+ onPreview({ title: 'Test', markdown: '', description: '', source_url: 'https://test.com' })}
+ >
+ Preview Website
+
+
+ ),
+}))
+
+vi.mock('../empty-dataset-creation-modal', () => ({
+ default: ({ show, onHide }: { show: boolean, onHide: () => void }) => (
+ show
+ ? (
+
+ Close
+
+ )
+ : null
+ ),
+}))
+
+// NotionConnector is a base component - imported directly without mock
+// It only depends on i18n which is globally mocked
+
+vi.mock('@/app/components/base/notion-page-selector', () => ({
+ NotionPageSelector: ({ onPreview }: { onPreview: (page: NotionPage) => void }) => (
+
+ onPreview({ page_id: 'page-1', type: 'page' } as NotionPage)}
+ >
+ Preview Notion
+
+
+ ),
+}))
+
+vi.mock('@/app/components/billing/vector-space-full', () => ({
+ default: () => Vector Space Full
,
+}))
+
+vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
+ default: ({ show, onClose }: { show: boolean, onClose: () => void }) => (
+ show
+ ? (
+
+ Close
+
+ )
+ : null
+ ),
+}))
+
+vi.mock('../file-preview', () => ({
+ default: ({ file, hidePreview }: { file: File, hidePreview: () => void }) => (
+
+ {file.name}
+ Hide
+
+ ),
+}))
+
+vi.mock('../notion-page-preview', () => ({
+ default: ({ currentPage, hidePreview }: { currentPage: NotionPage, hidePreview: () => void }) => (
+
+ {currentPage.page_id}
+ Hide
+
+ ),
+}))
+
+// WebsitePreview is a sibling component without API dependencies - imported directly
+// It only depends on i18n which is globally mocked
+
+vi.mock('./upgrade-card', () => ({
+ default: () => Upgrade Card
,
+}))
+
+// ==========================================
+// Test Data Builders
+// ==========================================
+
+const createMockCustomFile = (overrides: { id?: string, name?: string } = {}) => {
+ const file = new File(['test content'], overrides.name ?? 'test.txt', { type: 'text/plain' })
+ return Object.assign(file, {
+ id: overrides.id ?? 'uploaded-id',
+ extension: 'txt',
+ mime_type: 'text/plain',
+ created_by: 'user-1',
+ created_at: Date.now(),
+ })
+}
+
+const createMockFileItem = (overrides: Partial = {}): FileItem => ({
+ fileID: `file-${Date.now()}`,
+ file: createMockCustomFile(overrides.file as { id?: string, name?: string }),
+ progress: 100,
+ ...overrides,
+})
+
+const createMockNotionPage = (overrides: Partial = {}): NotionPage => ({
+ page_id: `page-${Date.now()}`,
+ type: 'page',
+ ...overrides,
+} as NotionPage)
+
+const createMockCrawlResult = (overrides: Partial = {}): CrawlResultItem => ({
+ title: 'Test Page',
+ markdown: 'Test content',
+ description: 'Test description',
+ source_url: 'https://example.com',
+ ...overrides,
+})
+
+const createMockDataSourceAuth = (overrides: Partial = {}): DataSourceAuth => ({
+ credential_id: 'cred-1',
+ provider: 'notion_datasource',
+ plugin_id: 'plugin-1',
+ credentials_list: [{ id: 'cred-1', name: 'Workspace 1' }],
+ ...overrides,
+} as DataSourceAuth)
+
+const defaultProps = {
+ dataSourceType: DataSourceType.FILE,
+ dataSourceTypeDisable: false,
+ onSetting: vi.fn(),
+ files: [] as FileItem[],
+ updateFileList: vi.fn(),
+ updateFile: vi.fn(),
+ notionPages: [] as NotionPage[],
+ notionCredentialId: '',
+ updateNotionPages: vi.fn(),
+ updateNotionCredentialId: vi.fn(),
+ onStepChange: vi.fn(),
+ changeType: vi.fn(),
+ websitePages: [] as CrawlResultItem[],
+ updateWebsitePages: vi.fn(),
+ onWebsiteCrawlProviderChange: vi.fn(),
+ onWebsiteCrawlJobIdChange: vi.fn(),
+ crawlOptions: {
+ crawl_sub_pages: true,
+ only_main_content: true,
+ includes: '',
+ excludes: '',
+ limit: 10,
+ max_depth: '',
+ use_sitemap: true,
+ } as CrawlOptions,
+ onCrawlOptionsChange: vi.fn(),
+ authedDataSourceList: [] as DataSourceAuth[],
+}
+
+// ==========================================
+// usePreviewState Hook Tests
+// ==========================================
+describe('usePreviewState Hook', () => {
+ // --------------------------------------------------------------------------
+ // Initial State Tests
+ // --------------------------------------------------------------------------
+ describe('Initial State', () => {
+ it('should initialize with all preview states undefined', () => {
+ // Arrange & Act
+ const { result } = renderHook(() => usePreviewState())
+
+ // Assert
+ expect(result.current.currentFile).toBeUndefined()
+ expect(result.current.currentNotionPage).toBeUndefined()
+ expect(result.current.currentWebsite).toBeUndefined()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // File Preview Tests
+ // --------------------------------------------------------------------------
+ describe('File Preview', () => {
+ it('should show file preview when showFilePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockFile = new File(['test'], 'test.txt')
+
+ // Act
+ act(() => {
+ result.current.showFilePreview(mockFile)
+ })
+
+ // Assert
+ expect(result.current.currentFile).toBe(mockFile)
+ })
+
+ it('should hide file preview when hideFilePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockFile = new File(['test'], 'test.txt')
+
+ act(() => {
+ result.current.showFilePreview(mockFile)
+ })
+
+ // Act
+ act(() => {
+ result.current.hideFilePreview()
+ })
+
+ // Assert
+ expect(result.current.currentFile).toBeUndefined()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Notion Page Preview Tests
+ // --------------------------------------------------------------------------
+ describe('Notion Page Preview', () => {
+ it('should show notion page preview when showNotionPagePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockPage = createMockNotionPage()
+
+ // Act
+ act(() => {
+ result.current.showNotionPagePreview(mockPage)
+ })
+
+ // Assert
+ expect(result.current.currentNotionPage).toBe(mockPage)
+ })
+
+ it('should hide notion page preview when hideNotionPagePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockPage = createMockNotionPage()
+
+ act(() => {
+ result.current.showNotionPagePreview(mockPage)
+ })
+
+ // Act
+ act(() => {
+ result.current.hideNotionPagePreview()
+ })
+
+ // Assert
+ expect(result.current.currentNotionPage).toBeUndefined()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Website Preview Tests
+ // --------------------------------------------------------------------------
+ describe('Website Preview', () => {
+ it('should show website preview when showWebsitePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockWebsite = createMockCrawlResult()
+
+ // Act
+ act(() => {
+ result.current.showWebsitePreview(mockWebsite)
+ })
+
+ // Assert
+ expect(result.current.currentWebsite).toBe(mockWebsite)
+ })
+
+ it('should hide website preview when hideWebsitePreview is called', () => {
+ // Arrange
+ const { result } = renderHook(() => usePreviewState())
+ const mockWebsite = createMockCrawlResult()
+
+ act(() => {
+ result.current.showWebsitePreview(mockWebsite)
+ })
+
+ // Act
+ act(() => {
+ result.current.hideWebsitePreview()
+ })
+
+ // Assert
+ expect(result.current.currentWebsite).toBeUndefined()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Callback Stability Tests (Memoization)
+ // --------------------------------------------------------------------------
+ describe('Callback Stability', () => {
+ it('should maintain stable showFilePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.showFilePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.showFilePreview).toBe(initialCallback)
+ })
+
+ it('should maintain stable hideFilePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.hideFilePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.hideFilePreview).toBe(initialCallback)
+ })
+
+ it('should maintain stable showNotionPagePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.showNotionPagePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.showNotionPagePreview).toBe(initialCallback)
+ })
+
+ it('should maintain stable hideNotionPagePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.hideNotionPagePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.hideNotionPagePreview).toBe(initialCallback)
+ })
+
+ it('should maintain stable showWebsitePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.showWebsitePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.showWebsitePreview).toBe(initialCallback)
+ })
+
+ it('should maintain stable hideWebsitePreview callback reference', () => {
+ // Arrange
+ const { result, rerender } = renderHook(() => usePreviewState())
+ const initialCallback = result.current.hideWebsitePreview
+
+ // Act
+ rerender()
+
+ // Assert
+ expect(result.current.hideWebsitePreview).toBe(initialCallback)
+ })
+ })
+})
+
+// ==========================================
+// DataSourceTypeSelector Component Tests
+// ==========================================
+describe('DataSourceTypeSelector', () => {
+ const defaultSelectorProps = {
+ currentType: DataSourceType.FILE,
+ disabled: false,
+ onChange: vi.fn(),
+ onClearPreviews: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // --------------------------------------------------------------------------
+ // Rendering Tests
+ // --------------------------------------------------------------------------
+ describe('Rendering', () => {
+ it('should render all data source options when web is enabled', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByText('datasetCreation.stepOne.dataSourceType.file')).toBeInTheDocument()
+ expect(screen.getByText('datasetCreation.stepOne.dataSourceType.notion')).toBeInTheDocument()
+ expect(screen.getByText('datasetCreation.stepOne.dataSourceType.web')).toBeInTheDocument()
+ })
+
+ it('should highlight active type', () => {
+ // Arrange & Act
+ const { container } = render(
+ ,
+ )
+
+ // Assert - The active item should have the active class
+ const items = container.querySelectorAll('[class*="dataSourceItem"]')
+ expect(items.length).toBeGreaterThan(0)
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // User Interactions Tests
+ // --------------------------------------------------------------------------
+ describe('User Interactions', () => {
+ it('should call onChange when a type is clicked', () => {
+ // Arrange
+ const onChange = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
+
+ // Assert
+ expect(onChange).toHaveBeenCalledWith(DataSourceType.NOTION)
+ })
+
+ it('should call onClearPreviews when a type is clicked', () => {
+ // Arrange
+ const onClearPreviews = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.web'))
+
+ // Assert
+ expect(onClearPreviews).toHaveBeenCalledWith(DataSourceType.WEB)
+ })
+
+ it('should not call onChange when disabled', () => {
+ // Arrange
+ const onChange = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
+
+ // Assert
+ expect(onChange).not.toHaveBeenCalled()
+ })
+
+ it('should not call onClearPreviews when disabled', () => {
+ // Arrange
+ const onClearPreviews = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
+
+ // Assert
+ expect(onClearPreviews).not.toHaveBeenCalled()
+ })
+ })
+})
+
+// ==========================================
+// NextStepButton Component Tests
+// ==========================================
+describe('NextStepButton', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // --------------------------------------------------------------------------
+ // Rendering Tests
+ // --------------------------------------------------------------------------
+ describe('Rendering', () => {
+ it('should render with correct label', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
+ })
+
+ it('should render with arrow icon', () => {
+ // Arrange & Act
+ const { container } = render( )
+
+ // Assert
+ const svgIcon = container.querySelector('svg')
+ expect(svgIcon).toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Props Tests
+ // --------------------------------------------------------------------------
+ describe('Props', () => {
+ it('should be disabled when disabled prop is true', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByRole('button')).toBeDisabled()
+ })
+
+ it('should be enabled when disabled prop is false', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByRole('button')).not.toBeDisabled()
+ })
+
+ it('should call onClick when clicked and not disabled', () => {
+ // Arrange
+ const onClick = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByRole('button'))
+
+ // Assert
+ expect(onClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('should not call onClick when clicked and disabled', () => {
+ // Arrange
+ const onClick = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByRole('button'))
+
+ // Assert
+ expect(onClick).not.toHaveBeenCalled()
+ })
+ })
+})
+
+// ==========================================
+// PreviewPanel Component Tests
+// ==========================================
+describe('PreviewPanel', () => {
+ const defaultPreviewProps = {
+ currentFile: undefined as File | undefined,
+ currentNotionPage: undefined as NotionPage | undefined,
+ currentWebsite: undefined as CrawlResultItem | undefined,
+ notionCredentialId: 'cred-1',
+ isShowPlanUpgradeModal: false,
+ hideFilePreview: vi.fn(),
+ hideNotionPagePreview: vi.fn(),
+ hideWebsitePreview: vi.fn(),
+ hidePlanUpgradeModal: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ // --------------------------------------------------------------------------
+ // Conditional Rendering Tests
+ // --------------------------------------------------------------------------
+ describe('Conditional Rendering', () => {
+ it('should not render FilePreview when currentFile is undefined', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
+ })
+
+ it('should render FilePreview when currentFile is defined', () => {
+ // Arrange
+ const file = new File(['test'], 'test.txt')
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('file-preview')).toBeInTheDocument()
+ })
+
+ it('should not render NotionPagePreview when currentNotionPage is undefined', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.queryByTestId('notion-page-preview')).not.toBeInTheDocument()
+ })
+
+ it('should render NotionPagePreview when currentNotionPage is defined', () => {
+ // Arrange
+ const page = createMockNotionPage()
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('notion-page-preview')).toBeInTheDocument()
+ })
+
+ it('should not render WebsitePreview when currentWebsite is undefined', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert - pagePreview is the title shown in WebsitePreview
+ expect(screen.queryByText('datasetCreation.stepOne.pagePreview')).not.toBeInTheDocument()
+ })
+
+ it('should render WebsitePreview when currentWebsite is defined', () => {
+ // Arrange
+ const website = createMockCrawlResult()
+
+ // Act
+ render( )
+
+ // Assert - Check for the preview title and source URL
+ expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
+ expect(screen.getByText(website.source_url)).toBeInTheDocument()
+ })
+
+ it('should not render PlanUpgradeModal when isShowPlanUpgradeModal is false', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.queryByTestId('plan-upgrade-modal')).not.toBeInTheDocument()
+ })
+
+ it('should render PlanUpgradeModal when isShowPlanUpgradeModal is true', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Event Handler Tests
+ // --------------------------------------------------------------------------
+ describe('Event Handlers', () => {
+ it('should call hideFilePreview when file preview close is clicked', () => {
+ // Arrange
+ const hideFilePreview = vi.fn()
+ const file = new File(['test'], 'test.txt')
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('hide-file-preview'))
+
+ // Assert
+ expect(hideFilePreview).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call hideNotionPagePreview when notion preview close is clicked', () => {
+ // Arrange
+ const hideNotionPagePreview = vi.fn()
+ const page = createMockNotionPage()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('hide-notion-preview'))
+
+ // Assert
+ expect(hideNotionPagePreview).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call hideWebsitePreview when website preview close is clicked', () => {
+ // Arrange
+ const hideWebsitePreview = vi.fn()
+ const website = createMockCrawlResult()
+ const { container } = render( )
+
+ // Act - Find the close button (div with cursor-pointer class containing the XMarkIcon)
+ const closeButton = container.querySelector('.cursor-pointer')
+ expect(closeButton).toBeInTheDocument()
+ fireEvent.click(closeButton!)
+
+ // Assert
+ expect(hideWebsitePreview).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call hidePlanUpgradeModal when modal close is clicked', () => {
+ // Arrange
+ const hidePlanUpgradeModal = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('close-upgrade-modal'))
+
+ // Assert
+ expect(hidePlanUpgradeModal).toHaveBeenCalledTimes(1)
+ })
+ })
+})
+
+// ==========================================
+// StepOne Component Tests
+// ==========================================
+describe('StepOne', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockDatasetDetail = undefined
+ mockPlan = {
+ type: Plan.professional,
+ usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
+ total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
+ }
+ mockEnableBilling = false
+ })
+
+ // --------------------------------------------------------------------------
+ // Rendering Tests
+ // --------------------------------------------------------------------------
+ describe('Rendering', () => {
+ it('should render without crashing', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
+ })
+
+ it('should render DataSourceTypeSelector when not editing existing dataset', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByText('datasetCreation.stepOne.dataSourceType.file')).toBeInTheDocument()
+ })
+
+ it('should render FileUploader when dataSourceType is FILE', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('file-uploader')).toBeInTheDocument()
+ })
+
+ it('should render NotionConnector when dataSourceType is NOTION and not authenticated', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert - NotionConnector shows sync title and connect button
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.connect/i })).toBeInTheDocument()
+ })
+
+ it('should render NotionPageSelector when dataSourceType is NOTION and authenticated', () => {
+ // Arrange
+ const authedDataSourceList = [createMockDataSourceAuth()]
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
+ })
+
+ it('should render Website when dataSourceType is WEB', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('website')).toBeInTheDocument()
+ })
+
+ it('should render empty dataset creation link when no datasetId', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation')).toBeInTheDocument()
+ })
+
+ it('should not render empty dataset creation link when datasetId exists', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert
+ expect(screen.queryByText('datasetCreation.stepOne.emptyDatasetCreation')).not.toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Props Tests
+ // --------------------------------------------------------------------------
+ describe('Props', () => {
+ it('should pass files to FileUploader', () => {
+ // Arrange
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('file-count')).toHaveTextContent('1')
+ })
+
+ it('should call onSetting when NotionConnector connect button is clicked', () => {
+ // Arrange
+ const onSetting = vi.fn()
+ render( )
+
+ // Act - The NotionConnector's button calls onSetting
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.connect/i }))
+
+ // Assert
+ expect(onSetting).toHaveBeenCalledTimes(1)
+ })
+
+ it('should call changeType when data source type is changed', () => {
+ // Arrange
+ const changeType = vi.fn()
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
+
+ // Assert
+ expect(changeType).toHaveBeenCalledWith(DataSourceType.NOTION)
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // State Management Tests
+ // --------------------------------------------------------------------------
+ describe('State Management', () => {
+ it('should open empty dataset modal when link is clicked', () => {
+ // Arrange
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
+
+ // Assert
+ expect(screen.getByTestId('empty-dataset-modal')).toBeInTheDocument()
+ })
+
+ it('should close empty dataset modal when close is clicked', () => {
+ // Arrange
+ render( )
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
+
+ // Act
+ fireEvent.click(screen.getByTestId('close-modal'))
+
+ // Assert
+ expect(screen.queryByTestId('empty-dataset-modal')).not.toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Memoization Tests
+ // --------------------------------------------------------------------------
+ describe('Memoization', () => {
+ it('should correctly compute isNotionAuthed based on authedDataSourceList', () => {
+ // Arrange - No auth
+ const { rerender } = render( )
+ // NotionConnector shows the sync title when not authenticated
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
+
+ // Act - Add auth
+ const authedDataSourceList = [createMockDataSourceAuth()]
+ rerender( )
+
+ // Assert
+ expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
+ })
+
+ it('should correctly compute fileNextDisabled when files are empty', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert - Button should be disabled
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+
+ it('should correctly compute fileNextDisabled when files are loaded', () => {
+ // Arrange
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+
+ // Assert - Button should be enabled
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).not.toBeDisabled()
+ })
+
+ it('should correctly compute fileNextDisabled when some files are not uploaded', () => {
+ // Arrange - Create a file item without id (not yet uploaded)
+ const file = new File(['test'], 'test.txt', { type: 'text/plain' })
+ const fileItem: FileItem = {
+ fileID: 'temp-id',
+ file: Object.assign(file, { id: undefined, extension: 'txt', mime_type: 'text/plain' }),
+ progress: 0,
+ }
+
+ // Act
+ render( )
+
+ // Assert - Button should be disabled
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Callback Tests
+ // --------------------------------------------------------------------------
+ describe('Callbacks', () => {
+ it('should call onStepChange when next button is clicked with valid files', () => {
+ // Arrange
+ const onStepChange = vi.fn()
+ const files = [createMockFileItem()]
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
+
+ // Assert
+ expect(onStepChange).toHaveBeenCalledTimes(1)
+ })
+
+ it('should show plan upgrade modal when batch upload not supported and multiple files', () => {
+ // Arrange
+ mockEnableBilling = true
+ mockPlan.type = Plan.sandbox
+ const files = [createMockFileItem(), createMockFileItem()]
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
+
+ // Assert
+ expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
+ })
+
+ it('should show upgrade card when in sandbox plan with files', () => {
+ // Arrange
+ mockEnableBilling = true
+ mockPlan.type = Plan.sandbox
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('upgrade-card')).toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Vector Space Full Tests
+ // --------------------------------------------------------------------------
+ describe('Vector Space Full', () => {
+ it('should show VectorSpaceFull when vector space is full and billing is enabled', () => {
+ // Arrange
+ mockEnableBilling = true
+ mockPlan.usage.vectorSpace = 100
+ mockPlan.total.vectorSpace = 100
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByTestId('vector-space-full')).toBeInTheDocument()
+ })
+
+ it('should disable next button when vector space is full', () => {
+ // Arrange
+ mockEnableBilling = true
+ mockPlan.usage.vectorSpace = 100
+ mockPlan.total.vectorSpace = 100
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+
+ // Assert
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Preview Integration Tests
+ // --------------------------------------------------------------------------
+ describe('Preview Integration', () => {
+ it('should show file preview when file preview button is clicked', () => {
+ // Arrange
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('preview-file'))
+
+ // Assert
+ expect(screen.getByTestId('file-preview')).toBeInTheDocument()
+ })
+
+ it('should hide file preview when hide button is clicked', () => {
+ // Arrange
+ render( )
+ fireEvent.click(screen.getByTestId('preview-file'))
+
+ // Act
+ fireEvent.click(screen.getByTestId('hide-file-preview'))
+
+ // Assert
+ expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
+ })
+
+ it('should show notion page preview when preview button is clicked', () => {
+ // Arrange
+ const authedDataSourceList = [createMockDataSourceAuth()]
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('preview-notion'))
+
+ // Assert
+ expect(screen.getByTestId('notion-page-preview')).toBeInTheDocument()
+ })
+
+ it('should show website preview when preview button is clicked', () => {
+ // Arrange
+ render( )
+
+ // Act
+ fireEvent.click(screen.getByTestId('preview-website'))
+
+ // Assert - Check for pagePreview title which is shown by WebsitePreview
+ expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Edge Cases
+ // --------------------------------------------------------------------------
+ describe('Edge Cases', () => {
+ it('should handle empty notionPages array', () => {
+ // Arrange
+ const authedDataSourceList = [createMockDataSourceAuth()]
+
+ // Act
+ render( )
+
+ // Assert - Button should be disabled when no pages selected
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+
+ it('should handle empty websitePages array', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert - Button should be disabled when no pages crawled
+ expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
+ })
+
+ it('should handle empty authedDataSourceList', () => {
+ // Arrange & Act
+ render( )
+
+ // Assert - Should show NotionConnector with connect button
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
+ })
+
+ it('should handle authedDataSourceList without notion credentials', () => {
+ // Arrange
+ const authedDataSourceList = [createMockDataSourceAuth({ credentials_list: [] })]
+
+ // Act
+ render( )
+
+ // Assert - Should show NotionConnector with connect button
+ expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
+ })
+
+ it('should clear previews when switching data source types', () => {
+ // Arrange
+ render( )
+ fireEvent.click(screen.getByTestId('preview-file'))
+ expect(screen.getByTestId('file-preview')).toBeInTheDocument()
+
+ // Act - Change to NOTION
+ fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
+
+ // Assert - File preview should be cleared
+ expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
+ })
+ })
+
+ // --------------------------------------------------------------------------
+ // Integration Tests
+ // --------------------------------------------------------------------------
+ describe('Integration', () => {
+ it('should complete file upload flow', () => {
+ // Arrange
+ const onStepChange = vi.fn()
+ const files = [createMockFileItem()]
+
+ // Act
+ render( )
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
+
+ // Assert
+ expect(onStepChange).toHaveBeenCalled()
+ })
+
+ it('should complete notion page selection flow', () => {
+ // Arrange
+ const onStepChange = vi.fn()
+ const authedDataSourceList = [createMockDataSourceAuth()]
+ const notionPages = [createMockNotionPage()]
+
+ // Act
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
+
+ // Assert
+ expect(onStepChange).toHaveBeenCalled()
+ })
+
+ it('should complete website crawl flow', () => {
+ // Arrange
+ const onStepChange = vi.fn()
+ const websitePages = [createMockCrawlResult()]
+
+ // Act
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
+
+ // Assert
+ expect(onStepChange).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx
index 5c74e69e7e..a86c9d86c2 100644
--- a/web/app/components/datasets/create/step-one/index.tsx
+++ b/web/app/components/datasets/create/step-one/index.tsx
@@ -1,29 +1,25 @@
'use client'
+
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
-import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
+import { RiFolder6Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
-import * as React from 'react'
-import { useCallback, useMemo, useState } from 'react'
+import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
-import Button from '@/app/components/base/button'
import NotionConnector from '@/app/components/base/notion-connector'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
-import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { Plan } from '@/app/components/billing/type'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
-import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import EmptyDatasetCreationModal from '../empty-dataset-creation-modal'
-import FilePreview from '../file-preview'
import FileUploader from '../file-uploader'
-import NotionPagePreview from '../notion-page-preview'
import Website from '../website'
-import WebsitePreview from '../website/preview'
+import { DataSourceTypeSelector, NextStepButton, PreviewPanel } from './components'
+import { usePreviewState } from './hooks'
import s from './index.module.css'
import UpgradeCard from './upgrade-card'
@@ -50,6 +46,24 @@ type IStepOneProps = {
authedDataSourceList: DataSourceAuth[]
}
+// Helper function to check if notion is authenticated
+function checkNotionAuth(authedDataSourceList: DataSourceAuth[]): boolean {
+ const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
+ return Boolean(notionSource && notionSource.credentials_list.length > 0)
+}
+
+// Helper function to get notion credential list
+function getNotionCredentialList(authedDataSourceList: DataSourceAuth[]) {
+ return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
+}
+
+// Lookup table for checking multiple items by data source type
+const MULTIPLE_ITEMS_CHECK: Record boolean> = {
+ [DataSourceType.FILE]: ({ files }) => files.length > 1,
+ [DataSourceType.NOTION]: ({ notionPages }) => notionPages.length > 1,
+ [DataSourceType.WEB]: ({ websitePages }) => websitePages.length > 1,
+}
+
const StepOne = ({
datasetId,
dataSourceType: inCreatePageDataSourceType,
@@ -72,76 +86,47 @@ const StepOne = ({
onCrawlOptionsChange,
authedDataSourceList,
}: IStepOneProps) => {
- const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
- const [showModal, setShowModal] = useState(false)
- const [currentFile, setCurrentFile] = useState()
- const [currentNotionPage, setCurrentNotionPage] = useState()
- const [currentWebsite, setCurrentWebsite] = useState()
const { t } = useTranslation()
+ const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
+ const { plan, enableBilling } = useProviderContext()
- const modalShowHandle = () => setShowModal(true)
- const modalCloseHandle = () => setShowModal(false)
+ // Preview state management
+ const {
+ currentFile,
+ currentNotionPage,
+ currentWebsite,
+ showFilePreview,
+ hideFilePreview,
+ showNotionPagePreview,
+ hideNotionPagePreview,
+ showWebsitePreview,
+ hideWebsitePreview,
+ } = usePreviewState()
- const updateCurrentFile = useCallback((file: File) => {
- setCurrentFile(file)
- }, [])
+ // Empty dataset modal state
+ const [showModal, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false)
- const hideFilePreview = useCallback(() => {
- setCurrentFile(undefined)
- }, [])
-
- const updateCurrentPage = useCallback((page: NotionPage) => {
- setCurrentNotionPage(page)
- }, [])
-
- const hideNotionPagePreview = useCallback(() => {
- setCurrentNotionPage(undefined)
- }, [])
-
- const updateWebsite = useCallback((website: CrawlResultItem) => {
- setCurrentWebsite(website)
- }, [])
-
- const hideWebsitePreview = useCallback(() => {
- setCurrentWebsite(undefined)
- }, [])
+ // Plan upgrade modal state
+ const [isShowPlanUpgradeModal, { setTrue: showPlanUpgradeModal, setFalse: hidePlanUpgradeModal }] = useBoolean(false)
+ // Computed values
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
const isInCreatePage = shouldShowDataSourceTypeList
- const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : dataset?.data_source_type
- const { plan, enableBilling } = useProviderContext()
- const allFileLoaded = (files.length > 0 && files.every(file => file.file.id))
- const hasNotin = notionPages.length > 0
+ // Default to FILE type when no type is provided from either source
+ const dataSourceType = isInCreatePage
+ ? (inCreatePageDataSourceType ?? DataSourceType.FILE)
+ : (dataset?.data_source_type ?? DataSourceType.FILE)
+
+ const allFileLoaded = files.length > 0 && files.every(file => file.file.id)
+ const hasNotion = notionPages.length > 0
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
- const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling
+ const isShowVectorSpaceFull = (allFileLoaded || hasNotion) && isVectorSpaceFull && enableBilling
const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
- const notSupportBatchUpload = !supportBatchUpload
- const [isShowPlanUpgradeModal, {
- setTrue: showPlanUpgradeModal,
- setFalse: hidePlanUpgradeModal,
- }] = useBoolean(false)
- const onStepChange = useCallback(() => {
- if (notSupportBatchUpload) {
- let isMultiple = false
- if (dataSourceType === DataSourceType.FILE && files.length > 1)
- isMultiple = true
+ const isNotionAuthed = useMemo(() => checkNotionAuth(authedDataSourceList), [authedDataSourceList])
+ const notionCredentialList = useMemo(() => getNotionCredentialList(authedDataSourceList), [authedDataSourceList])
- if (dataSourceType === DataSourceType.NOTION && notionPages.length > 1)
- isMultiple = true
-
- if (dataSourceType === DataSourceType.WEB && websitePages.length > 1)
- isMultiple = true
-
- if (isMultiple) {
- showPlanUpgradeModal()
- return
- }
- }
- doOnStepChange()
- }, [dataSourceType, doOnStepChange, files.length, notSupportBatchUpload, notionPages.length, showPlanUpgradeModal, websitePages.length])
-
- const nextDisabled = useMemo(() => {
+ const fileNextDisabled = useMemo(() => {
if (!files.length)
return true
if (files.some(file => !file.file.id))
@@ -149,109 +134,50 @@ const StepOne = ({
return isShowVectorSpaceFull
}, [files, isShowVectorSpaceFull])
- const isNotionAuthed = useMemo(() => {
- if (!authedDataSourceList)
- return false
- const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
- if (!notionSource)
- return false
- return notionSource.credentials_list.length > 0
- }, [authedDataSourceList])
+ // Clear previews when switching data source type
+ const handleClearPreviews = useCallback((newType: DataSourceType) => {
+ if (newType !== DataSourceType.FILE)
+ hideFilePreview()
+ if (newType !== DataSourceType.NOTION)
+ hideNotionPagePreview()
+ if (newType !== DataSourceType.WEB)
+ hideWebsitePreview()
+ }, [hideFilePreview, hideNotionPagePreview, hideWebsitePreview])
- const notionCredentialList = useMemo(() => {
- return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
- }, [authedDataSourceList])
+ // Handle step change with batch upload check
+ const onStepChange = useCallback(() => {
+ if (!supportBatchUpload && dataSourceType) {
+ const checkFn = MULTIPLE_ITEMS_CHECK[dataSourceType]
+ if (checkFn?.({ files, notionPages, websitePages })) {
+ showPlanUpgradeModal()
+ return
+ }
+ }
+ doOnStepChange()
+ }, [dataSourceType, doOnStepChange, files, supportBatchUpload, notionPages, showPlanUpgradeModal, websitePages])
return (
+ {/* Left Panel - Form */}
- {
- shouldShowDataSourceTypeList && (
+ {shouldShowDataSourceTypeList && (
+ <>
{t('steps.one', { ns: 'datasetCreation' })}
- )
- }
- {
- shouldShowDataSourceTypeList && (
-
-
{
- if (dataSourceTypeDisable)
- return
- changeType(DataSourceType.FILE)
- hideNotionPagePreview()
- hideWebsitePreview()
- }}
- >
-
-
- {t('stepOne.dataSourceType.file', { ns: 'datasetCreation' })}
-
-
-
{
- if (dataSourceTypeDisable)
- return
- changeType(DataSourceType.NOTION)
- hideFilePreview()
- hideWebsitePreview()
- }}
- >
-
-
- {t('stepOne.dataSourceType.notion', { ns: 'datasetCreation' })}
-
-
- {(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
-
{
- if (dataSourceTypeDisable)
- return
- changeType(DataSourceType.WEB)
- hideFilePreview()
- hideNotionPagePreview()
- }}
- >
-
-
- {t('stepOne.dataSourceType.web', { ns: 'datasetCreation' })}
-
-
- )}
-
- )
- }
+
+ >
+ )}
+
+ {/* File Data Source */}
{dataSourceType === DataSourceType.FILE && (
<>
{isShowVectorSpaceFull && (
@@ -268,24 +194,17 @@ const StepOne = ({
)}
-
-
-
- {t('stepOne.button', { ns: 'datasetCreation' })}
-
-
-
-
- {
- enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
-
- )
- }
+
+ {enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
+
+ )}
>
)}
+
+ {/* Notion Data Source */}
{dataSourceType === DataSourceType.NOTION && (
<>
{!isNotionAuthed &&
}
@@ -295,7 +214,7 @@ const StepOne = ({
page.page_id)}
onSelect={updateNotionPages}
- onPreview={updateCurrentPage}
+ onPreview={showNotionPagePreview}
credentialList={notionCredentialList}
onSelectCredential={updateNotionCredentialId}
datasetId={datasetId}
@@ -306,23 +225,21 @@ const StepOne = ({
)}
-
-
-
- {t('stepOne.button', { ns: 'datasetCreation' })}
-
-
-
-
+
>
)}
>
)}
+
+ {/* Web Data Source */}
{dataSourceType === DataSourceType.WEB && (
<>
)}
-
-
-
- {t('stepOne.button', { ns: 'datasetCreation' })}
-
-
-
-
+
>
)}
+
+ {/* Empty Dataset Creation Link */}
{!datasetId && (
<>
-
+
{t('stepOne.emptyDatasetCreation', { ns: 'datasetCreation' })}
>
)}
-
+
-
- {currentFile &&
}
- {currentNotionPage && (
-
- )}
- {currentWebsite &&
}
- {isShowPlanUpgradeModal && (
-
- )}
-
+
+ {/* Right Panel - Preview */}
+
)