- {!isInWebApp && !isInstalledApp && !isResponding && (
+ {!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
- {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
+ {(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
{!feedback?.rating && (
<>
diff --git a/web/app/components/apps/hooks/use-dsl-drag-drop.ts b/web/app/components/apps/hooks/use-dsl-drag-drop.ts
index dda5773062..77d89b87da 100644
--- a/web/app/components/apps/hooks/use-dsl-drag-drop.ts
+++ b/web/app/components/apps/hooks/use-dsl-drag-drop.ts
@@ -36,7 +36,7 @@ export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true
if (!e.dataTransfer)
return
- const files = [...e.dataTransfer.files]
+ const files = Array.from(e.dataTransfer.files)
if (files.length === 0)
return
diff --git a/web/app/components/apps/index.spec.tsx b/web/app/components/apps/index.spec.tsx
index c3dc39955d..c77c1bdb01 100644
--- a/web/app/components/apps/index.spec.tsx
+++ b/web/app/components/apps/index.spec.tsx
@@ -1,3 +1,5 @@
+import type { ReactNode } from 'react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import * as React from 'react'
@@ -22,6 +24,15 @@ vi.mock('@/app/education-apply/hooks', () => ({
},
}))
+vi.mock('@/hooks/use-import-dsl', () => ({
+ useImportDSL: () => ({
+ handleImportDSL: vi.fn(),
+ handleImportDSLConfirm: vi.fn(),
+ versions: [],
+ isFetching: false,
+ }),
+}))
+
// Mock List component
vi.mock('./list', () => ({
default: () => {
@@ -30,6 +41,25 @@ vi.mock('./list', () => ({
}))
describe('Apps', () => {
+ const createQueryClient = () => new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ })
+
+ const renderWithClient = (ui: React.ReactElement) => {
+ const queryClient = createQueryClient()
+ const wrapper = ({ children }: { children: ReactNode }) => (
+
{children}
+ )
+ return {
+ queryClient,
+ ...render(ui, { wrapper }),
+ }
+ }
+
beforeEach(() => {
vi.clearAllMocks()
documentTitleCalls = []
@@ -38,17 +68,17 @@ describe('Apps', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
- render(
)
+ renderWithClient(
)
expect(screen.getByTestId('apps-list')).toBeInTheDocument()
})
it('should render List component', () => {
- render(
)
+ renderWithClient(
)
expect(screen.getByText('Apps List')).toBeInTheDocument()
})
it('should have correct container structure', () => {
- const { container } = render(
)
+ const { container } = renderWithClient(
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('relative', 'flex', 'h-0', 'shrink-0', 'grow', 'flex-col')
})
@@ -56,19 +86,19 @@ describe('Apps', () => {
describe('Hooks', () => {
it('should call useDocumentTitle with correct title', () => {
- render(
)
+ renderWithClient(
)
expect(documentTitleCalls).toContain('common.menus.apps')
})
it('should call useEducationInit', () => {
- render(
)
+ renderWithClient(
)
expect(educationInitCalls).toBeGreaterThan(0)
})
})
describe('Integration', () => {
it('should render full component tree', () => {
- render(
)
+ renderWithClient(
)
// Verify container exists
expect(screen.getByTestId('apps-list')).toBeInTheDocument()
@@ -79,23 +109,32 @@ describe('Apps', () => {
})
it('should handle multiple renders', () => {
- const { rerender } = render(
)
+ const queryClient = createQueryClient()
+ const { rerender } = render(
+
+
+ ,
+ )
expect(screen.getByTestId('apps-list')).toBeInTheDocument()
- rerender(
)
+ rerender(
+
+
+ ,
+ )
expect(screen.getByTestId('apps-list')).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have overflow-y-auto class', () => {
- const { container } = render(
)
+ const { container } = renderWithClient(
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('overflow-y-auto')
})
it('should have background styling', () => {
- const { container } = render(
)
+ const { container } = renderWithClient(
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('bg-background-body')
})
diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx
index b151df1e1f..255bfbf9c5 100644
--- a/web/app/components/apps/index.tsx
+++ b/web/app/components/apps/index.tsx
@@ -1,7 +1,17 @@
'use client'
+import type { CreateAppModalProps } from '../explore/create-app-modal'
+import type { CurrentTryAppParams } from '@/context/explore-context'
+import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useEducationInit } from '@/app/education-apply/hooks'
+import AppListContext from '@/context/app-list-context'
import useDocumentTitle from '@/hooks/use-document-title'
+import { useImportDSL } from '@/hooks/use-import-dsl'
+import { DSLImportMode } from '@/models/app'
+import { fetchAppDetail } from '@/service/explore'
+import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
+import CreateAppModal from '../explore/create-app-modal'
+import TryApp from '../explore/try-app'
import List from './list'
const Apps = () => {
@@ -10,10 +20,124 @@ const Apps = () => {
useDocumentTitle(t('menus.apps', { ns: 'common' }))
useEducationInit()
+ const [currentTryAppParams, setCurrentTryAppParams] = useState
(undefined)
+ const currApp = currentTryAppParams?.app
+ const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
+ const hideTryAppPanel = useCallback(() => {
+ setIsShowTryAppPanel(false)
+ }, [])
+ const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
+ if (showTryAppPanel)
+ setCurrentTryAppParams(params)
+ else
+ setCurrentTryAppParams(undefined)
+ setIsShowTryAppPanel(showTryAppPanel)
+ }
+ const [isShowCreateModal, setIsShowCreateModal] = useState(false)
+
+ const handleShowFromTryApp = useCallback(() => {
+ setIsShowCreateModal(true)
+ }, [])
+
+ const [controlRefreshList, setControlRefreshList] = useState(0)
+ const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0)
+ const onSuccess = useCallback(() => {
+ setControlRefreshList(prev => prev + 1)
+ setControlHideCreateFromTemplatePanel(prev => prev + 1)
+ }, [])
+
+ const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
+
+ const {
+ handleImportDSL,
+ handleImportDSLConfirm,
+ versions,
+ isFetching,
+ } = useImportDSL()
+
+ const onConfirmDSL = useCallback(async () => {
+ await handleImportDSLConfirm({
+ onSuccess,
+ })
+ }, [handleImportDSLConfirm, onSuccess])
+
+ const onCreate: CreateAppModalProps['onConfirm'] = async ({
+ name,
+ icon_type,
+ icon,
+ icon_background,
+ description,
+ }) => {
+ hideTryAppPanel()
+
+ const { export_data } = await fetchAppDetail(
+ currApp?.app.id as string,
+ )
+ const payload = {
+ mode: DSLImportMode.YAML_CONTENT,
+ yaml_content: export_data,
+ name,
+ icon_type,
+ icon,
+ icon_background,
+ description,
+ }
+ await handleImportDSL(payload, {
+ onSuccess: () => {
+ setIsShowCreateModal(false)
+ },
+ onPending: () => {
+ setShowDSLConfirmModal(true)
+ },
+ })
+ }
+
return (
-
-
-
+
+
+
+ {isShowTryAppPanel && (
+
+ )}
+
+ {
+ showDSLConfirmModal && (
+ setShowDSLConfirmModal(false)}
+ onConfirm={onConfirmDSL}
+ confirmDisabled={isFetching}
+ />
+ )
+ }
+
+ {isShowCreateModal && (
+ setIsShowCreateModal(false)}
+ />
+ )}
+
+
)
}
diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx
index 8a236fe260..6bf79b7338 100644
--- a/web/app/components/apps/list.tsx
+++ b/web/app/components/apps/list.tsx
@@ -1,5 +1,6 @@
'use client'
+import type { FC } from 'react'
import {
RiApps2Line,
RiDragDropLine,
@@ -53,7 +54,12 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
ssr: false,
})
-const List = () => {
+type Props = {
+ controlRefreshList?: number
+}
+const List: FC = ({
+ controlRefreshList = 0,
+}) => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
@@ -110,6 +116,13 @@ const List = () => {
refetch,
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
+ useEffect(() => {
+ if (controlRefreshList > 0) {
+ refetch()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [controlRefreshList])
+
const anchorRef = useRef(null)
const options = [
{ value: 'all', text: t('types.all', { ns: 'app' }), icon: },
diff --git a/web/app/components/apps/new-app-card.tsx b/web/app/components/apps/new-app-card.tsx
index bfa7af3892..868da0dcb5 100644
--- a/web/app/components/apps/new-app-card.tsx
+++ b/web/app/components/apps/new-app-card.tsx
@@ -6,10 +6,12 @@ import {
useSearchParams,
} from 'next/navigation'
import * as React from 'react'
-import { useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { useContextSelector } from 'use-context-selector'
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
+import AppListContext from '@/context/app-list-context'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
@@ -55,6 +57,13 @@ const CreateAppCard = ({
return undefined
}, [dslUrl])
+ const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel)
+ useEffect(() => {
+ if (controlHideCreateFromTemplatePanel > 0)
+ // eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
+ setShowNewAppTemplateDialog(false)
+ }, [controlHideCreateFromTemplatePanel])
+
return (
{
+const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => {
return (
{
@@ -239,7 +244,14 @@ const ChatInputArea = ({
)
}
+ )}
>
)
}
diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
index 27e5bf6cad..5bce827754 100644
--- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
@@ -8,6 +8,7 @@ import {
RiMicLine,
RiSendPlane2Fill,
} from '@remixicon/react'
+import { noop } from 'es-toolkit/function'
import { memo } from 'react'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
@@ -15,6 +16,7 @@ import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
import { cn } from '@/utils/classnames'
type OperationProps = {
+ readonly?: boolean
fileConfig?: FileUpload
speechToTextConfig?: EnableType
onShowVoiceInput?: () => void
@@ -23,6 +25,7 @@ type OperationProps = {
ref?: Ref