mirror of
https://github.com/langgenius/dify.git
synced 2026-06-11 10:57:40 +08:00
refactor(web): compose tab header with dify-ui tabs (#37280)
This commit is contained in:
parent
2c5c8e82c3
commit
a83118c0f4
@ -255,11 +255,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app-sidebar/nav-link/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
@ -2366,14 +2361,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/try-app/tab.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/goto-anything/actions/commands/command-bus.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
@ -3716,17 +3703,6 @@
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
},
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -39,10 +39,15 @@ describe('Tabs wrappers', () => {
|
||||
|
||||
await expect.element(screen.getByRole('tablist')).toHaveClass(
|
||||
'flex',
|
||||
'gap-4',
|
||||
)
|
||||
await expect.element(screen.getByRole('tab', { name: 'First' })).toHaveClass(
|
||||
'touch-manipulation',
|
||||
'focus-visible:outline-hidden',
|
||||
'border-b-2',
|
||||
'border-transparent',
|
||||
'data-active:border-components-tab-active',
|
||||
'data-active:text-text-primary',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -26,17 +26,11 @@ type Story = StoryObj<typeof meta>
|
||||
export const Basic: Story = {
|
||||
render: () => (
|
||||
<Tabs defaultValue="overview" className="w-96">
|
||||
<TabsList className="gap-4 border-b border-divider-subtle">
|
||||
<TabsTab
|
||||
value="overview"
|
||||
className="border-b border-transparent px-0 py-2 system-sm-medium text-text-tertiary data-active:border-text-accent data-active:text-text-primary"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTab value="overview">
|
||||
Overview
|
||||
</TabsTab>
|
||||
<TabsTab
|
||||
value="activity"
|
||||
className="border-b border-transparent px-0 py-2 system-sm-medium text-text-tertiary data-active:border-text-accent data-active:text-text-primary"
|
||||
>
|
||||
<TabsTab value="activity">
|
||||
Activity
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
|
||||
@ -18,7 +18,7 @@ export function TabsList({
|
||||
}: TabsListProps) {
|
||||
return (
|
||||
<BaseTabs.List
|
||||
className={cn('flex', className)}
|
||||
className={cn('flex gap-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@ -34,7 +34,7 @@ export function TabsTab({
|
||||
}: TabsTabProps) {
|
||||
return (
|
||||
<BaseTabs.Tab
|
||||
className={cn('touch-manipulation focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid data-disabled:cursor-not-allowed data-disabled:text-text-disabled', className)}
|
||||
className={cn('touch-manipulation focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid relative flex cursor-pointer items-center border-b-2 border-transparent pt-2.5 pb-2 system-md-semibold text-text-tertiary data-active:border-components-tab-active data-active:text-text-primary data-disabled:cursor-not-allowed data-disabled:text-text-tertiary data-disabled:opacity-30 data-active:data-disabled:text-text-primary', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -197,13 +197,13 @@ describe('TextGeneration', () => {
|
||||
expect(screen.getByTestId('run-once-input-name')).toHaveTextContent('Gamma')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('tab-header-item-batch'))
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'share.generation.tabs.batch' }))
|
||||
expect(screen.getByRole('button', { name: 'run-batch' })).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('tab-header-item-saved'))
|
||||
fireEvent.click(screen.getByRole('tab', { name: /^share\.generation\.tabs\.saved/ }))
|
||||
expect(screen.getByTestId('saved-items-mock')).toHaveTextContent('2')
|
||||
|
||||
fireEvent.click(screen.getByTestId('tab-header-item-create'))
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'share.generation.tabs.create' }))
|
||||
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -220,7 +220,7 @@ describe('TextGeneration', () => {
|
||||
})
|
||||
expect(screen.getByTestId('result-single')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('tab-header-item-batch'))
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'share.generation.tabs.batch' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'run-batch' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('idle')).toBeInTheDocument()
|
||||
|
||||
@ -46,8 +46,8 @@ const NavLink = ({
|
||||
const isActive = active ?? (href ? href.toLowerCase().split('/')?.pop() === formattedSegment : false)
|
||||
const NavIcon = isActive ? iconMap.selected : iconMap.normal
|
||||
const linkClassName = cn(isActive
|
||||
? 'border-b-[0.25px] border-l-[0.75px] border-r-[0.25px] border-t-[0.75px] border-effects-highlight-lightmode-off bg-components-menu-item-bg-active text-text-accent-light-mode-only system-sm-semibold'
|
||||
: 'text-components-menu-item-text system-sm-medium hover:bg-components-menu-item-bg-hover hover:text-components-menu-item-text-hover', 'flex h-8 items-center rounded-lg pl-3 pr-1')
|
||||
? 'border-t-[0.75px] border-r-[0.25px] border-b-[0.25px] border-l-[0.75px] border-effects-highlight-lightmode-off bg-components-menu-item-bg-active system-sm-semibold text-text-accent-light-mode-only'
|
||||
: 'system-sm-medium text-components-menu-item-text hover:bg-components-menu-item-bg-hover hover:text-components-menu-item-text-hover', 'flex h-8 items-center rounded-lg pr-1 pl-3')
|
||||
|
||||
const renderIcon = () => (
|
||||
<div className={cn(mode !== 'expand' && '-ml-1')}>
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import TabHeader from '../index'
|
||||
|
||||
describe('TabHeader Component', () => {
|
||||
const mockItems = [
|
||||
{ id: 'tab1', name: 'General' },
|
||||
{ id: 'tab2', name: 'Settings' },
|
||||
{ id: 'tab3', name: 'Profile', isRight: true },
|
||||
{ id: 'tab4', name: 'Disabled Tab', disabled: true },
|
||||
]
|
||||
|
||||
it('should render all items with correct names', () => {
|
||||
render(<TabHeader items={mockItems} value="tab1" onChange={() => { }} />)
|
||||
|
||||
expect(screen.getByText('General')).toBeInTheDocument()
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument()
|
||||
expect(screen.getByText('Profile')).toBeInTheDocument()
|
||||
expect(screen.getByText('Disabled Tab')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should separate items into left and right containers correctly', () => {
|
||||
render(<TabHeader items={mockItems} value="tab1" onChange={() => { }} />)
|
||||
|
||||
const leftContainer = screen.getByTestId('tab-header-left')
|
||||
const rightContainer = screen.getByTestId('tab-header-right')
|
||||
|
||||
// Verify children count
|
||||
expect(leftContainer.children.length).toBe(3)
|
||||
expect(rightContainer.children.length).toBe(1)
|
||||
|
||||
// Verify specific item placement using within and toContainElement
|
||||
const profileTab = screen.getByTestId('tab-header-item-tab3')
|
||||
expect(rightContainer).toContainElement(profileTab)
|
||||
|
||||
const disabledTab = screen.getByTestId('tab-header-item-tab4')
|
||||
expect(leftContainer).toContainElement(disabledTab)
|
||||
})
|
||||
|
||||
it('should apply active styles to the selected tab', () => {
|
||||
const activeClass = 'custom-active-style'
|
||||
render(
|
||||
<TabHeader
|
||||
items={mockItems}
|
||||
value="tab2"
|
||||
activeItemClassName={activeClass}
|
||||
onChange={() => { }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const activeTab = screen.getByTestId('tab-header-item-tab2')
|
||||
expect(activeTab).toHaveClass('border-components-tab-active')
|
||||
expect(activeTab).toHaveClass(activeClass)
|
||||
|
||||
const inactiveTab = screen.getByTestId('tab-header-item-tab1')
|
||||
expect(inactiveTab).toHaveClass('text-text-tertiary')
|
||||
})
|
||||
|
||||
it('should call onChange when a non-disabled tab is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleChange = vi.fn()
|
||||
render(<TabHeader items={mockItems} value="tab1" onChange={handleChange} />)
|
||||
|
||||
await user.click(screen.getByText('Settings'))
|
||||
expect(handleChange).toHaveBeenCalledWith('tab2')
|
||||
})
|
||||
|
||||
it('should not call onChange when a disabled tab is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleChange = vi.fn()
|
||||
render(<TabHeader items={mockItems} value="tab1" onChange={handleChange} />)
|
||||
|
||||
const disabledTab = screen.getByTestId('tab-header-item-tab4')
|
||||
expect(disabledTab).toHaveClass('cursor-not-allowed')
|
||||
|
||||
await user.click(disabledTab)
|
||||
expect(handleChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render icon and extra content when provided', () => {
|
||||
const itemsWithExtras = [
|
||||
{
|
||||
id: 'extra',
|
||||
name: 'Extra',
|
||||
icon: <span data-testid="tab-icon">🚀</span>,
|
||||
extra: <span data-testid="tab-extra">New</span>,
|
||||
},
|
||||
]
|
||||
render(<TabHeader items={itemsWithExtras} value="extra" onChange={() => { }} />)
|
||||
|
||||
expect(screen.getByTestId('tab-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('tab-extra')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom class names for items and wrappers', () => {
|
||||
render(
|
||||
<TabHeader
|
||||
items={mockItems}
|
||||
value="tab1"
|
||||
itemClassName="my-text-class"
|
||||
itemWrapClassName="my-wrap-class"
|
||||
onChange={() => { }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tabWrap = screen.getByTestId('tab-header-item-tab1')
|
||||
// We target the inner div for the name class check
|
||||
const tabText = within(tabWrap).getByText('General')
|
||||
|
||||
expect(tabWrap).toHaveClass('my-wrap-class')
|
||||
expect(tabText).toHaveClass('my-text-class')
|
||||
})
|
||||
})
|
||||
@ -1,66 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { ITabHeaderProps } from '.'
|
||||
import { useState } from 'react'
|
||||
import TabHeader from '.'
|
||||
|
||||
const items: ITabHeaderProps['items'] = [
|
||||
{ id: 'overview', name: 'Overview' },
|
||||
{ id: 'playground', name: 'Playground' },
|
||||
{ id: 'changelog', name: 'Changelog', extra: <span className="ml-1 rounded-full bg-primary-50 px-2 py-0.5 text-xs text-primary-600">New</span> },
|
||||
{ id: 'docs', name: 'Docs', isRight: true },
|
||||
{ id: 'settings', name: 'Settings', isRight: true, disabled: true },
|
||||
]
|
||||
|
||||
const TabHeaderDemo = ({
|
||||
initialTab = 'overview',
|
||||
}: {
|
||||
initialTab?: string
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab)
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-3xl flex-col gap-6 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6">
|
||||
<div className="flex items-center justify-between text-xs tracking-[0.18em] text-text-tertiary uppercase">
|
||||
<span>Tabs</span>
|
||||
<code className="rounded-md bg-background-default px-2 py-1 text-[11px] text-text-tertiary">
|
||||
active="
|
||||
{activeTab}
|
||||
"
|
||||
</code>
|
||||
</div>
|
||||
<TabHeader
|
||||
items={items}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Navigation/TabHeader',
|
||||
component: TabHeaderDemo,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Two-sided header tabs with optional right-aligned actions. Disabled items illustrate read-only states.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
initialTab: {
|
||||
control: 'radio',
|
||||
options: items.map(item => item.id),
|
||||
},
|
||||
},
|
||||
args: {
|
||||
initialTab: 'overview',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof TabHeaderDemo>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Playground: Story = {}
|
||||
@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
|
||||
type Item = {
|
||||
id: string
|
||||
name: string
|
||||
isRight?: boolean
|
||||
icon?: React.ReactNode
|
||||
extra?: React.ReactNode
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type ITabHeaderProps = Readonly<{
|
||||
items: Item[]
|
||||
value: string
|
||||
itemClassName?: string
|
||||
itemWrapClassName?: string
|
||||
activeItemClassName?: string
|
||||
onChange: (value: string) => void
|
||||
}>
|
||||
|
||||
const TabHeader: FC<ITabHeaderProps> = ({
|
||||
items,
|
||||
value,
|
||||
itemClassName,
|
||||
itemWrapClassName,
|
||||
activeItemClassName,
|
||||
onChange,
|
||||
}) => {
|
||||
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
|
||||
<div
|
||||
key={id}
|
||||
data-testid={`tab-header-item-${id}`}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center border-b-2 border-transparent pt-2.5 pb-2 system-md-semibold',
|
||||
id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary',
|
||||
disabled && 'cursor-not-allowed opacity-30',
|
||||
itemWrapClassName,
|
||||
)}
|
||||
onClick={() => !disabled && onChange(id)}
|
||||
>
|
||||
{icon || ''}
|
||||
<div className={cn('ml-2', itemClassName)}>{name}</div>
|
||||
{extra || ''}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div data-testid="tab-header" className="flex justify-between">
|
||||
<div data-testid="tab-header-left" className="flex space-x-4">
|
||||
{items.filter(item => !item.isRight).map(renderItem)}
|
||||
</div>
|
||||
<div data-testid="tab-header-right" className="flex space-x-4">
|
||||
{items.filter(item => item.isRight).map(renderItem)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TabHeader)
|
||||
@ -3,7 +3,7 @@ import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
|
||||
import TryApp from '../index'
|
||||
import { TypeEnum } from '../tab'
|
||||
import { TypeEnum } from '../types'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal() as object
|
||||
@ -213,8 +213,7 @@ describe('TryApp (main index.tsx)', () => {
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const buttons = document.body.querySelectorAll('button')
|
||||
expect(buttons.length).toBeGreaterThan(0)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -281,16 +280,11 @@ describe('TryApp (main index.tsx)', () => {
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const buttons = document.body.querySelectorAll('button')
|
||||
const closeButton = Array.from(buttons).find(btn =>
|
||||
btn.querySelector('svg') || btn.className.includes('rounded-[10px]'),
|
||||
)
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
|
||||
if (closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import Tab, { TypeEnum } from '../tab'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal() as object
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
describe('Tab', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders tab with TRY value selected', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.TRY} onChange={mockOnChange} />)
|
||||
|
||||
expect(screen.getByText('explore.tryApp.tabHeader.try')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.tryApp.tabHeader.detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders tab with DETAIL value selected', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.DETAIL} onChange={mockOnChange} />)
|
||||
|
||||
expect(screen.getByText('explore.tryApp.tabHeader.try')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.tryApp.tabHeader.detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onChange when clicking a tab', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.TRY} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('explore.tryApp.tabHeader.detail'))
|
||||
expect(mockOnChange).toHaveBeenCalledWith(TypeEnum.DETAIL)
|
||||
})
|
||||
|
||||
it('calls onChange when clicking Try tab', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
render(<Tab value={TypeEnum.DETAIL} onChange={mockOnChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('explore.tryApp.tabHeader.try'))
|
||||
expect(mockOnChange).toHaveBeenCalledWith(TypeEnum.TRY)
|
||||
})
|
||||
|
||||
it('exports TypeEnum correctly', () => {
|
||||
expect(TypeEnum.TRY).toBe('try')
|
||||
expect(TypeEnum.DETAIL).toBe('detail')
|
||||
})
|
||||
})
|
||||
@ -4,9 +4,11 @@ import type { FC } from 'react'
|
||||
import type { App as AppType } from '@/models/explore'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
@ -15,7 +17,7 @@ import { useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import App from './app'
|
||||
import AppInfo from './app-info'
|
||||
import Preview from './preview'
|
||||
import Tab, { TypeEnum } from './tab'
|
||||
import { TypeEnum } from './types'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
@ -32,6 +34,7 @@ const TryApp: FC<Props> = ({
|
||||
onClose,
|
||||
onCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app)
|
||||
const canUseTryTab = IS_CLOUD_EDITION && (app ? isTrialApp : true)
|
||||
@ -62,25 +65,49 @@ const TryApp: FC<Props> = ({
|
||||
<AppUnavailable className="size-auto" isUnknownReason />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full flex-col">
|
||||
<Tabs
|
||||
value={activeType}
|
||||
onValueChange={selectedValue => setType(selectedValue)}
|
||||
className="flex h-full flex-col"
|
||||
>
|
||||
<div className="flex shrink-0 justify-between pl-4">
|
||||
<Tab
|
||||
value={activeType}
|
||||
onChange={setType}
|
||||
disableTry={app ? !isTrialApp : false}
|
||||
/>
|
||||
<TabsList>
|
||||
{IS_CLOUD_EDITION && (
|
||||
<TabsTab
|
||||
value={TypeEnum.TRY}
|
||||
disabled={app ? !isTrialApp : false}
|
||||
className="pt-2 data-active:border-util-colors-blue-brand-blue-brand-500"
|
||||
>
|
||||
<span className="system-md-semibold-uppercase">{t('tryApp.tabHeader.try', { ns: 'explore' })}</span>
|
||||
</TabsTab>
|
||||
)}
|
||||
<TabsTab
|
||||
value={TypeEnum.DETAIL}
|
||||
className="pt-2 data-active:border-util-colors-blue-brand-blue-brand-500"
|
||||
>
|
||||
<span className="system-md-semibold-uppercase">{t('tryApp.tabHeader.detail', { ns: 'explore' })}</span>
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
<Button
|
||||
size="large"
|
||||
variant="tertiary"
|
||||
aria-label={t('common.operation.close')}
|
||||
className="flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="i-ri-close-line size-5" />
|
||||
<span aria-hidden className="i-ri-close-line size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Main content */}
|
||||
<div className="mt-2 flex h-0 grow justify-between space-x-2">
|
||||
{activeType === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail} /> : <Preview appId={appId} appDetail={appDetail} />}
|
||||
{IS_CLOUD_EDITION && (
|
||||
<TabsPanel value={TypeEnum.TRY} className="min-w-0 flex-1">
|
||||
<App appId={appId} appDetail={appDetail} />
|
||||
</TabsPanel>
|
||||
)}
|
||||
<TabsPanel value={TypeEnum.DETAIL} className="min-w-0 flex-1">
|
||||
<Preview appId={appId} appDetail={appDetail} />
|
||||
</TabsPanel>
|
||||
<AppInfo
|
||||
className="w-[360px] shrink-0"
|
||||
appDetail={appDetail}
|
||||
@ -89,7 +116,7 @@ const TryApp: FC<Props> = ({
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
|
||||
export enum TypeEnum {
|
||||
TRY = 'try',
|
||||
DETAIL = 'detail',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: TypeEnum
|
||||
onChange: (value: TypeEnum) => void
|
||||
disableTry?: boolean
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
disableTry,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tabs = React.useMemo(() => {
|
||||
return [
|
||||
IS_CLOUD_EDITION ? { id: TypeEnum.TRY, name: t('tryApp.tabHeader.try', { ns: 'explore' }), disabled: disableTry } : null,
|
||||
{ id: TypeEnum.DETAIL, name: t('tryApp.tabHeader.detail', { ns: 'explore' }) },
|
||||
].filter(item => item !== null) as { id: TypeEnum, name: string }[]
|
||||
}, [t, disableTry])
|
||||
return (
|
||||
<TabHeader
|
||||
items={tabs}
|
||||
value={value}
|
||||
onChange={onChange as (value: string) => void}
|
||||
itemClassName="ml-0 system-md-semibold-uppercase"
|
||||
itemWrapClassName="pt-2"
|
||||
activeItemClassName="border-util-colors-blue-brand-blue-brand-500"
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tab)
|
||||
6
web/app/components/explore/try-app/types.ts
Normal file
6
web/app/components/explore/try-app/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const TypeEnum = {
|
||||
TRY: 'try',
|
||||
DETAIL: 'detail',
|
||||
} as const
|
||||
|
||||
export type TypeEnum = typeof TypeEnum[keyof typeof TypeEnum]
|
||||
@ -113,6 +113,7 @@ describe('TextGenerationSidebar', () => {
|
||||
|
||||
expect(screen.getByText('Text Generation')).toBeInTheDocument()
|
||||
expect(screen.getByText('Share description')).toBeInTheDocument()
|
||||
expect(screen.getByRole('tablist')).toHaveClass('w-full')
|
||||
expect(screen.getByTestId('run-once-mock')).toBeInTheDocument()
|
||||
expect(runOncePropsSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
inputs: { name: 'Alice' },
|
||||
@ -134,7 +135,7 @@ describe('TextGenerationSidebar', () => {
|
||||
vars: promptConfig.prompt_variables,
|
||||
isAllFinished: true,
|
||||
}))
|
||||
expect(screen.queryByTestId('tab-header-item-saved')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('tab', { name: /^share\.generation\.tabs\.saved/ })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render saved items and allow switching back to create tab', () => {
|
||||
|
||||
@ -5,6 +5,7 @@ import type { PromptConfig, SavedMessage, TextToSpeechConfig } from '@/models/de
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SavedItems from '@/app/components/app/text-generate/saved-items'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
@ -12,7 +13,6 @@ import Badge from '@/app/components/base/badge'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
import MenuDropdown from './menu-dropdown'
|
||||
import RunBatch from './run-batch'
|
||||
import RunOnce from './run-once'
|
||||
@ -71,7 +71,9 @@ const TextGenerationSidebar: FC<TextGenerationSidebarProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onValueChange={onTabChange}
|
||||
className={cn(
|
||||
'relative flex h-full shrink-0 flex-col',
|
||||
isPC ? 'w-[600px] max-w-[50%]' : resultExisted ? 'h-[calc(100%-64px)]' : '',
|
||||
@ -93,29 +95,25 @@ const TextGenerationSidebar: FC<TextGenerationSidebarProps> = ({
|
||||
{siteInfo.description && (
|
||||
<div className="system-xs-regular text-text-tertiary">{siteInfo.description}</div>
|
||||
)}
|
||||
<TabHeader
|
||||
items={[
|
||||
{ id: 'create', name: t('generation.tabs.create', { ns: 'share' }) },
|
||||
{ id: 'batch', name: t('generation.tabs.batch', { ns: 'share' }) },
|
||||
...(!isWorkflow
|
||||
? [{
|
||||
id: 'saved',
|
||||
name: t('generation.tabs.saved', { ns: 'share' }),
|
||||
isRight: true,
|
||||
icon: <span aria-hidden className="i-ri-bookmark-3-line size-4" />,
|
||||
extra: savedMessages.length > 0
|
||||
? (
|
||||
<Badge className="ml-1">
|
||||
{savedMessages.length}
|
||||
</Badge>
|
||||
)
|
||||
: null,
|
||||
}]
|
||||
: []),
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<TabsList className="w-full">
|
||||
<TabsTab value="create">
|
||||
<span className="ml-2">{t('generation.tabs.create', { ns: 'share' })}</span>
|
||||
</TabsTab>
|
||||
<TabsTab value="batch">
|
||||
<span className="ml-2">{t('generation.tabs.batch', { ns: 'share' })}</span>
|
||||
</TabsTab>
|
||||
{!isWorkflow && (
|
||||
<TabsTab value="saved" className="ml-auto">
|
||||
<span aria-hidden className="i-ri-bookmark-3-line size-4" />
|
||||
<span className="ml-2">{t('generation.tabs.saved', { ns: 'share' })}</span>
|
||||
{savedMessages.length > 0 && (
|
||||
<Badge className="ml-1">
|
||||
{savedMessages.length}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTab>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@ -124,7 +122,7 @@ const TextGenerationSidebar: FC<TextGenerationSidebarProps> = ({
|
||||
!isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
|
||||
)}
|
||||
>
|
||||
<div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
|
||||
<TabsPanel value="create" keepMounted>
|
||||
<RunOnce
|
||||
siteInfo={siteInfo}
|
||||
inputs={inputs}
|
||||
@ -136,22 +134,24 @@ const TextGenerationSidebar: FC<TextGenerationSidebarProps> = ({
|
||||
onVisionFilesChange={onVisionFilesChange}
|
||||
runControl={runControl}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(currentTab === 'batch' ? 'block' : 'hidden')}>
|
||||
</TabsPanel>
|
||||
<TabsPanel value="batch" keepMounted>
|
||||
<RunBatch
|
||||
vars={promptConfig.prompt_variables}
|
||||
onSend={onBatchSend}
|
||||
isAllFinished={allTasksRun}
|
||||
/>
|
||||
</div>
|
||||
{currentTab === 'saved' && (
|
||||
<SavedItems
|
||||
className={cn(isPC ? 'mt-6' : 'mt-4')}
|
||||
isShowTextToSpeech={textToSpeechConfig?.enabled}
|
||||
list={savedMessages}
|
||||
onRemove={onRemoveSavedMessage}
|
||||
onStartCreateContent={() => onTabChange('create')}
|
||||
/>
|
||||
</TabsPanel>
|
||||
{!isWorkflow && (
|
||||
<TabsPanel value="saved">
|
||||
<SavedItems
|
||||
className={cn(isPC ? 'mt-6' : 'mt-4')}
|
||||
isShowTextToSpeech={textToSpeechConfig?.enabled}
|
||||
list={savedMessages}
|
||||
onRemove={onRemoveSavedMessage}
|
||||
onStartCreateContent={() => onTabChange('create')}
|
||||
/>
|
||||
</TabsPanel>
|
||||
)}
|
||||
</div>
|
||||
{!customConfig?.remove_webapp_brand && (
|
||||
@ -170,7 +170,7 @@ const TextGenerationSidebar: FC<TextGenerationSidebarProps> = ({
|
||||
: <DifyLogo size="small" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -274,18 +274,6 @@ vi.mock('../last-run', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../tab', () => ({
|
||||
__esModule: true,
|
||||
TabType: { settings: 'settings', lastRun: 'lastRun' },
|
||||
default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => (
|
||||
<div>
|
||||
<button onClick={() => onChange('settings')}>settings-tab</button>
|
||||
<button onClick={() => onChange('lastRun')}>last-run-tab</button>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../trigger-subscription', () => ({
|
||||
TriggerSubscription: ({ children, onSubscriptionChange }: PropsWithChildren<{ onSubscriptionChange?: (value: { id: string }, callback?: () => void) => void }>) => (
|
||||
<div>
|
||||
@ -321,7 +309,7 @@ describe('workflow-panel index', () => {
|
||||
})
|
||||
|
||||
it('should render the settings panel and wire title, description, run, and close actions', async () => {
|
||||
const { container } = renderWorkflowComponent(
|
||||
renderWorkflowComponent(
|
||||
<BasePanel id="node-1" data={createData() as never}>
|
||||
<div>panel-child</div>
|
||||
</BasePanel>,
|
||||
@ -351,8 +339,7 @@ describe('workflow-panel index', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.panel.runThisStep' }))
|
||||
|
||||
const clickableItems = container.querySelectorAll('.cursor-pointer')
|
||||
fireEvent.click(clickableItems[clickableItems.length - 1] as HTMLElement)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(mockHandleSingleRun).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleNodeSelect).toHaveBeenCalledWith('node-1', true)
|
||||
@ -395,6 +382,7 @@ describe('workflow-panel index', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('last-run-panel')).toBeInTheDocument()
|
||||
expect(screen.getByRole('tabpanel')).toHaveClass('flex', 'flex-1', 'flex-col')
|
||||
})
|
||||
|
||||
it('should render the plain tab layout and allow last-run status updates', async () => {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC, ReactNode } from 'react'
|
||||
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -90,8 +91,8 @@ import {
|
||||
} from './helpers'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import Tab, { TabType } from './tab'
|
||||
import { TriggerSubscription } from './trigger-subscription'
|
||||
import { TabType } from './types'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
@ -480,6 +481,17 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
? t('debug.variableInspect.trigger.stop', { ns: 'workflow' })
|
||||
: runThisStepLabel
|
||||
|
||||
const panelTabs = (
|
||||
<TabsList>
|
||||
<TabsTab value={TabType.settings}>
|
||||
{t('debug.settingsTab', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</TabsTab>
|
||||
<TabsTab value={TabType.lastRun}>
|
||||
{t('debug.lastRunTab', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -496,8 +508,10 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
>
|
||||
<div className="h-10 w-0.5 rounded-xs bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid"></div>
|
||||
</div>
|
||||
<div
|
||||
<Tabs
|
||||
ref={containerRef}
|
||||
value={tabType}
|
||||
onValueChange={selectedValue => setTabType(selectedValue)}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-[width] ease-linear', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${nodePanelWidth}px`,
|
||||
@ -558,12 +572,14 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
<HelpLink nodeType={data.type} />
|
||||
<NodeActionsDropdown id={id} data={data} showHelpLink={false} />
|
||||
<div className="mx-3 h-3.5 w-px bg-divider-regular" />
|
||||
<div
|
||||
className="flex size-6 cursor-pointer items-center justify-center"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.operation.close')}
|
||||
className="flex size-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden"
|
||||
onClick={() => handleNodeSelect(id, true)}
|
||||
>
|
||||
<RiCloseLine className="size-4 text-text-tertiary" />
|
||||
</div>
|
||||
<RiCloseLine aria-hidden className="size-4 text-text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
@ -584,10 +600,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
{panelTabs}
|
||||
<AuthorizedInNode
|
||||
pluginPayload={{
|
||||
provider: currToolCollection?.name || '',
|
||||
@ -609,10 +622,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
isAuthorized={currentDataSource.is_authorized}
|
||||
>
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
{panelTabs}
|
||||
<AuthorizedInDataSourceNode
|
||||
onJumpToDataSourcePage={handleJumpToDataSourcePage}
|
||||
authorizationsNum={3}
|
||||
@ -627,76 +637,68 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
subscriptionIdSelected={data.subscription_id}
|
||||
onSubscriptionChange={handleSubscriptionChange}
|
||||
>
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
{panelTabs}
|
||||
</TriggerSubscription>
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsToolAuth && !currentDataSource && !currentTriggerPlugin && (
|
||||
<div className="flex items-center justify-between pr-3 pl-4">
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
{panelTabs}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Split />
|
||||
</div>
|
||||
{tabType === TabType.settings && (
|
||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
runInputDataRef,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className="border-t-[0.5px] border-divider-regular p-4">
|
||||
<div className="mb-1 flex items-center system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className="mb-2 system-xs-regular text-text-tertiary">
|
||||
{t('panel.addNextStep', { ns: 'workflow' })}
|
||||
</div>
|
||||
<NextStep selectedNode={selectedNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
<TabsPanel value={TabType.settings} className="flex flex-1 flex-col overflow-y-auto">
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
runInputDataRef,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className="border-t-[0.5px] border-divider-regular p-4">
|
||||
<div className="mb-1 flex items-center system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className="mb-2 system-xs-regular text-text-tertiary">
|
||||
{t('panel.addNextStep', { ns: 'workflow' })}
|
||||
</div>
|
||||
<NextStep selectedNode={selectedNode} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{readmeEntranceComponent}
|
||||
</TabsPanel>
|
||||
|
||||
{tabType === TabType.lastRun && (
|
||||
<TabsPanel value={TabType.lastRun} className="flex flex-1 flex-col">
|
||||
<LastRun
|
||||
appId={appDetail?.id || ''}
|
||||
nodeId={id}
|
||||
@ -710,9 +712,9 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
isPaused={isPaused}
|
||||
{...passedLogParams}
|
||||
/>
|
||||
)}
|
||||
</TabsPanel>
|
||||
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import { TabType } from '../tab'
|
||||
import { TabType } from '../types'
|
||||
|
||||
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: useLLMSingleRunFormParams,
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TabHeader from '@/app/components/base/tab-header'
|
||||
|
||||
export enum TabType {
|
||||
settings = 'settings',
|
||||
lastRun = 'lastRun',
|
||||
relations = 'relations',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: TabType
|
||||
onChange: (value: TabType) => void
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<TabHeader
|
||||
items={[
|
||||
{ id: TabType.settings, name: t('debug.settingsTab', { ns: 'workflow' }).toLocaleUpperCase() },
|
||||
{ id: TabType.lastRun, name: t('debug.lastRunTab', { ns: 'workflow' }).toLocaleUpperCase() },
|
||||
]}
|
||||
itemClassName="ml-0"
|
||||
value={value}
|
||||
onChange={onChange as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tab)
|
||||
@ -0,0 +1,7 @@
|
||||
export const TabType = {
|
||||
settings: 'settings',
|
||||
lastRun: 'lastRun',
|
||||
relations: 'relations',
|
||||
} as const
|
||||
|
||||
export type TabType = typeof TabType[keyof typeof TabType]
|
||||
Loading…
Reference in New Issue
Block a user