diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
index cbfd679ace..1fad833933 100644
--- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx
+++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx
@@ -91,6 +91,21 @@ vi.mock('@/service/use-workflow', () => ({
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
}))
+vi.mock('@/service/use-tools', () => ({
+ useWorkflowToolDetailByAppID: () => ({
+ data: undefined,
+ isLoading: false,
+ }),
+ useInvalidateAllWorkflowTools: () => vi.fn(),
+ useInvalidateWorkflowToolDetailByAppID: () => vi.fn(),
+}))
+
+vi.mock('@/context/app-context', () => ({
+ useAppContext: () => ({
+ isCurrentWorkspaceManager: true,
+ }),
+}))
+
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (...args: unknown[]) => mockToastError(...args),
@@ -121,6 +136,15 @@ vi.mock('../../app-access-control', () => ({
),
}))
+vi.mock('@/app/components/tools/workflow-tool', () => ({
+ WorkflowToolDrawer: ({ onHide }: { onHide: () => void }) => (
+
+ workflow tool drawer
+
+
+ ),
+}))
+
vi.mock('@langgenius/dify-ui/popover', () => import('@/__mocks__/base-ui-popover'))
vi.mock('../sections', () => ({
@@ -143,6 +167,7 @@ vi.mock('../sections', () => ({
+
)
},
@@ -231,6 +256,25 @@ describe('AppPublisher', () => {
expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
})
+ it('should keep workflow tool drawer mounted after closing the publish popover', () => {
+ mockAppDetail = {
+ ...mockAppDetail,
+ mode: AppModeEnum.WORKFLOW,
+ }
+
+ render(
+
,
+ )
+
+ fireEvent.click(screen.getByText('common.publish'))
+ fireEvent.click(screen.getByText('publisher-workflow-tool'))
+
+ expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
+ expect(screen.getByTestId('workflow-tool-drawer')).toBeInTheDocument()
+ })
+
it('should close embedded and access control panels through child callbacks', async () => {
render(
{
disabledFunctionTooltip="disabled"
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
- handlePublish={vi.fn()}
hasHumanInputNode={false}
hasTriggerNode={false}
- inputs={[]}
missingStartNode={false}
- onRefreshData={vi.fn()}
- outputs={[]}
- published={true}
publishedAt={Date.now()}
toolPublished
workflowToolAvailable={false}
+ workflowToolIsLoading={false}
+ workflowToolOutdated={false}
+ workflowToolIsCurrentWorkspaceManager
workflowToolMessage="workflow-disabled"
+ onConfigureWorkflowTool={vi.fn()}
/>,
)
@@ -223,17 +222,16 @@ describe('app-publisher sections', () => {
disabledFunctionTooltip="disabled"
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
- handlePublish={vi.fn()}
hasHumanInputNode={false}
hasTriggerNode={false}
- inputs={[]}
missingStartNode
- onRefreshData={vi.fn()}
- outputs={[]}
- published={false}
publishedAt={Date.now()}
toolPublished={false}
workflowToolAvailable
+ workflowToolIsLoading={false}
+ workflowToolOutdated={false}
+ workflowToolIsCurrentWorkspaceManager
+ onConfigureWorkflowTool={vi.fn()}
/>,
)
@@ -248,16 +246,16 @@ describe('app-publisher sections', () => {
disabledFunctionButton={false}
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
- handlePublish={vi.fn()}
hasHumanInputNode={false}
hasTriggerNode
- inputs={[]}
missingStartNode={false}
- outputs={[]}
- published={false}
publishedAt={undefined}
toolPublished={false}
workflowToolAvailable
+ workflowToolIsLoading={false}
+ workflowToolOutdated={false}
+ workflowToolIsCurrentWorkspaceManager
+ onConfigureWorkflowTool={vi.fn()}
/>,
)
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx
index fe6fe5806f..a066233107 100644
--- a/web/app/components/app/app-publisher/index.tsx
+++ b/web/app/components/app/app-publisher/index.tsx
@@ -5,13 +5,12 @@ import type { PublishWorkflowParams } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
-import { RiStoreLine } from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useKeyPress } from 'ahooks'
import {
memo,
+ use,
useCallback,
- useContext,
useEffect,
useMemo,
useState,
@@ -20,9 +19,12 @@ import { useTranslation } from 'react-i18next'
import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store'
import { trackEvent } from '@/app/components/base/amplitude'
+import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool'
+import { useConfigureButton } from '@/app/components/tools/workflow-tool/hooks/use-configure-button'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
import { WorkflowContext } from '@/app/components/workflow/context'
+import { appDefaultIconBackground } from '@/config'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { AccessMode } from '@/models/access-control'
@@ -57,8 +59,8 @@ export type AppPublisherProps = {
debugWithMultipleModel?: boolean
multipleModelConfigs?: ModelAndParameter[]
/** modelAndParameter is passed when debugWithMultipleModel is true */
- onPublish?: (params?: any) => Promise | any
- onRestore?: () => Promise | any
+ onPublish?: AppPublisherPublishHandler
+ onRestore?: AppPublisherRestoreHandler
onToggle?: (state: boolean) => void
crossAxisOffset?: number
toolPublished?: boolean
@@ -74,6 +76,12 @@ export type AppPublisherProps = {
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
+type AppPublisherPublishHandler
+ = | ((params?: ModelAndParameter | PublishWorkflowParams) => Promise | unknown)
+ | ((params?: unknown) => Promise | unknown)
+
+type AppPublisherRestoreHandler = () => Promise | unknown
+
const AppPublisher = ({
disabled = false,
publishDisabled = false,
@@ -100,11 +108,12 @@ const AppPublisher = ({
const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false)
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
+ const [workflowToolDrawerOpen, setWorkflowToolDrawerOpen] = useState(false)
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
const [publishingToMarketplace, setPublishingToMarketplace] = useState(false)
- const workflowStore = useContext(WorkflowContext)
+ const workflowStore = use(WorkflowContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(s => s.setAppDetail)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
@@ -273,6 +282,31 @@ const AppPublisher = ({
const workflowToolMessage = !hasPublishedVersion || !workflowToolAvailable
? t('common.workflowAsToolDisabledHint', { ns: 'workflow' })
: undefined
+ const workflowToolVisible = appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
+ const workflowToolPublished = !!toolPublished
+ const closeWorkflowToolDrawer = useCallback(() => setWorkflowToolDrawerOpen(false), [])
+ const workflowToolIcon = useMemo(() => ({
+ content: (appDetail?.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
+ background: (appDetail?.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
+ }), [appDetail?.icon, appDetail?.icon_background, appDetail?.icon_type])
+ const workflowTool = useConfigureButton({
+ enabled: workflowToolVisible,
+ published: workflowToolPublished,
+ detailNeedUpdate: workflowToolPublished && published,
+ workflowAppId: appDetail?.id ?? '',
+ icon: workflowToolIcon,
+ name: appDetail?.name ?? '',
+ description: appDetail?.description ?? '',
+ inputs,
+ outputs,
+ handlePublish,
+ onRefreshData,
+ onConfigured: closeWorkflowToolDrawer,
+ })
+ const openWorkflowToolDrawer = useCallback(() => {
+ handleOpenChange(false)
+ setWorkflowToolDrawerOpen(true)
+ }, [handleOpenChange])
const upgradeHighlightStyle = useMemo(() => ({
background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)',
WebkitBackgroundClip: 'text',
@@ -343,23 +377,22 @@ const AppPublisher = ({
handleOpenChange(false)
handleOpenInExplore()
}}
- handlePublish={handlePublish}
hasHumanInputNode={hasHumanInputNode}
hasTriggerNode={hasTriggerNode}
- inputs={inputs}
missingStartNode={missingStartNode}
- onRefreshData={onRefreshData}
- outputs={outputs}
- published={published}
publishedAt={publishedAt}
toolPublished={toolPublished}
workflowToolAvailable={workflowToolAvailable}
+ workflowToolIsLoading={workflowTool.isLoading}
+ workflowToolOutdated={workflowTool.outdated}
+ workflowToolIsCurrentWorkspaceManager={workflowTool.isCurrentWorkspaceManager}
workflowToolMessage={workflowToolMessage}
+ onConfigureWorkflowTool={openWorkflowToolDrawer}
/>
{systemFeatures.enable_creators_platform && (
}
+ icon={
}
disabled={!publishedAt || publishingToMarketplace}
onClick={handlePublishToMarketplace}
>
@@ -380,6 +413,15 @@ const AppPublisher = ({
/>
{showAppAccessControl &&
{ setShowAppAccessControl(false) }} />}
+ {workflowToolDrawerOpen && (
+
+ )}
>
)
}
diff --git a/web/app/components/app/app-publisher/sections.tsx b/web/app/components/app/app-publisher/sections.tsx
index 57522095ae..36422e0055 100644
--- a/web/app/components/app/app-publisher/sections.tsx
+++ b/web/app/components/app/app-publisher/sections.tsx
@@ -10,11 +10,9 @@ import {
} from '@langgenius/dify-ui/tooltip'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
-import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
import Loading from '@/app/components/base/loading'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
-import { appDefaultIconBackground } from '@/config'
import { AppModeEnum } from '@/types/app'
import ShortcutsName from '../../workflow/shortcuts-name'
import PublishWithMultipleModel from './publish-with-multiple-model'
@@ -46,11 +44,8 @@ type AccessSectionProps = {
type ActionsSectionProps = Pick & {
appDetail: {
@@ -67,9 +62,11 @@ type ActionsSectionProps = Pick void
handleOpenInExplore: () => void
- handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise
- published: boolean
+ workflowToolIsLoading: boolean
+ workflowToolOutdated: boolean
+ workflowToolIsCurrentWorkspaceManager: boolean
workflowToolMessage?: string
+ onConfigureWorkflowTool: () => void
}
export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => {
@@ -256,18 +253,17 @@ export const PublisherActionsSection = ({
disabledFunctionTooltip,
handleEmbed,
handleOpenInExplore,
- handlePublish,
hasHumanInputNode = false,
hasTriggerNode = false,
- inputs,
missingStartNode = false,
- onRefreshData,
- outputs,
- published,
publishedAt,
toolPublished,
workflowToolAvailable = true,
+ workflowToolIsLoading,
+ workflowToolOutdated,
+ workflowToolIsCurrentWorkspaceManager,
workflowToolMessage,
+ onConfigureWorkflowTool,
}: ActionsSectionProps) => {
const { t } = useTranslation()
@@ -305,7 +301,7 @@ export const PublisherActionsSection = ({
}
+ icon={}
>
{t('common.embedIntoSite', { ns: 'workflow' })}
@@ -340,18 +336,10 @@ export const PublisherActionsSection = ({
)}
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
index a350e3f316..261ad1a280 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
@@ -54,22 +54,22 @@ const Operation: FC = ({
onOpenChange={setOpen}
>
}
+ render={(
+
+
+
+ )}
onClick={e => e.stopPropagation()}
- >
-
-
-
-
+ />
{
act(() => {
capturedResizeCallbacks[0]?.([makeResizeEntry(80, 400)], {} as ResizeObserver)
+ flushAnimationFrames()
})
expect(screen.getByTestId('chat-container').style.paddingBottom).toBe('80px')
act(() => {
capturedResizeCallbacks[1]?.([makeResizeEntry(50, 560)], {} as ResizeObserver)
+ flushAnimationFrames()
})
expect(screen.getByTestId('chat-footer').style.width).toBe('560px')
diff --git a/web/app/components/base/chat/chat/use-chat-layout.ts b/web/app/components/base/chat/chat/use-chat-layout.ts
index 712382070d..1983a928ca 100644
--- a/web/app/components/base/chat/chat/use-chat-layout.ts
+++ b/web/app/components/base/chat/chat/use-chat-layout.ts
@@ -12,6 +12,11 @@ type UseChatLayoutOptions = {
sidebarCollapseState?: boolean
}
+const setStyleValue = (element: HTMLElement, property: 'paddingBottom' | 'width', value: string) => {
+ if (element.style[property] !== value)
+ element.style[property] = value
+}
+
export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutOptions) => {
const [width, setWidth] = useState(0)
const chatContainerRef = useRef(null)
@@ -21,6 +26,9 @@ export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutO
const userScrolledRef = useRef(false)
const isAutoScrollingRef = useRef(false)
const prevFirstMessageIdRef = useRef(undefined)
+ const resizeObserverFrameRef = useRef(null)
+ const pendingFooterBlockSizeRef = useRef(null)
+ const pendingContainerInlineSizeRef = useRef(null)
const handleScrollToBottom = useCallback(() => {
if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current) {
@@ -34,16 +42,39 @@ export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutO
}, [chatList.length])
const handleWindowResize = useCallback(() => {
- if (chatContainerRef.current)
- setWidth(document.body.clientWidth - (chatContainerRef.current.clientWidth + 16) - 8)
+ if (chatContainerRef.current) {
+ const nextWidth = document.body.clientWidth - (chatContainerRef.current.clientWidth + 16) - 8
+ setWidth(currentWidth => currentWidth === nextWidth ? currentWidth : nextWidth)
+ }
if (chatContainerRef.current && chatFooterRef.current)
- chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
+ setStyleValue(chatFooterRef.current, 'width', `${chatContainerRef.current.clientWidth}px`)
if (chatContainerInnerRef.current && chatFooterInnerRef.current)
- chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
+ setStyleValue(chatFooterInnerRef.current, 'width', `${chatContainerInnerRef.current.clientWidth}px`)
}, [])
+ const scheduleResizeObserverUpdate = useCallback(() => {
+ if (resizeObserverFrameRef.current !== null)
+ return
+
+ resizeObserverFrameRef.current = requestAnimationFrame(() => {
+ resizeObserverFrameRef.current = null
+
+ const footerBlockSize = pendingFooterBlockSizeRef.current
+ pendingFooterBlockSizeRef.current = null
+ if (footerBlockSize !== null && chatContainerRef.current) {
+ setStyleValue(chatContainerRef.current, 'paddingBottom', `${footerBlockSize}px`)
+ handleScrollToBottom()
+ }
+
+ const containerInlineSize = pendingContainerInlineSizeRef.current
+ pendingContainerInlineSizeRef.current = null
+ if (containerInlineSize !== null && chatFooterRef.current)
+ setStyleValue(chatFooterRef.current, 'width', `${containerInlineSize}px`)
+ })
+ }, [handleScrollToBottom])
+
useEffect(() => {
handleScrollToBottom()
const animationFrame = requestAnimationFrame(handleWindowResize)
@@ -77,26 +108,31 @@ export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutO
const resizeContainerObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { blockSize } = entry.borderBoxSize[0]!
- chatContainerRef.current!.style.paddingBottom = `${blockSize}px`
- handleScrollToBottom()
+ pendingFooterBlockSizeRef.current = blockSize
}
+ scheduleResizeObserverUpdate()
})
resizeContainerObserver.observe(chatFooterRef.current)
const resizeFooterObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBoxSize[0]!
- chatFooterRef.current!.style.width = `${inlineSize}px`
+ pendingContainerInlineSizeRef.current = inlineSize
}
+ scheduleResizeObserverUpdate()
})
resizeFooterObserver.observe(chatContainerRef.current)
return () => {
+ if (resizeObserverFrameRef.current !== null) {
+ cancelAnimationFrame(resizeObserverFrameRef.current)
+ resizeObserverFrameRef.current = null
+ }
resizeContainerObserver.disconnect()
resizeFooterObserver.disconnect()
}
}
- }, [handleScrollToBottom])
+ }, [scheduleResizeObserverUpdate])
useEffect(() => {
const setUserScrolled = () => {
diff --git a/web/app/components/base/icons/src/vender/line/development/index.ts b/web/app/components/base/icons/src/vender/line/development/index.ts
index 7c3c48aa5e..4278370eec 100644
--- a/web/app/components/base/icons/src/vender/line/development/index.ts
+++ b/web/app/components/base/icons/src/vender/line/development/index.ts
@@ -1,2 +1 @@
export { default as BracketsX } from './BracketsX'
-export { default as CodeBrowser } from './CodeBrowser'
diff --git a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx
index dc0dd438ce..900c12a416 100644
--- a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx
@@ -120,18 +120,12 @@ vi.mock('../document-title', () => ({
}))
vi.mock('../segment-add', () => ({
- default: ({ showNewSegmentModal, showBatchModal, embedding }: { showNewSegmentModal?: () => void, showBatchModal?: () => void, embedding?: boolean }) => (
+ SegmentAdd: ({ showNewSegmentModal, showBatchModal, embedding }: { showNewSegmentModal?: () => void, showBatchModal?: () => void, embedding?: boolean }) => (
),
- ProcessStatus: {
- WAITING: 'waiting',
- PROCESSING: 'processing',
- ERROR: 'error',
- COMPLETED: 'completed',
- },
}))
vi.mock('../../components/operations', () => ({
diff --git a/web/app/components/datasets/documents/detail/batch-modal/index.tsx b/web/app/components/datasets/documents/detail/batch-modal/index.tsx
index c0d9a58e98..4e190ef3fd 100644
--- a/web/app/components/datasets/documents/detail/batch-modal/index.tsx
+++ b/web/app/components/datasets/documents/detail/batch-modal/index.tsx
@@ -2,12 +2,15 @@
import type { FC } from 'react'
import type { ChunkingMode, FileItem } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
-import { RiCloseLine } from '@remixicon/react'
-import { noop } from 'es-toolkit/function'
+import {
+ Dialog,
+ DialogCloseButton,
+ DialogContent,
+ DialogTitle,
+} from '@langgenius/dify-ui/dialog'
import * as React from 'react'
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import Modal from '@/app/components/base/modal'
import CSVDownloader from './csv-downloader'
import CSVUploader from './csv-uploader'
@@ -18,8 +21,9 @@ type IBatchModalProps = {
onConfirm: (file: FileItem) => void
}
-const BatchModal: FC = ({
- isShow,
+type BatchModalContentProps = Omit
+
+const BatchModalContent: FC = ({
docForm,
onCancel,
onConfirm,
@@ -35,17 +39,13 @@ const BatchModal: FC = ({
onConfirm(currentCSV)
}
- useEffect(() => {
- if (!isShow)
- setCurrentCSV(undefined)
- }, [isShow])
-
return (
-
- {t('list.batchModal.title', { ns: 'datasetDocuments' })}
-
-
-
+
+ {t('list.batchModal.title', { ns: 'datasetDocuments' })}
+
= ({
{t('list.batchModal.run', { ns: 'datasetDocuments' })}
-
+
)
}
+
+const BatchModal: FC = ({
+ isShow,
+ docForm,
+ onCancel,
+ onConfirm,
+}) => {
+ return (
+
+ )
+}
+
export default React.memo(BatchModal)
diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
index f50b405c6f..900c974252 100644
--- a/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
+++ b/web/app/components/datasets/documents/detail/completed/__tests__/index.spec.tsx
@@ -137,9 +137,8 @@ vi.mock('../hooks/use-child-segment-data', () => ({
},
}))
-// Mock child components to simplify testing
-vi.mock('../components', () => ({
- MenuBar: ({ totalText, onInputChange, inputValue, isLoading, onSelectedAll, onChangeStatus }: {
+vi.mock('../components/menu-bar', () => ({
+ default: ({ totalText, onInputChange, inputValue, isLoading, onSelectedAll, onChangeStatus }: {
totalText: string
onInputChange: (value: string) => void
inputValue: string
@@ -167,7 +166,13 @@ vi.mock('../components', () => ({
)}
),
+}))
+
+vi.mock('../components/drawer-group', () => ({
DrawerGroup: () =>