Compare commits

...
Author SHA1 Message Date
twwu 6e75e7c5b0 Merge branch 'main' into feat/app-multi-env-deployments 2026-07-28 18:10:30 +08:00
twwu 8c91bce282 feat(workflow): update workflow tool publish action 2026-07-28 17:58:30 +08:00
twwu 24a66293fc feat: refine app publishing and dirty tracking 2026-07-28 16:17:57 +08:00
twwu be88e6adda feat(workflow): redesign publisher action menu 2026-07-27 16:47:33 +08:00
twwu 3b832da46a feat(workflow): show deployment details in version history
Preview workflow IDs in the copy action, add mock environment badges, and fix the action menu trigger's native button semantics.
2026-07-27 13:54:37 +08:00
twwu 8812fd649c feat: Add app detail routes for Access Point and Deploy 2026-07-27 11:04:00 +08:00
99 changed files with 3298 additions and 912 deletions
-8
View File
@@ -363,14 +363,6 @@
"count": 1
}
},
"web/app/components/app/app-publisher/sections.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 1
},
"jsx_a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx": {
"jsx_a11y/click-events-have-key-events": {
"count": 1
@@ -0,0 +1,5 @@
import { AppDetailSidebarSlot } from '../sidebar-page'
export default function AppAccessPointDetailSidebarSlot() {
return <AppDetailSidebarSlot />
}
@@ -0,0 +1,5 @@
import { AppDetailSidebarSlot } from '../sidebar-page'
export default function AppDeployDetailSidebarSlot() {
return <AppDetailSidebarSlot />
}
@@ -0,0 +1,3 @@
export default function AppAccessPointPage() {
return null
}
@@ -0,0 +1,3 @@
export default function AppDeployPage() {
return null
}
@@ -7,10 +7,12 @@ let mockAppMode = 'chat'
let mockPathname = '/app/app-1/logs'
let mockAppPermissionKeys: string[] = []
let mockIsRbacEnabled = true
let mockEnableAppDeploy = false
const mockConsoleState = vi.hoisted(() => ({
current: {
userProfile: { id: 'user-1' },
workspacePermissionKeys: [] as string[],
isCurrentWorkspaceEditor: true,
},
}))
@@ -18,6 +20,7 @@ const render = (ui: Parameters<typeof renderWithConsoleQuery>[0]) =>
renderWithConsoleQuery(ui, {
systemFeatures: {
rbac_enabled: mockIsRbacEnabled,
enable_app_deploy: mockEnableAppDeploy,
},
})
@@ -44,6 +47,10 @@ vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => mockConsoleState.current)
})
vi.mock('@/next/navigation', () => ({
usePathname: () => mockPathname,
@@ -72,6 +79,8 @@ describe('AppDetailSection', () => {
mockPathname = '/app/app-1/logs'
mockAppPermissionKeys = [AppACLPermission.Monitor]
mockIsRbacEnabled = true
mockEnableAppDeploy = false
mockConsoleState.current.isCurrentWorkspaceEditor = true
})
// Rendering behavior for app detail navigation entries.
@@ -183,6 +192,72 @@ describe('AppDetailSection', () => {
).not.toBeInTheDocument()
})
it('should render access point navigation using its app route', () => {
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.accessPoint' })).toHaveAttribute(
'href',
'/app/app-1/access-point',
)
expect(
screen.queryByRole('link', { name: 'common.appMenus.apiAccess' }),
).not.toBeInTheDocument()
})
it('should render deploy navigation for workflow apps when app deploy is available', () => {
// Arrange
mockAppMode = 'workflow'
mockEnableAppDeploy = true
// Act
render(<AppDetailSection />)
// Assert
expect(screen.getByRole('link', { name: 'common.appMenus.deploy' })).toHaveAttribute(
'href',
'/app/app-1/deploy',
)
})
it.each([
{
label: 'the app is not a workflow app',
mode: 'chat',
enableAppDeploy: true,
isCurrentWorkspaceEditor: true,
},
{
label: 'app deploy is disabled',
mode: 'workflow',
enableAppDeploy: false,
isCurrentWorkspaceEditor: true,
},
{
label: 'the current workspace role cannot use app deploy',
mode: 'workflow',
enableAppDeploy: true,
isCurrentWorkspaceEditor: false,
},
])(
'should hide deploy navigation when $label',
({ mode, enableAppDeploy, isCurrentWorkspaceEditor }) => {
// Arrange
mockAppMode = mode
mockEnableAppDeploy = enableAppDeploy
mockConsoleState.current.isCurrentWorkspaceEditor = isCurrentWorkspaceEditor
// Act
render(<AppDetailSection />)
// Assert
expect(
screen.queryByRole('link', { name: 'common.appMenus.deploy' }),
).not.toBeInTheDocument()
},
)
it('should render resource access navigation when app access config permission is granted', () => {
// Arrange
mockAppPermissionKeys = [AppACLPermission.AccessConfig]
@@ -3,18 +3,6 @@
import type { ComponentProps } from 'react'
import type { NavIcon } from './nav-link'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiDashboard2Fill,
RiDashboard2Line,
RiFileList3Fill,
RiFileList3Line,
RiLock2Fill,
RiLock2Line,
RiTerminalBoxFill,
RiTerminalBoxLine,
RiTerminalWindowFill,
RiTerminalWindowLine,
} from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { Fragment, useMemo } from 'react'
@@ -24,6 +12,7 @@ import Divider from '@/app/components/base/divider'
import Annotations from '@/app/components/base/icons/src/vender/Annotations'
import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { isCurrentWorkspaceEditorAtom } from '@/context/workspace-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { usePathname } from '@/next/navigation'
import { AppModeEnum } from '@/types/app'
@@ -45,6 +34,28 @@ const AnnotationNavIcon = ({ className, ...props }: ComponentProps<typeof Annota
AnnotationNavIcon.displayName = 'Annotations'
const createClassNameNavIcon = (iconClassName: string) => {
const ClassNameNavIcon = ({ className }: ComponentProps<'svg'>) => (
<span aria-hidden className={cn(iconClassName, className)} />
)
ClassNameNavIcon.displayName = 'ClassNameNavIcon'
return ClassNameNavIcon
}
const accessPointNavIcon = createClassNameNavIcon('i-custom-vender-agent-v2-access-point')
const terminalWindowLineNavIcon = createClassNameNavIcon('i-ri-terminal-window-line')
const terminalWindowFillNavIcon = createClassNameNavIcon('i-ri-terminal-window-fill')
const instanceLineNavIcon = createClassNameNavIcon('i-ri-instance-line')
const instanceFillNavIcon = createClassNameNavIcon('i-ri-instance-fill')
const fileListLineNavIcon = createClassNameNavIcon('i-ri-file-list-3-line')
const fileListFillNavIcon = createClassNameNavIcon('i-ri-file-list-3-fill')
const dashboardLineNavIcon = createClassNameNavIcon('i-ri-dashboard-2-line')
const dashboardFillNavIcon = createClassNameNavIcon('i-ri-dashboard-2-fill')
const lockLineNavIcon = createClassNameNavIcon('i-ri-lock-2-line')
const lockFillNavIcon = createClassNameNavIcon('i-ri-lock-2-fill')
const isLogsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/logs')
const isAnnotationsNavItem = (item: AppDetailNavItem) => item.href.endsWith('/annotations')
@@ -73,6 +84,7 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom)
const isRbacEnabled = systemFeatures.rbac_enabled
const appDetail = useStore((state) => state.appDetail)
const appInfoActions = useAppInfoActions({
@@ -85,6 +97,7 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
const appId = appDetail.id
const isWorkflowApp =
appDetail.mode === AppModeEnum.WORKFLOW || appDetail.mode === AppModeEnum.ADVANCED_CHAT
const supportsAppDeploy = appDetail.mode === AppModeEnum.WORKFLOW
const supportsAnnotations =
appDetail.mode !== AppModeEnum.WORKFLOW && appDetail.mode !== AppModeEnum.COMPLETION
const appACLCapabilities = getAppACLCapabilities(appDetail.permission_keys, {
@@ -100,24 +113,34 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
{
name: t(($) => $['appMenus.promptEng'], { ns: 'common' }),
href: `/app/${appId}/${isWorkflowApp ? 'workflow' : 'configuration'}`,
icon: RiTerminalWindowLine,
selectedIcon: RiTerminalWindowFill,
icon: terminalWindowLineNavIcon,
selectedIcon: terminalWindowFillNavIcon,
},
]
: []),
{
name: t(($) => $['appMenus.apiAccess'], { ns: 'common' }),
href: `/app/${appId}/develop`,
icon: RiTerminalBoxLine,
selectedIcon: RiTerminalBoxFill,
name: t(($) => $['appMenus.accessPoint'], { ns: 'common' }),
href: `/app/${appId}/access-point`,
icon: accessPointNavIcon,
selectedIcon: accessPointNavIcon,
},
...(supportsAppDeploy && isCurrentWorkspaceEditor && systemFeatures.enable_app_deploy
? [
{
name: t(($) => $['appMenus.deploy'], { ns: 'common' }),
href: `/app/${appId}/deploy`,
icon: instanceLineNavIcon,
selectedIcon: instanceFillNavIcon,
},
]
: []),
...(appACLCapabilities.canAccessLogAndAnnotation
? [
{
name: t(($) => $['appMenus.logs'], { ns: 'common' }),
href: `/app/${appId}/logs`,
icon: RiFileList3Line,
selectedIcon: RiFileList3Fill,
icon: fileListLineNavIcon,
selectedIcon: fileListFillNavIcon,
},
]
: []),
@@ -136,8 +159,8 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
{
name: t(($) => $['appMenus.overview'], { ns: 'common' }),
href: `/app/${appId}/overview`,
icon: RiDashboard2Line,
selectedIcon: RiDashboard2Fill,
icon: dashboardLineNavIcon,
selectedIcon: dashboardFillNavIcon,
},
]
: []),
@@ -146,13 +169,21 @@ const AppDetailSection = ({ expand = true }: AppDetailSectionProps) => {
{
name: t(($) => $['settings.resourceAccess'], { ns: 'common' }),
href: `/app/${appId}/access-config`,
icon: RiLock2Line,
selectedIcon: RiLock2Fill,
icon: lockLineNavIcon,
selectedIcon: lockFillNavIcon,
},
]
: []),
]
}, [appDetail, t, currentUserId, workspacePermissionKeys, isRbacEnabled])
}, [
appDetail,
t,
currentUserId,
workspacePermissionKeys,
isCurrentWorkspaceEditor,
isRbacEnabled,
systemFeatures.enable_app_deploy,
])
if (!appDetail) return null
@@ -57,6 +57,7 @@ vi.mock('@/app/components/base/features/hooks', () => ({
}))
describe('FeaturesWrappedAppPublisher', () => {
const resetAppConfig = vi.fn()
const publishedConfig = {
modelConfig: {
more_like_this: { enabled: true },
@@ -81,7 +82,6 @@ describe('FeaturesWrappedAppPublisher', () => {
allowed_file_upload_methods: ['remote_url'],
number_limits: 5,
},
resetAppConfig: vi.fn(),
},
}
@@ -106,13 +106,18 @@ describe('FeaturesWrappedAppPublisher', () => {
})
it('should restore published features after confirmation', async () => {
render(<FeaturesWrappedAppPublisher publishedConfig={publishedConfig as any} />)
render(
<FeaturesWrappedAppPublisher
publishedConfig={publishedConfig as any}
resetAppConfig={resetAppConfig}
/>,
)
fireEvent.click(screen.getByText('restore-through-wrapper'))
fireEvent.click(screen.getByRole('button', { name: /(?:^|\.)operation\.confirm(?=$|:)/ }))
await waitFor(() => {
expect(publishedConfig.modelConfig.resetAppConfig).toHaveBeenCalledTimes(1)
expect(resetAppConfig).toHaveBeenCalledTimes(1)
expect(mockSetFeatures).toHaveBeenCalledWith(
expect.objectContaining({
moreLikeThis: { enabled: true },
@@ -128,12 +133,18 @@ describe('FeaturesWrappedAppPublisher', () => {
citation: { enabled: true },
annotationReply: { enabled: true },
}),
{ silent: true },
)
})
})
it('should close restore confirmation without restoring when cancelled', async () => {
render(<FeaturesWrappedAppPublisher publishedConfig={publishedConfig as any} />)
render(
<FeaturesWrappedAppPublisher
publishedConfig={publishedConfig as any}
resetAppConfig={resetAppConfig}
/>,
)
fireEvent.click(screen.getByText('restore-through-wrapper'))
const dialog = screen.getByRole('alertdialog')
@@ -145,7 +156,7 @@ describe('FeaturesWrappedAppPublisher', () => {
await waitFor(() => {
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
})
expect(publishedConfig.modelConfig.resetAppConfig).not.toHaveBeenCalled()
expect(resetAppConfig).not.toHaveBeenCalled()
expect(mockSetFeatures).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,8 @@
/* oxlint-disable typescript/no-explicit-any */
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { WorkflowContext } from '@/app/components/workflow/context'
import { AccessMode } from '@/models/access-control'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
@@ -18,12 +20,14 @@ const mockSetAppDetail = vi.fn()
const mockTrackEvent = vi.fn()
const mockRefetch = vi.fn()
const mockUseGetUserCanAccessApp = vi.fn()
const mockOpenAsyncWindow = vi.fn()
const mockFetchInstalledAppList = vi.fn()
const mockFetchAppDetail = vi.fn()
const mockToastError = vi.fn()
const mockToastSuccess = vi.fn()
const mockWindowOpen = vi.fn()
const mockInvalidateAppWorkflow = vi.fn()
const mockUpdateWorkflow = vi.fn()
const mockFetchPublishedWorkflow = vi.fn()
let mockPublishedWorkflow: Record<string, any> | null = null
const sectionProps = vi.hoisted(() => ({
summary: null as null | Record<string, any>,
@@ -34,9 +38,20 @@ const hotkeyMocks = vi.hoisted(() => ({
hotkeys: [] as string[],
handlers: [] as Array<(event: { preventDefault: () => void }) => void>,
}))
const collaborationMocks = vi.hoisted(() => ({
handler: undefined as
| ((update: {
type: 'app_publish_update'
userId: string
data: Record<string, unknown>
timestamp: number
}) => void)
| undefined,
}))
let mockAppDetail: Record<string, any> | null = null
let mockWorkspacePermissionKeys: string[] = ['tool.manage']
let mockIsCurrentWorkspaceEditor = true
vi.mock('@tanstack/react-hotkeys', () => ({
useHotkey: (hotkey: string, handler: (event: { preventDefault: () => void }) => void) => {
@@ -64,10 +79,6 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
}),
}))
vi.mock('@/hooks/use-async-window-open', () => ({
useAsyncWindowOpen: () => mockOpenAsyncWindow,
}))
vi.mock('@/service/access-control/use-app-access-control', () => ({
useGetUserCanAccessApp: (params: unknown) => {
mockUseGetUserCanAccessApp(params)
@@ -83,10 +94,6 @@ vi.mock('@/service/access-control/use-app-access-control', () => ({
}),
}))
vi.mock('@/service/explore', () => ({
fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args),
}))
const mockPublishToCreatorsPlatform = vi.fn()
vi.mock('@/service/apps', () => ({
@@ -95,7 +102,22 @@ vi.mock('@/service/apps', () => ({
}))
vi.mock('@/service/use-workflow', () => ({
appWorkflowQueryOptions: (appId: string) => ({
queryKey: ['workflow', 'publish', appId],
queryFn: () => mockFetchPublishedWorkflow(appId),
}),
useAppWorkflow: () => ({ data: mockPublishedWorkflow, isSuccess: true }),
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
useUpdateWorkflow: () => ({ mutate: mockUpdateWorkflow }),
}))
vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({
collaborationManager: {
onAppPublishUpdate: (handler: NonNullable<(typeof collaborationMocks)['handler']>) => {
collaborationMocks.handler = handler
return vi.fn()
},
},
}))
vi.mock('@/service/use-tools', () => ({
@@ -110,6 +132,7 @@ vi.mock('@/service/use-tools', () => ({
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => ({
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor,
isCurrentWorkspaceManager: true,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
@@ -125,6 +148,7 @@ vi.mock('@/context/permission-state', async () => {
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (...args: unknown[]) => mockToastError(...args),
success: (...args: unknown[]) => mockToastSuccess(...args),
},
}))
@@ -132,16 +156,6 @@ vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
}))
vi.mock('@/app/components/app/overview/embedded', () => ({
default: ({ isShow, onClose }: { isShow: boolean; onClose: () => void }) =>
isShow ? (
<div data-testid="embedded-modal">
embedded modal
<button onClick={onClose}>close-embedded-modal</button>
</div>
) : null,
}))
vi.mock('../../app-access-control', () => {
const MockAccessControl = ({
onConfirm,
@@ -180,6 +194,7 @@ vi.mock('../sections', () => ({
<div>
<button onClick={() => void props.handlePublish()}>publisher-summary-publish</button>
<button onClick={() => void props.handleRestore()}>publisher-summary-restore</button>
<button onClick={props.onEditVersion}>publisher-summary-edit-version</button>
</div>
)
},
@@ -190,18 +205,18 @@ vi.mock('../sections', () => ({
PublisherActionsSection: (props: Record<string, any>) => {
sectionProps.actions = props
return (
<div>
<button onClick={props.handleEmbed}>publisher-embed</button>
<button onClick={() => void props.handleOpenInExplore()}>publisher-open-in-explore</button>
{props.handleOpenRunConfig && (
<>
<button onClick={() => props.handleOpenRunConfig(props.appURL)}>
publisher-run-config
</button>
<button onClick={() => props.handleOpenRunConfig(`${props.appURL}?mode=batch`)}>
publisher-batch-run-config
</button>
</>
<div data-testid="publisher-actions">
{props.showRunConfig && props.handleOpenRunConfig && (
<button onClick={() => props.handleOpenRunConfig(props.appURL)}>
publisher-run-config
</button>
)}
{props.showMarketplaceAction && (
<button disabled={props.marketplaceActionDisabled} onClick={props.onPublishToMarketplace}>
{props.publishingToMarketplace
? 'workflow.common.publishingToMarketplace'
: 'workflow.common.publishToMarketplace'}
</button>
)}
<button onClick={props.onConfigureWorkflowTool}>publisher-workflow-tool</button>
</div>
@@ -214,10 +229,14 @@ describe('AppPublisher', () => {
vi.clearAllMocks()
hotkeyMocks.hotkeys.length = 0
hotkeyMocks.handlers.length = 0
collaborationMocks.handler = undefined
sectionProps.summary = null
sectionProps.access = null
sectionProps.actions = null
mockPublishedWorkflow = null
mockFetchPublishedWorkflow.mockResolvedValue(null)
mockWorkspacePermissionKeys = ['tool.manage']
mockIsCurrentWorkspaceEditor = true
mockAppDetail = {
id: 'app-1',
name: 'Demo App',
@@ -228,17 +247,12 @@ describe('AppPublisher', () => {
access_token: 'token-1',
},
}
mockFetchInstalledAppList.mockResolvedValue({
installed_apps: [{ id: 'installed-1' }],
})
mockFetchAppDetail.mockResolvedValue({
id: 'app-1',
access_mode: AccessMode.PUBLIC,
})
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
return resolver()
})
Object.defineProperty(window, 'open', {
configurable: true,
writable: true,
value: mockWindowOpen,
})
@@ -282,16 +296,74 @@ describe('AppPublisher', () => {
})
})
it('should open the embedded modal from the actions section', () => {
it('should edit the current workflow version from the publish summary', () => {
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
mockPublishedWorkflow = {
created_at: Math.floor(Date.now() / 1000),
id: 'workflow-version-5',
marked_name: 'Release 5',
marked_comment: 'Initial notes',
}
render(<AppPublisher publishedAt={Date.now()} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-embed'))
expect(sectionProps.summary).toEqual(
expect.objectContaining({
isWorkflowApp: true,
versionInfo: mockPublishedWorkflow,
}),
)
fireEvent.click(screen.getByText('publisher-summary-edit-version'))
expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument()
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
const [titleInput, notesInput] = screen.getAllByRole('textbox')
fireEvent.change(titleInput!, { target: { value: 'Release 6' } })
fireEvent.change(notesInput!, { target: { value: 'Updated notes' } })
const publishButtons = screen.getAllByRole('button', {
name: /(?:^|\.)common\.publish(?=$|:)/,
})
fireEvent.click(publishButtons.at(-1)!)
expect(mockUpdateWorkflow).toHaveBeenCalledWith(
{
url: '/apps/app-1/workflows/workflow-version-5',
title: 'Release 6',
releaseNotes: 'Updated notes',
},
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
onSettled: expect.any(Function),
}),
)
const mutationCallbacks = mockUpdateWorkflow.mock.calls[0]![1]
mutationCallbacks.onSuccess()
expect(mockInvalidateAppWorkflow).toHaveBeenCalledWith('app-1')
expect(mockToastSuccess).toHaveBeenCalled()
})
it('should collect hidden inputs before opening published run links from config actions', async () => {
it('should expose the Deploy quick link for editable workflow apps when enabled', () => {
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
renderWithConsoleQuery(<AppPublisher publishedAt={Date.now()} />, {
systemFeatures: { webapp_auth: { enabled: true }, enable_app_deploy: true },
})
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions?.showDeployAction).toBe(true)
expect(sectionProps.actions?.appURL).toContain('/workflow/token-1')
})
it('should collect hidden inputs before opening the web app from its config action', async () => {
render(
<AppPublisher
publishedAt={Date.now()}
@@ -309,6 +381,8 @@ describe('AppPublisher', () => {
)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions?.showRunConfig).toBe(true)
fireEvent.click(screen.getByText('publisher-run-config'))
expect(
@@ -330,55 +404,21 @@ describe('AppPublisher', () => {
})
})
it('should open batch run config links with the configured hidden inputs', async () => {
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
render(
<AppPublisher
publishedAt={Date.now()}
inputs={[
{
variable: 'batch_secret',
label: 'Batch Secret',
type: 'text-input',
required: true,
hide: true,
default: '',
} as any,
]}
/>,
)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-batch-run-config'))
fireEvent.change(screen.getByLabelText('Batch Secret'), {
target: { value: 'batch-value' },
})
fireEvent.click(
screen.getByRole('button', { name: /(?:^|\.)overview\.appInfo\.launch(?=$|:)/ }),
)
await waitFor(() => {
expect(mockWindowOpen).toHaveBeenCalledWith(
`https://example.com${basePath}/workflow/token-1?mode=batch&batch_secret=${encodeURIComponent('batch-value')}`,
'_blank',
)
})
})
it('should keep workflow tool drawer mounted after closing the publish popover', () => {
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
render(<AppPublisher publishedAt={Date.now()} />)
render(<AppPublisher publishedAt={Date.now()} toolPublished />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions).toEqual(
expect.objectContaining({
toolPublished: true,
workflowToolOutdated: false,
}),
)
fireEvent.click(screen.getByText('publisher-workflow-tool'))
expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()
@@ -401,14 +441,9 @@ describe('AppPublisher', () => {
expect(sectionProps.actions?.workflowToolAvailable).toBe(false)
})
it('should close embedded and access control panels through child callbacks', async () => {
it('should close access control through its child callback', async () => {
render(<AppPublisher publishedAt={Date.now()} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-embed'))
fireEvent.click(screen.getByText('close-embedded-modal'))
expect(screen.queryByTestId('embedded-modal')).not.toBeInTheDocument()
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-access-control'))
expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
@@ -443,25 +478,6 @@ describe('AppPublisher', () => {
)
})
it('should open the installed explore page through the async window helper', async () => {
let openedUrl = ''
mockOpenAsyncWindow.mockImplementation(async (resolver: () => Promise<string>) => {
openedUrl = await resolver()
})
render(<AppPublisher publishedAt={Date.now()} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-open-in-explore'))
await waitFor(() => {
expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1)
expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1')
expect(openedUrl).toBe('/installed/installed-1')
expect(sectionProps.actions?.appURL).toBe(`https://example.com${basePath}/chat/token-1`)
})
})
it('should ignore the trigger when the publish button is disabled', () => {
render(<AppPublisher disabled publishedAt={Date.now()} onToggle={mockOnToggle} />)
@@ -479,8 +495,13 @@ describe('AppPublisher', () => {
const onRestore = vi.fn().mockResolvedValue(undefined)
mockOnPublish.mockResolvedValue(undefined)
render(
<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
const { rerender } = render(
<AppPublisher
hasUnpublishedChanges
publishedAt={Date.now()}
onPublish={mockOnPublish}
onRestore={onRestore}
/>,
)
expect(hotkeyMocks.hotkeys).toContain('Mod+Shift+P')
@@ -491,6 +512,30 @@ describe('AppPublisher', () => {
expect(mockOnPublish).toHaveBeenCalledTimes(1)
})
rerender(
<AppPublisher
hasUnpublishedChanges={false}
publishedAt={Date.now()}
onPublish={mockOnPublish}
onRestore={onRestore}
/>,
)
hotkeyMocks.handlers.at(-1)!({ preventDefault })
expect(mockOnPublish).toHaveBeenCalledTimes(1)
rerender(
<AppPublisher
hasUnpublishedChanges
publishedAt={Date.now()}
onPublish={mockOnPublish}
onRestore={onRestore}
/>,
)
hotkeyMocks.handlers.at(-1)!({ preventDefault })
await waitFor(() => {
expect(mockOnPublish).toHaveBeenCalledTimes(2)
})
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-summary-restore'))
@@ -500,13 +545,44 @@ describe('AppPublisher', () => {
expect(screen.queryByText('publisher-summary-publish')).not.toBeInTheDocument()
})
it('should keep the popover open when restore fails and reset published state after publish failures', async () => {
it('should require an explicit model selection when publishing in multiple model mode', () => {
const preventDefault = vi.fn()
render(
<AppPublisher
debugWithMultipleModel
hasUnpublishedChanges
multipleModelConfigs={[
{
id: 'model-1',
model: 'gpt-4o',
provider: 'openai',
parameters: {},
},
]}
publishedAt={Date.now()}
onPublish={mockOnPublish}
/>,
)
hotkeyMocks.handlers[0]!({ preventDefault })
expect(preventDefault).not.toHaveBeenCalled()
expect(mockOnPublish).not.toHaveBeenCalled()
})
it('should keep the popover open when restore and publish fail', async () => {
const preventDefault = vi.fn()
const onRestore = vi.fn().mockRejectedValue(new Error('restore failed'))
mockOnPublish.mockRejectedValueOnce(new Error('publish failed'))
render(
<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} onRestore={onRestore} />,
<AppPublisher
hasUnpublishedChanges
publishedAt={Date.now()}
onPublish={mockOnPublish}
onRestore={onRestore}
/>,
)
hotkeyMocks.handlers[0]!({ preventDefault })
@@ -526,57 +602,6 @@ describe('AppPublisher', () => {
expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument()
})
it('should report missing explore installations', async () => {
mockFetchInstalledAppList.mockResolvedValueOnce({
installed_apps: [],
})
mockOpenAsyncWindow.mockImplementation(
async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
try {
await resolver()
} catch (error) {
options.onError(error as Error)
}
},
)
render(<AppPublisher publishedAt={Date.now()} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-open-in-explore'))
await waitFor(() => {
expect(mockToastError).toHaveBeenCalledWith(
expect.stringMatching(/(?:^|\.)notPublishedYet(?=$|:)/),
)
})
})
it('should report explore errors when the app cannot be opened', async () => {
mockAppDetail = {
...mockAppDetail,
id: undefined,
}
mockOpenAsyncWindow.mockImplementation(
async (resolver: () => Promise<string>, options: { onError: (error: Error) => void }) => {
try {
await resolver()
} catch (error) {
options.onError(error as Error)
}
},
)
render(<AppPublisher publishedAt={Date.now()} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
fireEvent.click(screen.getByText('publisher-open-in-explore'))
await waitFor(() => {
expect(mockToastError).toHaveBeenCalledWith('App not found')
})
})
it('should show marketplace button and open redirect URL on success', async () => {
mockPublishToCreatorsPlatform.mockResolvedValue({
redirect_url: 'https://marketplace.example.com/publish?code=abc',
@@ -588,6 +613,12 @@ describe('AppPublisher', () => {
})
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions).toEqual(
expect.objectContaining({
marketplaceActionDisabled: false,
showMarketplaceAction: true,
}),
)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/))
await waitFor(() => {
@@ -624,6 +655,12 @@ describe('AppPublisher', () => {
})
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions).toEqual(
expect.objectContaining({
marketplaceActionDisabled: true,
showMarketplaceAction: true,
}),
)
const marketplaceButton = screen
.getByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/)
.closest('a, button, div[role="button"]') as HTMLElement
@@ -637,6 +674,7 @@ describe('AppPublisher', () => {
render(<AppPublisher publishedAt={Date.now()} onPublish={mockOnPublish} />)
fireEvent.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.actions?.showMarketplaceAction).toBe(false)
expect(
screen.queryByText(/(?:^|\.)common\.publishToMarketplace(?=$|:)/),
).not.toBeInTheDocument()
@@ -656,4 +694,116 @@ describe('AppPublisher', () => {
})
expect(screen.getByTestId('access-control'))!.toBeInTheDocument()
})
it('should not infer an app mode when app detail is unavailable', async () => {
const user = userEvent.setup()
mockAppDetail = null
render(<AppPublisher publishedAt={Date.now()} />)
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.summary).toEqual(
expect.objectContaining({
isChatApp: false,
isWorkflowApp: false,
}),
)
})
it('should derive workflow changes from draft and published hashes', async () => {
const user = userEvent.setup()
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
mockPublishedWorkflow = {
created_at: 1_710_000_100,
hash: 'published-hash',
}
const { rerender } = render(
<AppPublisher
draftHash="published-hash"
draftUpdatedAt={1_710_000_200_000}
publishedAt={1_710_000_100_000}
/>,
)
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.summary?.hasUnpublishedChanges).toBe(false)
rerender(
<AppPublisher
draftHash="changed-draft-hash"
draftUpdatedAt={1_710_000_100_000}
publishedAt={1_710_000_200_000}
/>,
)
expect(sectionProps.summary?.hasUnpublishedChanges).toBe(true)
})
it('should keep workflow publishing available when the published hash is unavailable', async () => {
const user = userEvent.setup()
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
mockPublishedWorkflow = {
created_at: 1_710_000_100,
hash: '',
}
render(
<AppPublisher
draftHash="draft-hash"
draftUpdatedAt={1_710_000_200_000}
publishedAt={1_710_000_100_000}
/>,
)
await user.click(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/))
expect(sectionProps.summary).toEqual(
expect.objectContaining({
hasUnpublishedChanges: true,
publishedAt: 1_710_000_100_000,
}),
)
})
it('should refresh the shared workflow query and store after a collaborator publishes', async () => {
const setPublishedAt = vi.fn()
const workflowStore = {
getState: () => ({ setPublishedAt }),
}
mockAppDetail = {
...mockAppDetail,
mode: AppModeEnum.WORKFLOW,
}
mockFetchPublishedWorkflow.mockResolvedValue({
created_at: 1_710_000_300,
hash: 'published-hash',
})
render(
<WorkflowContext value={workflowStore as any}>
<AppPublisher draftHash="draft-hash" publishedAt={1_710_000_100_000} />
</WorkflowContext>,
)
act(() => {
collaborationMocks.handler?.({
type: 'app_publish_update',
userId: 'collaborator-1',
data: { action: 'published' },
timestamp: 1_710_000_300_000,
})
})
await waitFor(() => {
expect(mockFetchPublishedWorkflow).toHaveBeenCalledWith('app-1')
expect(setPublishedAt).toHaveBeenCalledWith(1_710_000_300)
})
})
})
@@ -143,6 +143,27 @@ describe('PublishWithMultipleModel', () => {
expect(screen.queryByText(/(?:^|\.)publishAs(?=$|:)/)).not.toBeInTheDocument()
})
it('should disable the trigger when publishing is unavailable', () => {
render(
<PublishWithMultipleModel
disabled
multipleModelConfigs={[
{
id: 'config-1',
provider: 'openai',
model: 'gpt-4o',
parameters: {},
},
]}
onSelect={vi.fn()}
/>,
)
expect(
screen.getByRole('button', { name: /(?:^|\.)operation\.applyConfig(?=$|:)/ }),
).toBeDisabled()
})
it('should open matching model options and call onSelect', () => {
const handleSelect = vi.fn()
const modelConfig = {
@@ -1,6 +1,7 @@
/* oxlint-disable typescript/no-explicit-any */
import type { ReactNode } from 'react'
import type { VersionHistory } from '@/types/workflow'
import { fireEvent, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AccessMode } from '@/models/access-control'
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
import { AppModeEnum } from '@/types/app'
@@ -12,45 +13,19 @@ import {
} from '../sections'
vi.mock('../publish-with-multiple-model', () => ({
default: ({ onSelect }: { onSelect: (item: Record<string, unknown>) => void }) => (
<button type="button" onClick={() => onSelect({ model: 'gpt-4o' })}>
default: ({
disabled,
onSelect,
}: {
disabled?: boolean
onSelect: (item: Record<string, unknown>) => void
}) => (
<button type="button" disabled={disabled} onClick={() => onSelect({ model: 'gpt-4o' })}>
publish-multiple-model
</button>
),
}))
vi.mock('../suggested-action', () => ({
default: ({
children,
onClick,
link,
disabled,
actionButton,
}: {
children: ReactNode
onClick?: () => void
link?: string
disabled?: boolean
actionButton?: { ariaLabel: string; onClick: () => void }
}) => (
<div>
<button type="button" data-link={link} disabled={disabled} onClick={onClick}>
{children}
</button>
{actionButton && (
<button
type="button"
aria-label={actionButton.ariaLabel}
disabled={disabled}
onClick={actionButton.onClick}
>
{actionButton.ariaLabel}
</button>
)}
</div>
),
}))
vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({
default: (props: Record<string, unknown>) => (
<div>
@@ -60,6 +35,32 @@ vi.mock('@/app/components/tools/workflow-tool/configure-button', () => ({
),
}))
const createVersionInfo = (overrides: Partial<VersionHistory> = {}): VersionHistory => ({
id: 'workflow-version-1',
graph: {
nodes: [],
edges: [],
},
created_at: 1_710_000_000,
created_by: {
id: 'user-1',
name: 'Alice',
email: 'alice@example.com',
},
hash: 'hash-1',
updated_at: 1_710_000_000,
updated_by: {
id: 'user-1',
name: 'Alice',
email: 'alice@example.com',
},
tool_published: false,
version: '2024-03-09T16:00:00Z',
marked_name: '',
marked_comment: '',
...overrides,
})
describe('app-publisher sections', () => {
it('should render restore controls for published chat apps', () => {
const handleRestore = vi.fn()
@@ -71,10 +72,10 @@ describe('app-publisher sections', () => {
formatTimeFromNow={() => '3 minutes ago'}
handlePublish={vi.fn()}
handleRestore={handleRestore}
hasUnpublishedChanges
isChatApp
multipleModelConfigs={[]}
publishDisabled={false}
published={false}
publishedAt={Date.now()}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
@@ -85,6 +86,35 @@ describe('app-publisher sections', () => {
expect(handleRestore).toHaveBeenCalled()
})
it('should disable restore for published chat apps without unpublished changes', async () => {
const user = userEvent.setup()
const handleRestore = vi.fn()
render(
<PublisherSummarySection
debugWithMultipleModel={false}
draftUpdatedAt={Date.now()}
formatTimeFromNow={() => '3 minutes ago'}
handlePublish={vi.fn()}
handleRestore={handleRestore}
hasUnpublishedChanges={false}
isChatApp
multipleModelConfigs={[]}
publishDisabled={false}
publishedAt={Date.now()}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
/>,
)
const restoreButton = screen.getByRole('button', {
name: /(?:^|\.)common\.restore(?=$|:)/,
})
expect(restoreButton).toBeDisabled()
await user.click(restoreButton)
expect(handleRestore).not.toHaveBeenCalled()
})
it('should expose the access control warning when subjects are missing', () => {
render(
<PublisherAccessSection
@@ -100,7 +130,7 @@ describe('app-publisher sections', () => {
expect(screen.getByText(/(?:^|\.)publishApp\.notSetDesc(?=$|:)/)).toBeInTheDocument()
})
it('should render the publish update action when the draft has not been published yet', () => {
it('should render the initial publish action when the draft has not been published yet', () => {
render(
<PublisherSummarySection
debugWithMultipleModel={false}
@@ -111,17 +141,118 @@ describe('app-publisher sections', () => {
isChatApp={false}
multipleModelConfigs={[]}
publishDisabled={false}
published={false}
publishedAt={undefined}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
/>,
)
expect(screen.getByText(/(?:^|\.)common\.publishUpdate(?=$|:)/)).toBeInTheDocument()
expect(screen.getByText(/(?:^|\.)common\.notPublishedYet(?=$|:)/)).toBeInTheDocument()
expect(screen.getByText(/(?:^|\.)common\.publish(?=$|:)/)).toBeInTheDocument()
expect(screen.getByText('P')).toBeInTheDocument()
expect(screen.getByText(/(?:^|\.)common\.unpublishedChanges(?=$|:)/)).toBeInTheDocument()
})
it('should render multiple-model publishing', () => {
it('should expose naming for an unnamed published workflow with no draft changes', () => {
const onEditVersion = vi.fn()
render(
<PublisherSummarySection
debugWithMultipleModel={false}
draftUpdatedAt={1_710_000_000_000}
formatTimeFromNow={() => '17 days ago'}
handlePublish={vi.fn()}
handleRestore={vi.fn()}
hasUnpublishedChanges={false}
isChatApp={false}
isWorkflowApp
multipleModelConfigs={[]}
onEditVersion={onEditVersion}
publishDisabled={false}
publishedAt={1_710_000_100_000}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
versionInfo={createVersionInfo()}
/>,
)
expect(screen.getByText('2024-03-09T16:00:00Z')).toBeInTheDocument()
const nameButton = screen.getByRole('button', {
name: /versionHistory\.nameIt\b/,
})
fireEvent.click(nameButton)
expect(onEditVersion).toHaveBeenCalledTimes(1)
expect(screen.getByRole('button', { name: /common\.published\b/ })).toBeDisabled()
expect(screen.queryByText('P')).not.toBeInTheDocument()
expect(screen.getByText(/common\.noChanges\b/)).toBeInTheDocument()
})
it('should show named workflow metadata and publish update when its draft changed', () => {
const onEditVersion = vi.fn()
render(
<PublisherSummarySection
debugWithMultipleModel={false}
draftUpdatedAt={1_710_000_200_000}
formatTimeFromNow={() => '2 minutes ago'}
handlePublish={vi.fn()}
handleRestore={vi.fn()}
hasUnpublishedChanges
isChatApp={false}
isWorkflowApp
multipleModelConfigs={[]}
onEditVersion={onEditVersion}
publishDisabled={false}
publishedAt={1_710_000_100_000}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
versionInfo={createVersionInfo({
marked_name: 'Sprint-42',
marked_comment: 'Fixed data synchronization and page loading.',
})}
/>,
)
expect(screen.getByText('Sprint-42')).toBeInTheDocument()
expect(screen.getByText('Fixed data synchronization and page loading.')).toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', {
name: /versionHistory\.editVersionInfo\b/,
}),
)
expect(onEditVersion).toHaveBeenCalledTimes(1)
expect(screen.getByRole('button', { name: /common\.publishUpdate\b/ })).toBeEnabled()
expect(screen.getByText(/common\.unpublishedChanges\b/)).toBeInTheDocument()
expect(screen.getByText(/common\.savedAt\b/)).toBeInTheDocument()
})
it('should keep non-workflow apps free of workflow version details and saved time', () => {
render(
<PublisherSummarySection
debugWithMultipleModel={false}
draftUpdatedAt={1_710_000_200_000}
formatTimeFromNow={() => '2 minutes ago'}
handlePublish={vi.fn()}
handleRestore={vi.fn()}
hasUnpublishedChanges
isChatApp
isWorkflowApp={false}
multipleModelConfigs={[]}
publishDisabled={false}
publishedAt={1_710_000_100_000}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
/>,
)
expect(screen.getByText(/common\.latestPublished\b/)).toBeInTheDocument()
expect(screen.queryByText('#5')).not.toBeInTheDocument()
expect(screen.queryByText(/versionHistory\.nameIt\b/)).not.toBeInTheDocument()
expect(screen.getByText(/common\.unpublishedChanges\b/)).toBeInTheDocument()
expect(screen.queryByText(/common\.savedAt\b/)).not.toBeInTheDocument()
})
it('should keep multiple-model publishing available without publish config changes', () => {
const handlePublish = vi.fn()
render(
@@ -131,11 +262,11 @@ describe('app-publisher sections', () => {
formatTimeFromNow={() => '1 minute ago'}
handlePublish={handlePublish}
handleRestore={vi.fn()}
hasUnpublishedChanges={false}
isChatApp={false}
multipleModelConfigs={[{ id: '1' } as any]}
publishDisabled={false}
published={false}
publishedAt={undefined}
publishedAt={Date.now()}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
/>,
@@ -146,6 +277,27 @@ describe('app-publisher sections', () => {
expect(handlePublish).toHaveBeenCalledWith({ model: 'gpt-4o' })
})
it('should disable multiple-model publishing when publishing is unavailable', () => {
render(
<PublisherSummarySection
debugWithMultipleModel
draftUpdatedAt={Date.now()}
formatTimeFromNow={() => '1 minute ago'}
handlePublish={vi.fn()}
handleRestore={vi.fn()}
hasUnpublishedChanges={false}
isChatApp={false}
multipleModelConfigs={[{ id: '1' } as any]}
publishDisabled
publishedAt={Date.now()}
startNodeLimitExceeded={false}
upgradeHighlightStyle={{}}
/>,
)
expect(screen.getByRole('button', { name: 'publish-multiple-model' })).toBeDisabled()
})
it('should render the upgrade hint when the start node limit is exceeded', () => {
render(
<PublisherSummarySection
@@ -157,7 +309,6 @@ describe('app-publisher sections', () => {
isChatApp={false}
multipleModelConfigs={[]}
publishDisabled={false}
published={false}
publishedAt={undefined}
startNodeLimitExceeded
upgradeHighlightStyle={{}}
@@ -213,12 +364,13 @@ describe('app-publisher sections', () => {
).not.toBeInTheDocument()
})
it('should render workflow actions, batch run links, and workflow tool configuration', () => {
const handleOpenInExplore = vi.fn()
const handleEmbed = vi.fn()
it('should render the published workflow actions with Workflow as Tool after Marketplace', async () => {
const user = userEvent.setup()
const handleOpenRunConfig = vi.fn()
const onConfigureWorkflowTool = vi.fn()
const onPublishToMarketplace = vi.fn()
const { rerender } = render(
render(
<PublisherActionsSection
appDetail={{
id: 'workflow-app',
@@ -232,92 +384,280 @@ describe('app-publisher sections', () => {
appURL="https://example.com/app"
disabledFunctionButton={false}
disabledFunctionTooltip="disabled"
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
handleOpenRunConfig={handleOpenRunConfig}
handlePublish={vi.fn()}
hasHumanInputNode={false}
hasTriggerNode={false}
missingStartNode={false}
published={false}
publishedAt={Date.now()}
showBatchRunConfig
showDeployAction
showMarketplaceAction
showRunConfig
toolPublished
workflowToolAvailable={false}
workflowToolAvailable
workflowToolIsLoading={false}
workflowToolOutdated={false}
workflowToolMessage="workflow-disabled"
onConfigureWorkflowTool={vi.fn()}
onPublishToMarketplace={onPublishToMarketplace}
onConfigureWorkflowTool={onConfigureWorkflowTool}
/>,
)
expect(screen.getByText(/(?:^|\.)common\.batchRunApp(?=$|:)/)).toHaveAttribute(
'data-link',
'https://example.com/app?mode=batch',
expect(screen.getByRole('link', { name: /common\.openWebApp\b/ })).toHaveAttribute(
'href',
'https://example.com/app',
)
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ })[0]!)
fireEvent.click(screen.getByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ }))
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app')
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)operation\.config(?=$|:)/ })[1]!)
expect(handleOpenRunConfig).toHaveBeenCalledWith('https://example.com/app?mode=batch')
fireEvent.click(screen.getByText(/(?:^|\.)common\.openInExplore(?=$|:)/))
expect(handleOpenInExplore).toHaveBeenCalled()
expect(screen.getByText('workflow-tool-configure')).toBeInTheDocument()
expect(screen.getByText('workflow-disabled')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /appMenus\.accessPoint\b/ })).toHaveAttribute(
'href',
'/app/workflow-app/access-point',
)
expect(screen.getByRole('link', { name: /appMenus\.deploy\b/ })).toHaveAttribute(
'href',
'/app/workflow-app/deploy',
)
rerender(
const marketplaceAction = screen.getByRole('button', {
name: /common\.publishToMarketplace\b/,
})
const workflowToolAction = screen.getByRole('button', {
name: /common\.workflowAsTool\b/,
})
expect(
marketplaceAction.compareDocumentPosition(workflowToolAction) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
expect(screen.getByRole('status', { name: /common\.configureRequired\b/ })).toBeInTheDocument()
await user.click(marketplaceAction)
expect(onPublishToMarketplace).toHaveBeenCalledTimes(1)
await user.click(workflowToolAction)
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
})
it('should expose Configure and Manage in Tools actions for a ready workflow tool', async () => {
const user = userEvent.setup()
const onConfigureWorkflowTool = vi.fn()
render(
<PublisherActionsSection
appDetail={{
id: 'chat-app',
mode: AppModeEnum.CHAT,
name: 'Chat App',
id: 'workflow-app',
mode: AppModeEnum.WORKFLOW,
name: 'Workflow App',
}}
appURL="https://example.com/app?foo=bar"
disabledFunctionButton
disabledFunctionTooltip="disabled"
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
handleOpenRunConfig={handleOpenRunConfig}
handlePublish={vi.fn()}
appURL="https://example.com/app"
disabledFunctionButton={false}
hasHumanInputNode={false}
hasTriggerNode={false}
missingStartNode
published={false}
publishedAt={Date.now()}
toolPublished={false}
showDeployAction
toolPublished
workflowToolAvailable
workflowToolIsLoading={false}
workflowToolOutdated={false}
onConfigureWorkflowTool={vi.fn()}
onConfigureWorkflowTool={onConfigureWorkflowTool}
/>,
)
fireEvent.click(screen.getByText(/(?:^|\.)common\.embedIntoSite(?=$|:)/))
expect(handleEmbed).toHaveBeenCalled()
expect(screen.getByText(/(?:^|\.)common\.accessAPIReference(?=$|:)/)).toBeDisabled()
expect(
screen.getByRole('status', { name: /common\.workflowAsToolReady\b/ }),
).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.manageInTools\b/ })).toHaveAttribute(
'href',
'/integrations/tools/workflow',
)
await user.click(screen.getByRole('button', { name: /common\.configure\b/ }))
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
})
it('should show the disabled reason below setup and configured workflow tool actions', () => {
const commonProps = {
appDetail: {
id: 'workflow-app',
mode: AppModeEnum.WORKFLOW,
},
appURL: 'https://example.com/app',
disabledFunctionButton: false,
hasHumanInputNode: false,
hasTriggerNode: false,
onConfigureWorkflowTool: vi.fn(),
publishedAt: Date.now(),
workflowToolAvailable: false,
workflowToolIsLoading: false,
workflowToolMessage: 'Workflow tool unavailable',
}
const { rerender } = render(<PublisherActionsSection {...commonProps} toolPublished={false} />)
const setupAction = screen.getByRole('button', { name: /common\.workflowAsTool\b/ })
const setupReason = screen.getByText('Workflow tool unavailable')
expect(setupAction).toBeDisabled()
expect(setupReason).toBeVisible()
expect(
setupAction.compareDocumentPosition(setupReason) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
rerender(<PublisherActionsSection {...commonProps} toolPublished />)
const configureAction = screen.getByRole('button', { name: /common\.configure\b/ })
const manageAction = screen.getByRole('button', { name: /common\.manageInTools\b/ })
const configuredReason = screen.getByText('Workflow tool unavailable')
expect(configureAction).toBeDisabled()
expect(manageAction).toBeDisabled()
expect(configuredReason).toBeVisible()
expect(
manageAction.compareDocumentPosition(configuredReason) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
})
it('should surface update-needed and loading states for a configured workflow tool', async () => {
const user = userEvent.setup()
const onConfigureWorkflowTool = vi.fn()
const commonProps = {
appDetail: {
id: 'workflow-app',
mode: AppModeEnum.WORKFLOW,
},
appURL: 'https://example.com/app',
disabledFunctionButton: false,
hasHumanInputNode: false,
hasTriggerNode: false,
onConfigureWorkflowTool,
publishedAt: Date.now(),
toolPublished: true,
workflowToolAvailable: true,
}
const { rerender } = render(
<PublisherActionsSection
{...commonProps}
workflowToolIsLoading={false}
workflowToolOutdated
/>,
)
expect(
screen.getByRole('status', { name: /common\.workflowAsToolUpdateNeeded\b/ }),
).toBeInTheDocument()
expect(screen.getByText(/common\.workflowAsToolTip\b/)).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: /common\.workflowAsToolReconfigure\b/ }))
expect(onConfigureWorkflowTool).toHaveBeenCalledTimes(1)
rerender(
<PublisherActionsSection
appDetail={{ id: 'trigger-app', mode: AppModeEnum.WORKFLOW }}
{...commonProps}
workflowToolIsLoading
workflowToolOutdated={false}
/>,
)
expect(screen.getByRole('button', { name: /common\.workflowAsTool\b/ })).toBeDisabled()
expect(screen.getByRole('status', { name: /loading\b/ })).toBeInTheDocument()
expect(screen.queryByText(/common\.workflowAsToolTip\b/)).not.toBeInTheDocument()
})
it('should keep Access Point and Deploy available for trigger workflows', () => {
render(
<PublisherActionsSection
appDetail={{
id: 'trigger-app',
mode: AppModeEnum.WORKFLOW,
}}
appURL="https://example.com/app"
disabledFunctionButton={false}
handleEmbed={handleEmbed}
handleOpenInExplore={handleOpenInExplore}
handleOpenRunConfig={handleOpenRunConfig}
handlePublish={vi.fn()}
hasHumanInputNode={false}
hasTriggerNode
missingStartNode={false}
published={false}
publishedAt={undefined}
toolPublished={false}
publishedAt={Date.now()}
showDeployAction
workflowToolAvailable
workflowToolIsLoading={false}
workflowToolOutdated={false}
onConfigureWorkflowTool={vi.fn()}
/>,
)
expect(screen.queryByText(/(?:^|\.)common\.runApp(?=$|:)/)).not.toBeInTheDocument()
expect(screen.queryByText(/(?:^|\.)common\.openWebApp(?=$|:)/)).not.toBeInTheDocument()
expect(screen.queryByText(/(?:^|\.)common\.workflowAsTool(?=$|:)/)).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /appMenus\.accessPoint\b/ })).toHaveAttribute(
'href',
'/app/trigger-app/access-point',
)
expect(screen.getByRole('link', { name: /appMenus\.deploy\b/ })).toHaveAttribute(
'href',
'/app/trigger-app/deploy',
)
})
it('should expose unavailable quick links as disabled buttons before the first publish', () => {
render(
<PublisherActionsSection
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
appURL="https://example.com/app"
disabledFunctionButton
hasHumanInputNode={false}
hasTriggerNode={false}
publishedAt={undefined}
showDeployAction
workflowToolAvailable
workflowToolIsLoading={false}
onConfigureWorkflowTool={vi.fn()}
/>,
)
expect(screen.getByText(/(?:^|\.)common\.openWebApp(?=$|:)/).closest('button')).toBeDisabled()
expect(
screen.getByText(/(?:^|\.)appMenus\.accessPoint(?=$|:)/).closest('button'),
).toBeDisabled()
expect(screen.getByText(/(?:^|\.)appMenus\.deploy(?=$|:)/).closest('button')).toBeDisabled()
expect(
screen.getByText(/(?:^|\.)common\.workflowAsTool(?=$|:)/).closest('button'),
).toBeDisabled()
})
it('should show the disabled reason when hovering an unavailable action', async () => {
const user = userEvent.setup()
render(
<PublisherActionsSection
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
appURL="https://example.com/app"
disabledFunctionButton
disabledFunctionTooltip="Open web app unavailable"
hasHumanInputNode={false}
hasTriggerNode={false}
publishedAt={undefined}
workflowToolAvailable
workflowToolIsLoading={false}
onConfigureWorkflowTool={vi.fn()}
/>,
)
await user.hover(screen.getByRole('button', { name: /common\.openWebApp\b/ }))
expect(await screen.findByRole('tooltip')).toHaveTextContent('Open web app unavailable')
})
it('should keep an unavailable action with a tooltip keyboard focusable', async () => {
const user = userEvent.setup()
render(
<PublisherActionsSection
appDetail={{ id: 'workflow-app', mode: AppModeEnum.WORKFLOW }}
appURL="https://example.com/app"
disabledFunctionButton
disabledFunctionTooltip="Open web app unavailable"
hasHumanInputNode={false}
hasTriggerNode={false}
publishedAt={undefined}
workflowToolAvailable
workflowToolIsLoading={false}
onConfigureWorkflowTool={vi.fn()}
/>,
)
await user.tab()
const action = screen.getByRole('button', { name: /common\.openWebApp\b/ })
expect(action).toHaveFocus()
expect(action).toHaveAttribute('aria-disabled', 'true')
expect(action).toHaveAccessibleDescription('Open web app unavailable')
expect(await screen.findByRole('tooltip')).toHaveTextContent('Open web app unavailable')
})
})
@@ -1,79 +1,133 @@
import type { MouseEvent as ReactMouseEvent } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SuggestedAction from '../suggested-action'
describe('SuggestedAction', () => {
it('should render an enabled external link', () => {
render(<SuggestedAction link="https://example.com/docs">Open docs</SuggestedAction>)
it('should render an enabled external link with supporting copy', () => {
render(
<SuggestedAction
link="https://example.com/docs"
external
description="Read the documentation"
>
Open docs
</SuggestedAction>,
)
const link = screen.getByRole('link', { name: 'Open docs' })
expect(link).toHaveAttribute('href', 'https://example.com/docs')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAccessibleDescription('Read the documentation')
expect(screen.getByText('Read the documentation')).toBeInTheDocument()
})
it('should block clicks when disabled', () => {
it('should render internal destinations without opening a new tab', () => {
render(
<SuggestedAction link="/app/app-1/deploy" description="Push versions to environments">
Deploy
</SuggestedAction>,
)
const link = screen.getByRole('link', { name: 'Deploy' })
expect(link).toHaveAttribute('href', '/app/app-1/deploy')
expect(link).not.toHaveAttribute('target')
expect(link).toHaveAccessibleDescription('Push versions to environments')
})
it('should use native disabled button semantics for unavailable links', () => {
const handleClick = vi.fn()
render(
<SuggestedAction link="https://example.com/docs" disabled onClick={handleClick}>
Disabled action
</SuggestedAction>,
)
const link = screen.getByText('Disabled action').closest('a') as HTMLAnchorElement
fireEvent.click(link)
expect(link).not.toHaveAttribute('href')
expect(handleClick).not.toHaveBeenCalled()
})
it('should forward click events when enabled', () => {
const handleClick = vi.fn((event: ReactMouseEvent<HTMLAnchorElement>) => {
event.preventDefault()
})
render(
<SuggestedAction link="https://example.com/docs" onClick={handleClick}>
Enabled action
</SuggestedAction>,
)
fireEvent.click(screen.getByRole('link', { name: 'Enabled action' }))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should render and trigger the trailing action button when configured', () => {
const handleActionClick = vi.fn()
render(
<SuggestedAction
link="https://example.com/docs"
disabled
description="Unavailable until published"
onClick={handleClick}
>
Disabled action
</SuggestedAction>,
)
const action = screen.getByRole('button', { name: 'Disabled action' })
fireEvent.click(action)
expect(action).toBeDisabled()
expect(screen.queryByRole('link', { name: /Disabled action/ })).not.toBeInTheDocument()
expect(handleClick).not.toHaveBeenCalled()
})
it('should keep an explained disabled action in the keyboard tab order', async () => {
const user = userEvent.setup()
const handleClick = vi.fn()
render(
<SuggestedAction disabled focusableWhenDisabled onClick={handleClick}>
Disabled action with explanation
</SuggestedAction>,
)
await user.tab()
const action = screen.getByRole('button', { name: 'Disabled action with explanation' })
expect(action).toHaveFocus()
expect(action).toHaveAttribute('aria-disabled', 'true')
await user.keyboard('{Enter}')
expect(handleClick).not.toHaveBeenCalled()
})
it('should render and trigger an enabled button action', () => {
const handleClick = vi.fn()
render(
<SuggestedAction
description="Use as a tool in other apps"
endIcon={<span data-testid="configure-icon" />}
onClick={handleClick}
>
Workflow as Tool
</SuggestedAction>,
)
fireEvent.click(screen.getByRole('button', { name: 'Workflow as Tool' }))
expect(handleClick).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('configure-icon')).toBeInTheDocument()
})
it('should keep the main link separate from a trailing action button', () => {
const handleActionClick = vi.fn()
render(
<SuggestedAction
link="https://example.com/app"
external
actionButton={{
ariaLabel: 'Configure action',
icon: <span>config</span>,
onClick: handleActionClick,
}}
>
Configurable action
Open web app
</SuggestedAction>,
)
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
expect(screen.getByRole('link', { name: 'Configurable action' })).toHaveAttribute(
expect(screen.getByRole('link', { name: 'Open web app' })).toHaveAttribute(
'href',
'https://example.com/docs',
'https://example.com/app',
)
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
expect(handleActionClick).toHaveBeenCalledTimes(1)
})
it('should block action button clicks when disabled', () => {
it('should disable both controls when an action with a trailing button is unavailable', () => {
const handleActionClick = vi.fn()
render(
<SuggestedAction
link="https://example.com/docs"
link="https://example.com/app"
external
disabled
actionButton={{
ariaLabel: 'Configure action',
@@ -81,11 +135,15 @@ describe('SuggestedAction', () => {
onClick: handleActionClick,
}}
>
Disabled with action
Open web app
</SuggestedAction>,
)
fireEvent.click(screen.getByRole('button', { name: 'Configure action' }))
expect(screen.getByRole('button', { name: 'Open web app' })).toBeDisabled()
const actionButton = screen.getByRole('button', { name: 'Configure action' })
fireEvent.click(actionButton)
expect(actionButton).toBeDisabled()
expect(handleActionClick).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,30 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
type ActionTooltipProps = {
children: ReactNode
disabled: boolean
tooltip?: ReactNode
}
const ActionTooltip = ({ children, disabled, tooltip }: ActionTooltipProps) => {
if (!tooltip) return <>{children}</>
return (
<Tooltip>
<TooltipTrigger
render={
<div
className={cn('flex w-full', disabled && 'cursor-not-allowed *:pointer-events-none')}
/>
}
>
{children}
</TooltipTrigger>
<TooltipContent role="tooltip">{tooltip}</TooltipContent>
</Tooltip>
)
}
export default ActionTooltip
@@ -0,0 +1 @@
export const APP_PUBLISH_DRAFT_CHANGED = 'APP_PUBLISH_DRAFT_CHANGED'
@@ -2,8 +2,8 @@ import type {
AppPublisherProps,
AppPublisherPublishParams,
} from '@/app/components/app/app-publisher'
import type { ConfigurationPublishConfig } from '@/app/components/app/configuration/hooks/use-configuration-utils'
import type { Features, FileUpload } from '@/app/components/base/features/types'
import type { ModelConfig } from '@/models/debug'
import {
AlertDialog,
AlertDialogActions,
@@ -14,7 +14,6 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AppPublisher } from '@/app/components/app/app-publisher'
@@ -23,18 +22,12 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { Resolution } from '@/types/app'
type PublishedModelConfig = ModelConfig & {
resetAppConfig?: () => void
}
type Props = Omit<AppPublisherProps, 'onPublish'> & {
onPublish?: (
params?: AppPublisherPublishParams,
features?: Features,
) => Promise<unknown> | unknown
publishedConfig: {
modelConfig: PublishedModelConfig
}
publishedConfig: ConfigurationPublishConfig
resetAppConfig?: () => void
}
@@ -43,6 +36,7 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
const features = useFeatures((s) => s.features)
const featuresStore = useFeaturesStore()
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
const { resetAppConfig } = props
const {
more_like_this,
opening_statement,
@@ -54,10 +48,9 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
retriever_resource,
annotation_reply,
file_upload,
resetAppConfig,
} = props.publishedConfig.modelConfig
const handleConfirm = useCallback(() => {
const handleConfirm = () => {
resetAppConfig?.()
const { features, setFeatures } = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
@@ -90,9 +83,9 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
number_limits: file_upload?.number_limits || file_upload?.image?.number_limits || 3,
} as FileUpload
})
setFeatures(newFeatures)
setFeatures(newFeatures, { silent: true })
setRestoreConfirmOpen(false)
}, [featuresStore, props])
}
const handlePublish = useCallback(
(params?: AppPublisherPublishParams) => {
+106 -87
View File
@@ -12,6 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
import { toast } from '@langgenius/dify-ui/toast'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { use, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { WorkflowLaunchDialog } from '@/app/components/app/overview/app-card-sections'
@@ -20,10 +21,8 @@ import {
createWorkflowLaunchInitialValues,
isWorkflowLaunchInputSupported,
} from '@/app/components/app/overview/app-card-utils'
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 { buildInstalledAppPath } from '@/app/components/explore/installed-app/routes'
import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions'
import { WorkflowToolDrawer } from '@/app/components/tools/workflow-tool'
import { useConfigureButton } from '@/app/components/tools/workflow-tool/hooks/use-configure-button'
@@ -31,18 +30,20 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
import { WorkflowContext } from '@/app/components/workflow/context'
import { appDefaultIconBackground } from '@/config'
import { isCurrentWorkspaceEditorAtom } from '@/context/workspace-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
import { fetchAppDetail, publishToCreatorsPlatform } from '@/service/apps'
import { fetchInstalledAppList } from '@/service/explore'
import { appDetailQueryKeyPrefix } from '@/service/use-apps'
import { useInvalidateAppWorkflow } from '@/service/use-workflow'
import { fetchPublishedWorkflow } from '@/service/workflow'
import {
appWorkflowQueryOptions,
useAppWorkflow,
useInvalidateAppWorkflow,
useUpdateWorkflow,
} from '@/service/use-workflow'
import { AppModeEnum } from '@/types/app'
import { basePath } from '@/utils/var'
import AccessControl from '../app-access-control'
import { APP_PUBLISH_HOTKEY } from './hotkeys'
import {
@@ -50,12 +51,12 @@ import {
PublisherActionsSection,
PublisherSummarySection,
} from './sections'
import SuggestedAction from './suggested-action'
import {
getDisabledFunctionTooltip,
getPublisherAppUrl,
isPublisherAccessConfigured,
} from './utils'
import VersionInfoModal from './version-info-modal'
export type AppPublisherProps = {
disabled?: boolean
@@ -63,6 +64,10 @@ export type AppPublisherProps = {
publishedAt?: number
/** only needed in workflow / chatflow mode */
draftUpdatedAt?: number
/** Current persisted workflow draft hash, used to compare with the published workflow. */
draftHash?: string
/** Non-workflow editors should pass their local dirty state. */
hasUnpublishedChanges?: boolean
debugWithMultipleModel?: boolean
multipleModelConfigs?: ModelAndParameter[]
/** modelAndParameter is passed when debugWithMultipleModel is true */
@@ -94,6 +99,8 @@ export function AppPublisher({
publishDisabled = false,
publishedAt,
draftUpdatedAt,
draftHash,
hasUnpublishedChanges,
debugWithMultipleModel = false,
multipleModelConfigs = [],
onPublish,
@@ -112,32 +119,44 @@ export function AppPublisher({
}: AppPublisherProps) {
const { t } = useTranslation()
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 [workflowLaunchDialogOpen, setWorkflowLaunchDialogOpen] = useState(false)
const [workflowLaunchTargetUrl, setWorkflowLaunchTargetUrl] = useState('')
const [workflowLaunchValues, setWorkflowLaunchValues] = useState<
Record<string, WorkflowLaunchInputValue>
>({})
const [publishingToMarketplace, setPublishingToMarketplace] = useState(false)
const [editVersionInfoOpen, setEditVersionInfoOpen] = useState(false)
const workflowStore = use(WorkflowContext)
const appDetail = useAppStore((state) => state.appDetail)
const setAppDetail = useAppStore((state) => state.setAppDetail)
const canManageTools = useCanManageTools()
const isCurrentWorkspaceEditor = useAtomValue(isCurrentWorkspaceEditorAtom)
const queryClient = useQueryClient()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { formatTimeFromNow } = useFormatTimeFromNow()
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode })
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(
appDetail?.mode || AppModeEnum.CHAT,
const appMode = appDetail?.mode
const isWorkflowApp = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
const isChatApp =
appMode === AppModeEnum.CHAT ||
appMode === AppModeEnum.AGENT_CHAT ||
appMode === AppModeEnum.COMPLETION
const { data: publishedWorkflow, isSuccess: isPublishedWorkflowSuccess } = useAppWorkflow(
isWorkflowApp ? (appDetail?.id ?? '') : '',
)
const currentPublishedAt =
isWorkflowApp && isPublishedWorkflowSuccess
? publishedWorkflow?.created_at
? publishedWorkflow.created_at * 1000
: undefined
: publishedAt
const { mutate: updateWorkflow } = useUpdateWorkflow()
const hiddenLaunchVariables: WorkflowHiddenStartVariable[] = (inputs ?? []).filter(
(input) => input.hide === true,
)
@@ -166,7 +185,6 @@ export function AppPublisher({
appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS,
)
const invalidateAppWorkflow = useInvalidateAppWorkflow()
const openAsyncWindow = useAsyncWindowOpen()
const isAppAccessSet = isPublisherAccessConfigured(appDetail, appAccessSubjects)
@@ -176,10 +194,10 @@ export function AppPublisher({
appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS &&
!userCanAccessApp?.result,
)
const disabledFunctionButton = !publishedAt || missingStartNode || noAccessPermission
const disabledFunctionButton = !currentPublishedAt || missingStartNode || noAccessPermission
const disabledFunctionTooltip = getDisabledFunctionTooltip({
t,
publishedAt,
publishedAt: currentPublishedAt,
missingStartNode,
noAccessPermission,
})
@@ -187,7 +205,6 @@ export function AppPublisher({
async function handlePublish(params?: ModelAndParameter | PublishWorkflowParams) {
try {
await onPublish?.(params)
setPublished(true)
const appId = appDetail?.id
const socket = appId ? webSocketClient.getSocket(appId) : null
@@ -214,7 +231,6 @@ export function AppPublisher({
})
} catch (error) {
console.warn('[app-publisher] publish failed', error)
setPublished(false)
}
}
@@ -233,25 +249,6 @@ export function AppPublisher({
onToggle?.(nextOpen)
setOpen(nextOpen)
if (nextOpen) setPublished(false)
}
async function handleOpenInExplore() {
await openAsyncWindow(
async () => {
if (!appDetail?.id) throw new Error('App not found')
const { installed_apps } = await fetchInstalledAppList(appDetail.id)
if (installed_apps?.length > 0)
return `${basePath}${buildInstalledAppPath(installed_apps[0]!.id)}`
throw new Error(t(($) => $.notPublishedYet, { ns: 'app' }))
},
{
onError: (err) => {
toast.error(`${err.message || err}`)
},
},
)
}
async function handleAccessControlUpdate() {
@@ -304,9 +301,50 @@ export function AppPublisher({
}
}
const hasPublishedVersion = Boolean(currentPublishedAt)
const workflowHasUnpublishedChanges =
!currentPublishedAt ||
!draftHash ||
!publishedWorkflow?.hash ||
draftHash !== publishedWorkflow.hash
const resolvedHasUnpublishedChanges =
hasUnpublishedChanges ?? (isWorkflowApp ? workflowHasUnpublishedChanges : !currentPublishedAt)
function handleOpenVersionInfo() {
if (!publishedWorkflow) return
handleOpenChange(false)
setEditVersionInfoOpen(true)
}
function handleUpdateVersionInfo(params: { id?: string; title: string; releaseNotes: string }) {
if (!appDetail?.id || !params.id) return
updateWorkflow(
{
url: `/apps/${appDetail.id}/workflows/${params.id}`,
title: params.title,
releaseNotes: params.releaseNotes,
},
{
onSuccess: () => {
toast.success(t(($) => $['versionHistory.action.updateSuccess'], { ns: 'workflow' }))
invalidateAppWorkflow(appDetail.id)
},
onError: () => {
toast.error(t(($) => $['versionHistory.action.updateFailure'], { ns: 'workflow' }))
},
onSettled: () => {
setEditVersionInfoOpen(false)
},
},
)
}
useHotkey(APP_PUBLISH_HOTKEY, (e) => {
if (debugWithMultipleModel) return
e.preventDefault()
if (publishDisabled || published) return
if (publishDisabled || (hasPublishedVersion && !resolvedHasUnpublishedChanges)) return
handlePublish()
})
@@ -317,11 +355,10 @@ export function AppPublisher({
const unsubscribe = collaborationManager.onAppPublishUpdate((update: CollaborationUpdate) => {
const action = typeof update.data.action === 'string' ? update.data.action : undefined
if (action === 'published') {
invalidateAppWorkflow(appId)
fetchPublishedWorkflow(`/apps/${appId}/workflows/publish`)
void queryClient
.fetchQuery(appWorkflowQueryOptions(appId))
.then((publishedWorkflow) => {
if (publishedWorkflow?.created_at)
workflowStore?.getState().setPublishedAt(publishedWorkflow.created_at)
workflowStore?.getState().setPublishedAt(publishedWorkflow?.created_at ?? 0)
})
.catch((error) => {
console.warn('[app-publisher] refresh published workflow failed', error)
@@ -330,9 +367,8 @@ export function AppPublisher({
})
return unsubscribe
}, [appDetail?.id, invalidateAppWorkflow, workflowStore])
}, [appDetail?.id, queryClient, workflowStore])
const hasPublishedVersion = !!publishedAt
const workflowToolVisible =
appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
const workflowToolAvailableForUser = workflowToolAvailable && canManageTools
@@ -353,7 +389,7 @@ export function AppPublisher({
const workflowTool = useConfigureButton({
enabled: workflowToolVisible && canManageTools,
published: workflowToolPublished,
detailNeedUpdate: workflowToolPublished && published,
detailNeedUpdate: workflowToolPublished && !resolvedHasUnpublishedChanges,
workflowAppId: appDetail?.id ?? '',
icon: workflowToolIcon,
name: appDetail?.name ?? '',
@@ -395,20 +431,23 @@ export function AppPublisher({
alignOffset={crossAxisOffset}
popupClassName="border-none bg-transparent shadow-none"
>
<div className="w-[320px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
<div className="w-98 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5">
<PublisherSummarySection
debugWithMultipleModel={debugWithMultipleModel}
draftUpdatedAt={draftUpdatedAt}
formatTimeFromNow={formatTimeFromNow}
handlePublish={handlePublish}
handleRestore={handleRestore}
hasUnpublishedChanges={resolvedHasUnpublishedChanges}
isChatApp={isChatApp}
isWorkflowApp={isWorkflowApp}
multipleModelConfigs={multipleModelConfigs}
onEditVersion={handleOpenVersionInfo}
publishDisabled={publishDisabled}
published={published}
publishedAt={publishedAt}
publishedAt={currentPublishedAt}
startNodeLimitExceeded={startNodeLimitExceeded}
upgradeHighlightStyle={upgradeHighlightStyle}
versionInfo={publishedWorkflow}
/>
<PublisherAccessSection
enabled={systemFeatures.webapp_auth.enabled}
@@ -428,57 +467,29 @@ export function AppPublisher({
appURL={appURL}
disabledFunctionButton={disabledFunctionButton}
disabledFunctionTooltip={disabledFunctionTooltip}
handleEmbed={() => {
setEmbeddingModalOpen(true)
handleOpenChange(false)
}}
handleOpenInExplore={() => {
handleOpenChange(false)
handleOpenInExplore()
}}
handleOpenRunConfig={handleOpenWorkflowLaunchDialog}
handlePublish={handlePublish}
hasHumanInputNode={hasHumanInputNode}
hasTriggerNode={hasTriggerNode}
missingStartNode={missingStartNode}
published={published}
publishedAt={publishedAt}
showBatchRunConfig={
hiddenLaunchVariables.length > 0 &&
(appDetail?.mode === AppModeEnum.WORKFLOW ||
appDetail?.mode === AppModeEnum.COMPLETION)
marketplaceActionDisabled={!currentPublishedAt}
publishedAt={currentPublishedAt}
publishingToMarketplace={publishingToMarketplace}
showDeployAction={
appDetail?.mode === AppModeEnum.WORKFLOW &&
isCurrentWorkspaceEditor &&
systemFeatures.enable_app_deploy
}
showMarketplaceAction={systemFeatures.enable_creators_platform}
showRunConfig={hiddenLaunchVariables.length > 0}
toolPublished={toolPublished}
toolPublished={workflowToolPublished}
workflowToolAvailable={workflowToolAvailableForUser}
workflowToolIsLoading={workflowTool.isLoading}
workflowToolOutdated={workflowTool.outdated}
workflowToolMessage={workflowToolMessage}
workflowToolOutdated={workflowTool.outdated}
onConfigureWorkflowTool={openWorkflowToolDrawer}
onPublishToMarketplace={handlePublishToMarketplace}
/>
{systemFeatures.enable_creators_platform && (
<div className="border-t border-divider-subtle p-4">
<SuggestedAction
icon={<span className="i-ri-store-line size-4" />}
disabled={!publishedAt || publishingToMarketplace}
onClick={handlePublishToMarketplace}
>
{publishingToMarketplace
? t(($) => $['common.publishingToMarketplace'], { ns: 'workflow' })
: t(($) => $['common.publishToMarketplace'], { ns: 'workflow' })}
</SuggestedAction>
</div>
)}
</div>
</PopoverContent>
<EmbeddedModal
siteInfo={appDetail?.site}
isShow={embeddingModalOpen}
onClose={() => setEmbeddingModalOpen(false)}
appBaseUrl={appBaseURL}
accessToken={accessToken}
hiddenInputs={hiddenLaunchVariables}
/>
{showAppAccessControl && (
<AccessControl
app={appDetail!}
@@ -499,6 +510,14 @@ export function AppPublisher({
onSubmit={handleWorkflowLaunchConfirm}
/>
</Popover>
{editVersionInfoOpen && (
<VersionInfoModal
isOpen={editVersionInfoOpen}
versionInfo={publishedWorkflow ?? undefined}
onClose={() => setEditVersionInfoOpen(false)}
onPublish={handleUpdateVersionInfo}
/>
)}
{workflowToolDrawerOpen && canManageTools && (
<WorkflowToolDrawer
isAdd={!workflowToolPublished}
@@ -19,11 +19,13 @@ import { useProviderContext } from '@/context/provider-context'
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'
type PublishWithMultipleModelProps = {
disabled?: boolean
multipleModelConfigs: ModelAndParameter[]
// textGenerationModelList?: Model[]
onSelect: (v: ModelAndParameter) => void
}
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
disabled = false,
multipleModelConfigs,
// textGenerationModelList = [],
onSelect,
@@ -55,13 +57,13 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
}
})
const triggerDisabled = disabled || !validModelConfigs.length
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
disabled={!validModelConfigs.length}
render={
<Button variant="primary" disabled={!validModelConfigs.length} className="mt-3 w-full" />
}
disabled={triggerDisabled}
render={<Button variant="primary" disabled={triggerDisabled} className="w-full" />}
>
<>
{t(($) => $['operation.applyConfig'], { ns: 'appDebug' })}
@@ -69,12 +71,12 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
</>
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="w-[288px] p-1">
<div className="flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary">
<div className="flex h-5.5 items-center px-3 text-xs font-medium text-text-tertiary">
{t(($) => $.publishAs, { ns: 'appDebug' })}
</div>
{validModelConfigs.map((item, index) => (
<DropdownMenuItem key={item.id} className="gap-0 px-3" onClick={() => onSelect(item)}>
<span className="min-w-[18px] italic">#{index + 1}</span>
<span className="min-w-4.5 italic">#{index + 1}</span>
<ModelIcon modelName={item.model} provider={item.providerItem} className="ml-2" />
<div
className="ml-1 truncate text-text-secondary"
+286 -185
View File
@@ -1,21 +1,22 @@
import type { CSSProperties, ReactNode } from 'react'
import type { CSSProperties } from 'react'
import type { ModelAndParameter } from '../configuration/debug/types'
import type { AppPublisherProps } from './index'
import type { PublishWorkflowParams } from '@/types/workflow'
import type { PublishWorkflowParams, VersionHistory } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { formatForDisplay } from '@tanstack/react-hotkeys'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
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 { AppModeEnum } from '@/types/app'
import ActionTooltip from './action-tooltip'
import { APP_PUBLISH_HOTKEY } from './hotkeys'
import PublishWithMultipleModel from './publish-with-multiple-model'
import SuggestedAction from './suggested-action'
import { ACCESS_MODE_MAP } from './utils'
import WorkflowToolAction from './workflow-tool-action'
type SummarySectionProps = Pick<
AppPublisherProps,
@@ -29,9 +30,12 @@ type SummarySectionProps = Pick<
formatTimeFromNow: (value: number) => string
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
handleRestore: () => Promise<void>
hasUnpublishedChanges?: boolean
isChatApp: boolean
published: boolean
isWorkflowApp?: boolean
onEditVersion?: () => void
upgradeHighlightStyle: CSSProperties
versionInfo?: VersionHistory | null
}
type AccessSectionProps = {
@@ -44,12 +48,7 @@ type AccessSectionProps = {
type ActionsSectionProps = Pick<
AppPublisherProps,
| 'hasHumanInputNode'
| 'hasTriggerNode'
| 'missingStartNode'
| 'toolPublished'
| 'publishedAt'
| 'workflowToolAvailable'
'hasHumanInputNode' | 'hasTriggerNode' | 'publishedAt' | 'toolPublished' | 'workflowToolAvailable'
> & {
appDetail:
| {
@@ -66,17 +65,17 @@ type ActionsSectionProps = Pick<
appURL: string
disabledFunctionButton: boolean
disabledFunctionTooltip?: string
handleEmbed: () => void
handleOpenInExplore: () => void
handleOpenRunConfig?: (url: string) => void
handlePublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
published: boolean
showBatchRunConfig?: boolean
marketplaceActionDisabled?: boolean
publishingToMarketplace?: boolean
showDeployAction?: boolean
showMarketplaceAction?: boolean
showRunConfig?: boolean
workflowToolIsLoading: boolean
workflowToolOutdated: boolean
workflowToolMessage?: string
workflowToolOutdated?: boolean
onConfigureWorkflowTool: () => void
onPublishToMarketplace?: () => void
}
export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MAP }) => {
@@ -98,97 +97,227 @@ export const AccessModeDisplay = ({ mode }: { mode?: keyof typeof ACCESS_MODE_MA
)
}
const PublisherTimelineMarker = ({ position }: { position: 'top' | 'bottom' }) => (
<span
className={cn(
'relative flex w-4 shrink-0 items-start p-1',
position === 'top' ? 'self-stretch' : 'h-4',
)}
>
<span
aria-hidden
className="relative z-1 size-2 rounded-full border-2 border-text-quaternary bg-components-panel-bg"
/>
<span
aria-hidden
className={cn(
'absolute left-1/2 w-0.5 -translate-x-1/2 bg-divider-subtle',
position === 'top' ? 'top-3.5 -bottom-3.5' : '-top-3.5 h-4',
)}
/>
</span>
)
export const PublisherSummarySection = ({
debugWithMultipleModel = false,
draftUpdatedAt,
formatTimeFromNow,
handlePublish,
handleRestore,
hasUnpublishedChanges,
isChatApp,
isWorkflowApp = false,
multipleModelConfigs = [],
onEditVersion,
publishDisabled = false,
published,
publishedAt,
startNodeLimitExceeded = false,
upgradeHighlightStyle,
versionInfo,
}: SummarySectionProps) => {
const { t } = useTranslation()
const hasPublishedVersion = Boolean(publishedAt)
const resolvedHasUnpublishedChanges = hasUnpublishedChanges ?? !hasPublishedVersion
const publishedTimestamp =
publishedAt || (versionInfo?.created_at ? versionInfo.created_at * 1000 : undefined)
const publisherName = versionInfo?.created_by.name
const markedName = versionInfo?.marked_name
const markedComment = versionInfo?.marked_comment
const publishButtonDisabled =
publishDisabled || (hasPublishedVersion && !resolvedHasUnpublishedChanges)
const publishButtonLabel = !hasPublishedVersion
? t(($) => $['common.publish'], { ns: 'workflow' })
: resolvedHasUnpublishedChanges
? t(($) => $['common.publishUpdate'], { ns: 'workflow' })
: t(($) => $['common.published'], { ns: 'workflow' })
return (
<div className="p-4 pt-3">
<div className="flex h-6 items-center system-xs-medium-uppercase text-text-tertiary">
{publishedAt
? t(($) => $['common.latestPublished'], { ns: 'workflow' })
: t(($) => $['common.currentDraftUnpublished'], { ns: 'workflow' })}
</div>
{publishedAt ? (
<div className="flex items-center justify-between">
<div className="flex items-center system-sm-medium text-text-secondary">
{t(($) => $['common.publishedAt'], { ns: 'workflow' })} {formatTimeFromNow(publishedAt)}
<div className="flex flex-col gap-3 p-4">
<div className="flex items-start gap-1 px-1 py-0.5">
<PublisherTimelineMarker position="top" />
{!hasPublishedVersion ? (
<p className="min-w-0 flex-1 system-xs-regular text-text-tertiary">
{t(($) => $['common.notPublishedYet'], { ns: 'workflow' })}
</p>
) : isWorkflowApp ? (
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex min-h-4 min-w-0 items-center gap-1">
<span className="truncate system-sm-semibold text-text-secondary">
{markedName || versionInfo?.version}
</span>
<span aria-hidden className="system-xs-regular text-text-tertiary">
·
</span>
{markedName ? (
<button
type="button"
className="flex size-4 shrink-0 items-center justify-center rounded text-text-tertiary outline-hidden hover:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
aria-label={t(($) => $['versionHistory.editVersionInfo'], { ns: 'workflow' })}
disabled={!versionInfo || !onEditVersion}
onClick={onEditVersion}
>
<span aria-hidden className="i-ri-edit-line size-3.5" />
</button>
) : (
<button
type="button"
className="flex min-w-0 items-center gap-1 rounded text-text-accent outline-hidden hover:text-text-accent-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-wait"
disabled={!versionInfo || !onEditVersion}
onClick={onEditVersion}
>
<span aria-hidden className="i-ri-edit-line size-3.5 shrink-0" />
<span className="truncate system-xs-medium">
{t(($) => $['versionHistory.nameIt'], { ns: 'workflow' })}
</span>
</button>
)}
</div>
{markedComment && (
<>
<p className="line-clamp-3 system-xs-regular wrap-break-word text-text-tertiary">
{markedComment}
</p>
<span aria-hidden className="my-1 h-px w-4 bg-divider-regular" />
</>
)}
{publishedTimestamp && (
<p className="system-xs-regular text-text-tertiary">
{publisherName
? t(($) => $['common.publishedBy'], {
ns: 'workflow',
time: formatTimeFromNow(publishedTimestamp),
author: publisherName,
})
: `${t(($) => $['common.publishedAt'], { ns: 'workflow' })} ${formatTimeFromNow(publishedTimestamp)}`}
</p>
)}
</div>
{isChatApp && (
) : (
<div className="flex min-w-0 flex-1 items-start justify-between gap-2">
<div className="flex min-w-0 flex-col">
<p className="truncate system-sm-semibold text-text-secondary">
{t(($) => $['common.latestPublished'], { ns: 'workflow' })}
</p>
{publishedTimestamp && (
<p className="truncate system-xs-regular text-text-tertiary">
{publisherName
? t(($) => $['common.publishedBy'], {
ns: 'workflow',
time: formatTimeFromNow(publishedTimestamp),
author: publisherName,
})
: `${t(($) => $['common.publishedAt'], { ns: 'workflow' })} ${formatTimeFromNow(publishedTimestamp)}`}
</p>
)}
</div>
{isChatApp && (
<Button
variant="secondary"
size="small"
className="h-6 shrink-0 gap-1"
onClick={handleRestore}
disabled={!resolvedHasUnpublishedChanges}
>
<span aria-hidden className="i-ri-reset-left-line size-3.5" />
{t(($) => $['common.restore'], { ns: 'workflow' })}
</Button>
)}
</div>
)}
</div>
<div className="flex w-full flex-col">
{debugWithMultipleModel ? (
<PublishWithMultipleModel
disabled={publishDisabled}
multipleModelConfigs={multipleModelConfigs}
onSelect={(item) => handlePublish(item)}
/>
) : (
<>
<Button
variant="secondary-accent"
size="small"
onClick={handleRestore}
disabled={published}
variant="primary"
className="w-full"
onClick={() => handlePublish()}
disabled={publishButtonDisabled}
>
{t(($) => $['common.restore'], { ns: 'workflow' })}
{publishButtonDisabled ? (
publishButtonLabel
) : (
<span className="flex items-center gap-1">
<span>{publishButtonLabel}</span>
<KbdGroup aria-hidden>
{APP_PUBLISH_HOTKEY.split('+').map((key) => (
<Kbd key={key} color="white">
{formatForDisplay(key)}
</Kbd>
))}
</KbdGroup>
</span>
)}
</Button>
)}
</div>
) : (
<div className="flex items-center system-sm-medium text-text-secondary">
{t(($) => $['common.autoSaved'], { ns: 'workflow' })} ·
{Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
</div>
)}
{debugWithMultipleModel ? (
<PublishWithMultipleModel
multipleModelConfigs={multipleModelConfigs}
onSelect={(item) => handlePublish(item)}
/>
) : (
<>
<Button
variant="primary"
className="mt-3 w-full"
onClick={() => handlePublish()}
disabled={publishDisabled || published}
>
{published ? (
t(($) => $['common.published'], { ns: 'workflow' })
) : (
<div className="flex gap-1">
<span>{t(($) => $['common.publishUpdate'], { ns: 'workflow' })}</span>
<KbdGroup>
{APP_PUBLISH_HOTKEY.split('+').map((key) => (
<Kbd key={key} color="white">
{formatForDisplay(key)}
</Kbd>
))}
</KbdGroup>
{startNodeLimitExceeded && (
<div className="mt-3 flex flex-col items-stretch">
<p
className="text-sm/5 font-semibold text-transparent"
style={upgradeHighlightStyle}
>
<span className="block">
{t(($) => $['publishLimit.startNodeTitlePrefix'], { ns: 'workflow' })}
</span>
<span className="block">
{t(($) => $['publishLimit.startNodeTitleSuffix'], { ns: 'workflow' })}
</span>
</p>
<p className="mt-1 text-xs/4 text-text-secondary">
{t(($) => $['publishLimit.startNodeDesc'], { ns: 'workflow' })}
</p>
<UpgradeBtn isShort className="mt-2.25 mb-3 h-8 w-23.25 self-start" />
</div>
)}
</Button>
{startNodeLimitExceeded && (
<div className="mt-3 flex flex-col items-stretch">
<p className="text-sm/5 font-semibold text-transparent" style={upgradeHighlightStyle}>
<span className="block">
{t(($) => $['publishLimit.startNodeTitlePrefix'], { ns: 'workflow' })}
</span>
<span className="block">
{t(($) => $['publishLimit.startNodeTitleSuffix'], { ns: 'workflow' })}
</span>
</p>
<p className="mt-1 text-xs/4 text-text-secondary">
{t(($) => $['publishLimit.startNodeDesc'], { ns: 'workflow' })}
</p>
<UpgradeBtn isShort className="mt-[9px] mb-[12px] h-[32px] w-[93px] self-start" />
</div>
</>
)}
</div>
<div className="flex items-center gap-1 py-0.5 pr-0.5 pl-1">
<PublisherTimelineMarker position="bottom" />
<p className="min-w-0 flex-1 truncate system-xs-regular text-text-tertiary">
{resolvedHasUnpublishedChanges ? (
<>
{t(($) => $['common.unpublishedChanges'], { ns: 'workflow' })}
{isWorkflowApp && Boolean(draftUpdatedAt) && (
<>
{' · '}
{t(($) => $['common.savedAt'], {
ns: 'workflow',
time: formatTimeFromNow(draftUpdatedAt!),
})}
</>
)}
</>
) : (
t(($) => $['common.noChanges'], { ns: 'workflow' })
)}
</>
)}
</p>
</div>
</div>
)
}
@@ -219,8 +348,9 @@ export const PublisherAccessSection = ({
{t(($) => $['publishApp.title'], { ns: 'app' })}
</p>
</div>
<div
className="flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pr-2 pl-2.5 hover:bg-primary-50 hover:text-text-accent"
<button
type="button"
className="flex h-8 w-full cursor-pointer items-center gap-x-0.5 rounded-lg border-0 bg-components-input-bg-normal py-1 pr-2 pl-2.5 text-left outline-hidden hover:bg-primary-50 hover:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={onClick}
>
<div className="flex grow items-center gap-x-1.5 overflow-hidden pr-1">
@@ -234,7 +364,7 @@ export const PublisherAccessSection = ({
<div className="flex size-4 shrink-0 items-center justify-center">
<span className="i-ri-arrow-right-s-line size-4 text-text-quaternary" />
</div>
</div>
</button>
{!isAppAccessSet && (
<p className="mt-1 system-xs-regular text-text-warning">
{t(($) => $['publishApp.notSetDesc'], { ns: 'app' })}
@@ -246,143 +376,114 @@ export const PublisherAccessSection = ({
)
}
const ActionTooltip = ({
disabled,
tooltip,
children,
}: {
disabled: boolean
tooltip?: ReactNode
children: ReactNode
}) => {
if (!disabled || !tooltip) return <>{children}</>
return (
<Tooltip>
<TooltipTrigger render={<div className="flex">{children}</div>} />
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
)
}
export const PublisherActionsSection = ({
appDetail,
appURL,
disabledFunctionButton,
disabledFunctionTooltip,
handleEmbed,
handleOpenInExplore,
handleOpenRunConfig,
hasHumanInputNode = false,
hasTriggerNode = false,
missingStartNode = false,
marketplaceActionDisabled = false,
publishedAt,
showBatchRunConfig = false,
publishingToMarketplace = false,
showDeployAction = false,
showMarketplaceAction = false,
showRunConfig = false,
toolPublished,
toolPublished = false,
workflowToolAvailable = true,
workflowToolIsLoading,
workflowToolOutdated,
workflowToolMessage,
workflowToolOutdated = false,
onConfigureWorkflowTool,
onPublishToMarketplace,
}: ActionsSectionProps) => {
const { t } = useTranslation()
if (hasTriggerNode) return null
const workflowToolDisabled = !publishedAt || !workflowToolAvailable
const appId = appDetail?.id
const hasPublishedVersion = Boolean(publishedAt)
const showOpenWebApp = !hasTriggerNode
const showDeploy = Boolean(showDeployAction && appId)
const showWorkflowTool =
appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && !hasTriggerNode
const navigationDisabled = !hasPublishedVersion || !appId
const workflowToolDisabled =
!hasPublishedVersion || !workflowToolAvailable || (toolPublished && workflowToolIsLoading)
return (
<div className="flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3">
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
<SuggestedAction
className="flex-1"
disabled={disabledFunctionButton}
link={appURL}
icon={<span className="i-ri-play-circle-line size-4" />}
actionButton={
showRunConfig
? {
ariaLabel: t(($) => $['operation.config'], { ns: 'common' }),
icon: <span className="i-ri-settings-2-line size-4" />,
onClick: () => handleOpenRunConfig?.(appURL),
}
: undefined
}
>
{t(($) => $['common.runApp'], { ns: 'workflow' })}
</SuggestedAction>
</ActionTooltip>
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION ? (
<div className="flex flex-col border-t-[0.5px] border-t-divider-regular p-3">
{showOpenWebApp && (
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
<SuggestedAction
className="flex-1"
disabled={disabledFunctionButton}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<span className="i-ri-play-list-2-line size-4" />}
description={
disabledFunctionButton && disabledFunctionTooltip
? disabledFunctionTooltip
: t(($) => $['common.openWebAppDescription'], { ns: 'workflow' })
}
external
focusableWhenDisabled={Boolean(disabledFunctionTooltip)}
link={appURL}
icon={<span className="i-ri-planet-line size-4" />}
actionButton={
showBatchRunConfig
showRunConfig && handleOpenRunConfig
? {
ariaLabel: t(($) => $['operation.config'], { ns: 'common' }),
icon: <span className="i-ri-settings-2-line size-4" />,
onClick: () =>
handleOpenRunConfig?.(
`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`,
),
onClick: () => handleOpenRunConfig(appURL),
}
: undefined
}
>
{t(($) => $['common.batchRunApp'], { ns: 'workflow' })}
{t(($) => $['common.openWebApp'], { ns: 'workflow' })}
</SuggestedAction>
</ActionTooltip>
) : (
)}
<SuggestedAction
disabled={navigationDisabled}
description={t(($) => $['common.accessPointDescription'], { ns: 'workflow' })}
link={appId ? `/app/${appId}/access-point` : undefined}
icon={<span className="i-custom-vender-agent-v2-access-point size-4" />}
>
{t(($) => $['appMenus.accessPoint'], { ns: 'common' })}
</SuggestedAction>
{showDeploy && (
<SuggestedAction
onClick={handleEmbed}
disabled={!publishedAt}
icon={<span className="i-custom-vender-line-development-code-browser size-4" />}
disabled={navigationDisabled}
description={t(($) => $['common.deployDescription'], { ns: 'workflow' })}
link={`/app/${appId}/deploy`}
icon={<span className="i-ri-instance-line size-4" />}
>
{t(($) => $['common.embedIntoSite'], { ns: 'workflow' })}
{t(($) => $['appMenus.deploy'], { ns: 'common' })}
</SuggestedAction>
)}
<ActionTooltip disabled={disabledFunctionButton} tooltip={disabledFunctionTooltip}>
{showMarketplaceAction && (
<SuggestedAction
className="flex-1"
onClick={() => {
if (publishedAt) handleOpenInExplore()
}}
disabled={disabledFunctionButton}
icon={<span className="i-ri-planet-line size-4" />}
disabled={marketplaceActionDisabled || publishingToMarketplace || !onPublishToMarketplace}
description={t(($) => $['common.publishToMarketplaceDescription'], {
ns: 'workflow',
})}
icon={<span className="i-ri-store-2-line size-4" />}
onClick={onPublishToMarketplace}
>
{t(($) => $['common.openInExplore'], { ns: 'workflow' })}
{publishingToMarketplace
? t(($) => $['common.publishingToMarketplace'], { ns: 'workflow' })
: t(($) => $['common.publishToMarketplace'], { ns: 'workflow' })}
</SuggestedAction>
</ActionTooltip>
<ActionTooltip
disabled={!publishedAt || missingStartNode}
tooltip={
!publishedAt
? t(($) => $.notPublishedYet, { ns: 'app' })
: t(($) => $.noUserInputNode, { ns: 'app' })
}
>
<SuggestedAction
className="flex-1"
disabled={!publishedAt || missingStartNode}
link="./develop"
icon={<span className="i-ri-terminal-box-line size-4" />}
>
{t(($) => $['common.accessAPIReference'], { ns: 'workflow' })}
</SuggestedAction>
</ActionTooltip>
{appDetail?.mode === AppModeEnum.WORKFLOW && !hasHumanInputNode && (
<WorkflowToolConfigureButton
disabled={workflowToolDisabled}
published={!!toolPublished}
isLoading={workflowToolIsLoading}
outdated={workflowToolOutdated}
onConfigure={onConfigureWorkflowTool}
disabledReason={workflowToolMessage}
/>
)}
{showWorkflowTool && (
<>
<div aria-hidden className="m-1 h-px bg-divider-subtle" />
<WorkflowToolAction
disabled={workflowToolDisabled}
isLoading={workflowToolIsLoading}
message={workflowToolMessage}
outdated={workflowToolOutdated}
published={toolPublished}
onConfigure={onConfigureWorkflowTool}
/>
</>
)}
</div>
)
@@ -1,80 +1,166 @@
import type { HTMLProps, PropsWithChildren, MouseEvent as ReactMouseEvent } from 'react'
import type { PropsWithChildren, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowRightUpLine } from '@remixicon/react'
import { useId } from 'react'
import Link from '@/next/link'
type SuggestedActionButton = {
ariaLabel: string
icon: React.ReactNode
icon: ReactNode
onClick: (event: ReactMouseEvent<HTMLButtonElement>) => void
}
type SuggestedActionProps = PropsWithChildren<
HTMLProps<HTMLAnchorElement> & {
icon?: React.ReactNode
link?: string
disabled?: boolean
actionButton?: SuggestedActionButton
}
>
type SuggestedActionProps = PropsWithChildren<{
icon?: ReactNode
link?: string
external?: boolean
disabled?: boolean
focusableWhenDisabled?: boolean
description?: ReactNode
endIcon?: ReactNode
actionButton?: SuggestedActionButton
className?: string
onClick?: () => void
}>
const SuggestedAction = ({
icon,
link,
disabled,
external = false,
disabled = false,
focusableWhenDisabled = false,
description,
endIcon,
actionButton,
children,
className,
onClick,
actionButton,
...props
}: SuggestedActionProps) => {
const handleClick = (event: ReactMouseEvent<HTMLAnchorElement>) => {
if (disabled) {
event.preventDefault()
return
}
onClick?.(event)
}
const handleActionClick = (event: ReactMouseEvent<HTMLButtonElement>) => {
if (disabled) {
event.preventDefault()
return
}
actionButton?.onClick(event)
}
const mainAction = (
<a
href={disabled ? undefined : link}
target="_blank"
rel="noreferrer"
className={cn(
'flex min-w-0 items-center justify-start gap-2 px-2.5 py-2 text-text-secondary transition-colors',
actionButton
? 'flex-1 rounded-l-lg'
: 'rounded-lg bg-background-section-burn not-first:mt-1',
disabled
? 'cursor-not-allowed opacity-30 shadow-xs'
: 'cursor-pointer hover:bg-state-accent-hover hover:text-text-accent',
)}
onClick={handleClick}
{...props}
>
<div className="relative size-4 shrink-0">{icon}</div>
<div className="shrink grow basis-0 system-sm-medium">{children}</div>
<RiArrowRightUpLine className="size-3.5 shrink-0" />
</a>
const id = useId()
const labelId = `${id}-label`
const descriptionId = `${id}-description`
const interactiveClassName = cn(
'group flex h-10 min-w-0 items-center gap-2 border-0 bg-transparent p-1 text-left outline-hidden transition-colors',
actionButton ? 'flex-1 rounded-l-lg' : 'w-full rounded-lg',
disabled
? cn(
'cursor-not-allowed',
!actionButton && 'opacity-30',
focusableWhenDisabled && 'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
)
: 'cursor-pointer hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
!actionButton && className,
)
const content = (
<>
<span
aria-hidden
className={cn(
'flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-components-panel-on-panel-item-bg text-text-secondary shadow-xs transition-colors',
!disabled && 'group-hover:text-text-accent group-focus-visible:text-text-accent',
)}
>
{icon}
</span>
<span className="min-w-0 flex-1">
<span
id={labelId}
className={cn(
'block truncate system-sm-medium text-text-secondary transition-colors',
!disabled && 'group-hover:text-text-primary group-focus-visible:text-text-primary',
)}
>
{children}
</span>
{description && (
<span
id={descriptionId}
className={cn(
'hidden truncate system-xs-regular text-text-tertiary',
!disabled && 'group-hover:block group-focus-visible:block',
)}
>
{description}
</span>
)}
</span>
<span className="flex min-h-4 min-w-4 shrink-0 items-center justify-center text-text-quaternary">
{endIcon ?? <span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />}
</span>
</>
)
const accessibilityProps = {
'aria-labelledby': labelId,
'aria-describedby': description ? descriptionId : undefined,
}
let mainAction: ReactNode
if (disabled) {
mainAction = (
<button
type="button"
disabled={!focusableWhenDisabled}
aria-disabled={focusableWhenDisabled || undefined}
className={interactiveClassName}
onClick={
focusableWhenDisabled
? (event) => {
event.preventDefault()
event.stopPropagation()
}
: undefined
}
onKeyDown={
focusableWhenDisabled
? (event) => {
if (event.key === 'Enter' || event.key === ' ') event.preventDefault()
}
: undefined
}
{...accessibilityProps}
>
{content}
</button>
)
} else if (!link) {
mainAction = (
<button
type="button"
className={interactiveClassName}
onClick={onClick}
{...accessibilityProps}
>
{content}
</button>
)
} else if (external) {
mainAction = (
<a
href={link}
target="_blank"
rel="noreferrer"
className={interactiveClassName}
onClick={onClick}
{...accessibilityProps}
>
{content}
</a>
)
} else {
mainAction = (
<Link href={link} className={interactiveClassName} onClick={onClick} {...accessibilityProps}>
{content}
</Link>
)
}
if (!actionButton) return mainAction
return (
<div
className={cn(
'flex items-stretch rounded-lg bg-background-section-burn not-first:mt-1',
disabled ? 'opacity-30 shadow-xs' : '',
'flex w-full min-w-0 items-stretch rounded-lg',
disabled && 'opacity-30',
className,
)}
>
@@ -84,14 +170,16 @@ const SuggestedAction = ({
aria-label={actionButton.ariaLabel}
disabled={disabled}
className={cn(
'flex w-9 shrink-0 items-center justify-center rounded-r-lg border-l-[0.5px] border-divider-subtle text-text-tertiary transition-colors',
'flex w-9 shrink-0 items-center justify-center rounded-r-lg border-l-[0.5px] border-divider-subtle text-text-tertiary outline-hidden transition-colors',
disabled
? 'cursor-not-allowed'
: 'cursor-pointer hover:bg-state-accent-hover hover:text-text-accent',
: 'cursor-pointer hover:bg-state-base-hover hover:text-text-accent focus-visible:bg-state-base-hover focus-visible:text-text-accent focus-visible:ring-2 focus-visible:ring-state-accent-solid',
)}
onClick={handleActionClick}
onClick={actionButton.onClick}
>
{actionButton.icon}
<span aria-hidden className="flex size-4 items-center justify-center">
{actionButton.icon}
</span>
</button>
</div>
)
@@ -0,0 +1,9 @@
type WorkflowToolDisabledReasonProps = {
message: string
}
const WorkflowToolDisabledReason = ({ message }: WorkflowToolDisabledReasonProps) => {
return <p className="mt-1 px-2.5 pb-2 system-xs-regular text-text-tertiary opacity-30">{message}</p>
}
export default WorkflowToolDisabledReason
@@ -0,0 +1,133 @@
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { buildIntegrationPath } from '@/app/components/integrations/routes'
import Link from '@/next/link'
import SuggestedAction from '../suggested-action'
import WorkflowToolDisabledReason from './disabled-reason'
import WorkflowToolLoadingStatus from './loading-status'
import WorkflowToolSetupStatus from './setup-status'
import WorkflowToolStateLabel from './state-label'
type WorkflowToolActionProps = {
disabled: boolean
isLoading: boolean
message?: string
outdated: boolean
published: boolean
onConfigure: () => void
}
const WorkflowToolAction = ({
disabled,
isLoading,
message,
outdated,
published,
onConfigure,
}: WorkflowToolActionProps) => {
const { t } = useTranslation()
const disabledReason = disabled ? message : undefined
const workflowToolLabel = t(($) => $['common.workflowAsTool'], { ns: 'workflow' })
if (!published || isLoading)
return (
<div
className={cn(
'w-full rounded-lg'
)}
>
<SuggestedAction
className="flex-1"
disabled={disabled}
description={t(($) => $['common.workflowAsToolDescription'], { ns: 'workflow' })}
endIcon={
isLoading ? (
<WorkflowToolLoadingStatus label={t(($) => $.loading, { ns: 'appApi' })} />
) : (
<WorkflowToolSetupStatus
label={t(($) => $['common.configureRequired'], { ns: 'workflow' })}
/>
)
}
icon={<span className="i-ri-hammer-line size-4" />}
onClick={onConfigure}
>
{workflowToolLabel}
</SuggestedAction>
{disabledReason && <WorkflowToolDisabledReason message={disabledReason} />}
</div>
)
const configureLabel = outdated
? t(($) => $['common.workflowAsToolReconfigure'], { ns: 'workflow' })
: t(($) => $['common.configure'], { ns: 'workflow' })
const manageInToolsLabel = t(($) => $['common.manageInTools'], { ns: 'workflow' })
const stateLabel = outdated
? t(($) => $['common.workflowAsToolUpdateNeeded'], { ns: 'workflow' })
: t(($) => $['common.workflowAsToolReady'], { ns: 'workflow' })
return (
<div
className={cn(
'flex w-full flex-col rounded-lg',
disabled && 'opacity-30',
)}
>
<div className="flex items-start gap-2 p-1">
<span
aria-hidden
className="flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-components-panel-on-panel-item-bg text-text-secondary shadow-xs"
>
<span className="i-ri-hammer-line size-4" />
</span>
<div className="min-w-0 flex-1">
<div className="flex min-h-7 items-center gap-2 pt-1 pr-1 pb-1">
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
{workflowToolLabel}
</span>
<WorkflowToolStateLabel label={stateLabel} outdated={outdated} />
</div>
{outdated && (
<p className="pr-4 pb-1 system-xs-regular text-text-warning">
{t(($) => $['common.workflowAsToolTip'], { ns: 'workflow' })}
</p>
)}
<div className="flex items-center gap-1 py-1">
<Button
variant="secondary"
size="small"
disabled={disabled}
className="gap-1 px-1.5"
onClick={onConfigure}
>
<span aria-hidden className="i-ri-equalizer-2-line size-3.5" />
{configureLabel}
</Button>
{disabled ? (
<button
type="button"
disabled
className="flex h-6 cursor-not-allowed items-center gap-1 rounded-md px-2 system-xs-medium text-components-button-tertiary-text-disabled"
>
{manageInToolsLabel}
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
</button>
) : (
<Link
href={buildIntegrationPath('workflow-tool')}
className="flex h-6 items-center gap-1 rounded-md px-2 system-xs-medium text-text-tertiary outline-hidden hover:bg-components-button-tertiary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
{manageInToolsLabel}
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
</Link>
)}
</div>
</div>
</div>
{disabledReason && <WorkflowToolDisabledReason message={disabledReason} />}
</div>
)
}
export default WorkflowToolAction
@@ -0,0 +1,15 @@
type WorkflowToolLoadingStatusProps = {
label: string
}
const WorkflowToolLoadingStatus = ({ label }: WorkflowToolLoadingStatusProps) => {
return (
<span
role="status"
aria-label={label}
className="i-ri-loader-2-line size-4 animate-spin motion-reduce:animate-none"
/>
)
}
export default WorkflowToolLoadingStatus
@@ -0,0 +1,17 @@
type WorkflowToolSetupStatusProps = {
label: string
}
const WorkflowToolSetupStatus = ({ label }: WorkflowToolSetupStatusProps) => {
return (
<span
role="status"
aria-label={label}
className="rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase whitespace-nowrap text-text-tertiary"
>
{label}
</span>
)
}
export default WorkflowToolSetupStatus
@@ -0,0 +1,25 @@
import { cn } from '@langgenius/dify-ui/cn'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
type WorkflowToolStateLabelProps = {
label: string
outdated: boolean
}
const WorkflowToolStateLabel = ({ label, outdated }: WorkflowToolStateLabelProps) => {
return (
<span
role="status"
aria-label={label}
className={cn(
'flex shrink-0 items-center gap-1 system-xs-semibold-uppercase',
outdated ? 'text-util-colors-warning-warning-600' : 'text-util-colors-green-green-600',
)}
>
<StatusDot size="small" status={outdated ? 'warning' : 'success'} />
{label}
</span>
)
}
export default WorkflowToolStateLabel
@@ -214,6 +214,11 @@ const createViewModel = (
publishedConfig: {
modelConfig: createContextValue().modelConfig,
completionParams: {},
promptMode: createContextValue().promptMode,
chatPromptConfig: createContextValue().chatPromptConfig,
completionPromptConfig: createContextValue().completionPromptConfig,
datasetConfigs: createContextValue().datasetConfigs,
externalDataToolsConfig: createContextValue().externalDataToolsConfig,
},
resetAppConfig: vi.fn(),
} as ComponentProps<typeof AppPublisher>,
@@ -248,6 +253,7 @@ const createViewModel = (
onCompletionParamsChange: vi.fn(),
onConfirmUseGPT4: vi.fn(),
onEnableMultipleModelDebug: vi.fn(),
onFeatureStoreChange: vi.fn(),
onFeaturesChange: vi.fn(),
onHideDebugPanel: vi.fn(),
onModelChange: vi.fn(),
@@ -99,6 +99,7 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
onCompletionParamsChange,
onConfirmUseGPT4,
onEnableMultipleModelDebug,
onFeatureStoreChange,
onFeaturesChange,
onHideDebugPanel,
onModelChange,
@@ -128,7 +129,7 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
return (
<ConfigContext.Provider value={contextValue}>
<FeaturesProvider features={featuresData}>
<FeaturesProvider features={featuresData} onFeaturesChange={onFeatureStoreChange}>
<MittProvider>
<div className="flex h-full flex-col">
<div className="relative flex h-[200px] grow pt-14">
@@ -1143,6 +1143,7 @@ describe('Debug', () => {
enabled: true,
}),
}),
{ silent: true },
)
})
@@ -369,7 +369,7 @@ const Debug: FC<IDebug> = ({
enabled: supportedVision,
}
})
setFeatures(newFeatures)
setFeatures(newFeatures, { silent: true })
}
}, [debugWithMultipleModel, featuresStore, mode, multipleModelConfigs, textGenerationModelList])
@@ -24,6 +24,7 @@ describe('useAdvancedPromptConfig', () => {
it('should update the advanced chat prompt and mark user changes', () => {
const handleUserChangedPrompt = vi.fn()
const handlePublishConfigChange = vi.fn()
const { result } = renderHook(() =>
useAdvancedPromptConfig({
appMode: AppModeEnum.CHAT,
@@ -36,6 +37,7 @@ describe('useAdvancedPromptConfig', () => {
completionParams: {},
setCompletionParams: vi.fn(),
setStop: vi.fn(),
onPublishConfigChange: handlePublishConfigChange,
}),
)
@@ -51,6 +53,7 @@ describe('useAdvancedPromptConfig', () => {
])
expect(result.current.hasSetBlockStatus.query).toBe(true)
expect(handleUserChangedPrompt).toHaveBeenCalledTimes(1)
expect(handlePublishConfigChange).toHaveBeenCalledTimes(1)
})
it('should derive simple prompt block status from the pre-prompt', () => {
@@ -1,5 +1,6 @@
/* oxlint-disable typescript/no-explicit-any */
import type { VisionSettings } from '@/types/app'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { withSelectorKey } from '@/test/i18n-mock'
import {
AgentStrategy,
@@ -153,12 +154,20 @@ describe('useConfiguration utils', () => {
is_team_authorization: false,
},
] as any,
datasetConfigs: {
datasets: { datasets: [] },
retrieval_model: RETRIEVE_TYPE.multiWay,
top_k: 4,
} as any,
deletedTools: [{ provider_id: 'tool-1', tool_name: 'search' }],
mode: AppModeEnum.AGENT_CHAT,
nextDataSets: [{ id: 'dataset-1' }] as any,
})
expect(publishedConfig.completionParams).toEqual({ temperature: 0.7 })
expect(publishedConfig.datasetConfigs.top_k).toBe(4)
expect(publishedConfig.promptMode).toBe('simple')
expect(publishedConfig.externalDataToolsConfig).toHaveLength(1)
expect(publishedConfig.modelConfig).toEqual(
expect.objectContaining({
dataSets: [{ id: 'dataset-1' }],
@@ -180,6 +189,76 @@ describe('useConfiguration utils', () => {
)
})
it('should normalize an empty chat prompt config for completion apps', () => {
const publishedConfig = buildPublishedConfig({
backendModelConfig: {
chat_prompt_config: {},
completion_prompt_config: {
prompt: { text: 'completion' },
conversation_histories_role: {
assistant_prefix: '',
user_prefix: '',
},
},
dataset_configs: {
datasets: { datasets: [] },
},
external_data_tools: [],
model: {
provider: 'langgenius/openai/openai',
name: 'gpt-4o',
mode: ModelModeType.completion,
completion_params: {},
},
user_input_form: [],
} as any,
collectionList: [],
datasetConfigs: {
datasets: { datasets: [] },
retrieval_model: RETRIEVE_TYPE.multiWay,
} as any,
mode: AppModeEnum.COMPLETION,
nextDataSets: [],
})
expect(publishedConfig.chatPromptConfig).toEqual(DEFAULT_CHAT_PROMPT_CONFIG)
expect(publishedConfig.modelConfig.chat_prompt_config).toEqual(DEFAULT_CHAT_PROMPT_CONFIG)
})
it('should normalize an empty completion prompt config for chat apps', () => {
const publishedConfig = buildPublishedConfig({
backendModelConfig: {
chat_prompt_config: {
prompt: [{ role: 'system', text: 'chat' }],
},
completion_prompt_config: {},
dataset_configs: {
datasets: { datasets: [] },
},
external_data_tools: [],
model: {
provider: 'langgenius/openai/openai',
name: 'gpt-4o',
mode: ModelModeType.chat,
completion_params: {},
},
user_input_form: [],
} as any,
collectionList: [],
datasetConfigs: {
datasets: { datasets: [] },
retrieval_model: RETRIEVE_TYPE.multiWay,
} as any,
mode: AppModeEnum.CHAT,
nextDataSets: [],
})
expect(publishedConfig.completionPromptConfig).toEqual(DEFAULT_COMPLETION_PROMPT_CONFIG)
expect(publishedConfig.modelConfig.completion_prompt_config).toEqual(
DEFAULT_COMPLETION_PROMPT_CONFIG,
)
})
it('should build dataset configs with reranking defaults', () => {
const datasetConfigs = buildConfigurationDatasetConfigs({
backendModelConfig: {
@@ -665,7 +744,6 @@ describe('useConfiguration utils', () => {
const onPublish = createPublishHandler({
appId: 'app-1',
chatPromptConfig: { prompt: [{ role: 'system', text: 'hi' }] } as any,
citationConfig: { enabled: true } as any,
completionParamsState: { temperature: 0.7 },
completionPromptConfig: {
prompt: { text: 'completion' },
@@ -680,13 +758,13 @@ describe('useConfiguration utils', () => {
datasetConfigs: {
datasets: { datasets: [] },
retrieval_model: RETRIEVE_TYPE.multiWay,
top_k: 7,
} as any,
externalDataToolsConfig: [],
externalDataToolsConfig: [{ enabled: true, variable: 'external' }] as any,
hasSetBlockStatus: {
history: true,
query: true,
},
introduction: 'hello',
isAdvancedMode: true,
isFunctionCall: true,
mode: AppModeEnum.CHAT,
@@ -711,36 +789,40 @@ describe('useConfiguration utils', () => {
workflow_file_upload_limit: 1,
},
} as any,
moreLikeThisConfig: { enabled: true },
promptEmpty: false,
promptMode: 'advanced' as any,
resolvedModelModeType: ModelModeType.chat,
setCanReturnToSimpleMode,
setPublishedConfig,
speechToTextConfig: { enabled: false } as any,
suggestedQuestionsAfterAnswerConfig: { enabled: false } as any,
t,
textToSpeechConfig: { enabled: false, voice: '', language: '' } as any,
})
const result = await onPublish(mockUpdateAppModelConfig, undefined, {
moreLikeThis: { enabled: true },
opening: { enabled: false, opening_statement: '', suggested_questions: [] },
moderation: { enabled: false },
speech2text: { enabled: false },
text2speech: { enabled: false, voice: '', language: '' },
file: {
enabled: false,
image: {
const result = await onPublish(
mockUpdateAppModelConfig,
{
model: 'published-model',
provider: 'published-provider',
parameters: { temperature: 0.2 },
},
{
moreLikeThis: { enabled: true },
opening: { enabled: false, opening_statement: '', suggested_questions: [] },
moderation: { enabled: true },
speech2text: { enabled: false },
text2speech: { enabled: false, voice: '', language: '' },
file: {
enabled: false,
detail: 'low',
number_limits: 1,
transfer_methods: ['local_file'],
},
image: {
enabled: false,
detail: 'low',
number_limits: 1,
transfer_methods: ['local_file'],
},
} as any,
suggested: { enabled: false },
citation: { enabled: true },
} as any,
suggested: { enabled: false },
citation: { enabled: true },
} as any)
)
expect(result).toBe(true)
expect(mockUpdateAppModelConfig).toHaveBeenCalledWith(
@@ -753,7 +835,24 @@ describe('useConfiguration utils', () => {
url: '/apps/app-1/model-config',
}),
)
expect(setPublishedConfig).toHaveBeenCalledTimes(1)
expect(setPublishedConfig).toHaveBeenCalledWith(
expect.objectContaining({
chatPromptConfig: { prompt: [{ role: 'system', text: 'hi' }] },
completionParams: { temperature: 0.2 },
datasetConfigs: expect.objectContaining({ top_k: 7 }),
externalDataToolsConfig: [{ enabled: true, variable: 'external' }],
modelConfig: expect.objectContaining({
file_upload: expect.objectContaining({
image: expect.objectContaining({ detail: 'low' }),
}),
model_id: 'published-model',
opening_statement: '',
provider: 'published-provider',
sensitive_word_avoidance: { enabled: true },
}),
promptMode: 'advanced',
}),
)
expect(mockToastSuccess).toHaveBeenCalledWith('api.success')
expect(setCanReturnToSimpleMode).toHaveBeenCalledWith(false)
})
@@ -764,7 +863,6 @@ describe('useConfiguration utils', () => {
createPublishHandler({
appId: 'app-1',
chatPromptConfig: { prompt: [{ role: 'system', text: 'hi' }] } as any,
citationConfig: { enabled: false } as any,
completionParamsState: { temperature: 0.7 },
completionPromptConfig: {
prompt: { text: 'completion' },
@@ -782,7 +880,6 @@ describe('useConfiguration utils', () => {
history: true,
query: true,
},
introduction: 'hello',
isAdvancedMode: true,
isFunctionCall: false,
mode: AppModeEnum.CHAT,
@@ -801,16 +898,12 @@ describe('useConfiguration utils', () => {
workflow_file_upload_limit: 1,
},
} as any,
moreLikeThisConfig: { enabled: false },
promptEmpty: false,
promptMode: 'advanced' as any,
resolvedModelModeType: ModelModeType.completion,
setCanReturnToSimpleMode: vi.fn(),
setPublishedConfig: vi.fn(),
speechToTextConfig: { enabled: false } as any,
suggestedQuestionsAfterAnswerConfig: { enabled: false } as any,
t,
textToSpeechConfig: { enabled: false, voice: '', language: '' } as any,
...overrides,
})
@@ -1,5 +1,6 @@
/* oxlint-disable typescript/no-explicit-any */
import { act, waitFor } from '@testing-library/react'
import { APP_PUBLISH_DRAFT_CHANGED } from '@/app/components/app/app-publisher/events'
import { updateAppModelConfig } from '@/service/apps'
import { renderHook } from '@/test/console/render'
import { AppModeEnum, ModelModeType } from '@/types/app'
@@ -20,7 +21,14 @@ const mockSetConversationHistoriesRole = vi.fn()
const mockSetChatPromptConfig = vi.fn()
const mockSetCompletionPromptConfig = vi.fn()
const mockSetCurrentAdvancedPrompt = vi.fn()
type MockEvent = {
type: string
instanceId?: string
}
let mockEventSubscription: ((event: MockEvent) => void) | undefined
const mockEventEmit = vi.fn((event: MockEvent) => mockEventSubscription?.(event))
type AdvancedPromptConfigOptions = {
onPublishConfigChange?: () => void
onUserChangedPrompt: () => void
setStop: (stop: string[]) => void
}
@@ -85,6 +93,17 @@ vi.mock('@/context/provider-context', () => ({
}),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: {
emit: mockEventEmit,
useSubscription: (callback: (event: MockEvent) => void) => {
mockEventSubscription = callback
},
},
}),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
@@ -195,6 +214,7 @@ vi.mock('@/utils/completion-params', () => ({
describe('useConfiguration', () => {
beforeEach(() => {
vi.clearAllMocks()
mockEventSubscription = undefined
latestAdvancedPromptConfigOptions = undefined
mockTempStopState = []
mockCurrentModelFeatures = ['vision']
@@ -304,6 +324,236 @@ describe('useConfiguration', () => {
)
})
it('should track configuration events and clear them after publishing', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
act(() => {
result.current.contextValue.setDatasetConfigs({
...result.current.contextValue.datasetConfigs,
top_k: 8,
})
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(true)
expect(mockEventEmit).toHaveBeenCalledWith({
type: APP_PUBLISH_DRAFT_CHANGED,
instanceId: 'app-1',
})
await act(async () => {
await result.current.appPublisherProps.onPublish!(undefined, result.current.featuresData)
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
expect(result.current.appPublisherProps.publishedConfig.datasetConfigs.top_k).toBe(8)
act(() => {
result.current.contextValue.setDatasetConfigs({
...result.current.contextValue.datasetConfigs,
top_k: 10,
})
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(true)
mockEventEmit.mockClear()
mockSetChatPromptConfig.mockClear()
mockSetCompletionPromptConfig.mockClear()
act(() => {
result.current.appPublisherProps.resetAppConfig?.()
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
expect(result.current.contextValue.datasetConfigs.top_k).toBe(8)
expect(mockSetChatPromptConfig).toHaveBeenCalledWith({
prompt: [{ role: 'system', text: 'hi' }],
})
expect(mockSetCompletionPromptConfig).toHaveBeenCalledWith({
prompt: { text: 'completion' },
conversation_histories_role: {
assistant_prefix: 'assistant',
user_prefix: 'user',
},
})
expect(mockEventEmit).not.toHaveBeenCalled()
})
it('should ignore debug formatting events and track feature-store changes', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
act(() => {
mockEventSubscription?.({ type: 'ORCHESTRATE_CHANGED' })
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
act(() => {
result.current.onFeatureStoreChange()
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
act(() => {
result.current.onFeatureStoreChange({ moreLikeThis: { enabled: true } })
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(true)
})
it('should enable multiple-model mode without marking publish config dirty', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
mockEventEmit.mockClear()
act(() => {
result.current.onEnableMultipleModelDebug()
})
expect(mockHandleMultipleModelConfigsChange).toHaveBeenCalledWith(
true,
expect.arrayContaining([
expect.objectContaining({
model: 'gpt-4o',
provider: 'langgenius/openai/openai',
}),
]),
)
expect(mockEventEmit).not.toHaveBeenCalled()
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
})
it('should update multiple-model debug configs without marking publish config dirty', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
mockEventEmit.mockClear()
const modelConfigs = [
{
id: 'model-1',
model: 'gpt-4o',
provider: 'langgenius/openai/openai',
parameters: { temperature: 0.7 },
},
{
id: 'model-2',
model: 'gpt-4.1',
provider: 'langgenius/openai/openai',
parameters: {},
},
{
id: 'model-3',
model: '',
provider: '',
parameters: {},
},
]
act(() => {
result.current.onMultipleModelConfigsChange(true, modelConfigs)
})
expect(mockHandleMultipleModelConfigsChange).toHaveBeenCalledWith(true, modelConfigs)
expect(mockEventEmit).not.toHaveBeenCalled()
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
})
it('should keep multiple-model debug state when restoring published config', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
act(() => {
result.current.onEnableMultipleModelDebug()
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
act(() => {
result.current.contextValue.setDatasetConfigs({
...result.current.contextValue.datasetConfigs,
top_k: 8,
})
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(true)
mockEventEmit.mockClear()
mockHandleMultipleModelConfigsChange.mockClear()
act(() => {
result.current.appPublisherProps.resetAppConfig?.()
})
expect(mockHandleMultipleModelConfigsChange).not.toHaveBeenCalled()
expect(mockEventEmit).not.toHaveBeenCalled()
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
})
it('should silently sync the selected multiple-model config after publishing', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
act(() => {
result.current.contextValue.setDatasetConfigs({
...result.current.contextValue.datasetConfigs,
top_k: 8,
})
})
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(true)
mockEventEmit.mockClear()
await act(async () => {
await result.current.appPublisherProps.onPublish!(
{
id: 'model-2',
model: 'gpt-4.1',
provider: 'langgenius/openai/openai',
parameters: { temperature: 0.2 },
},
result.current.featuresData,
)
})
expect(result.current.contextValue.modelConfig.model_id).toBe('gpt-4.1')
expect(result.current.contextValue.modelConfig.provider).toBe('langgenius/openai/openai')
expect(result.current.contextValue.completionParams).toEqual({ temperature: 0.2 })
expect(result.current.appPublisherProps.publishedConfig.modelConfig.model_id).toBe('gpt-4.1')
expect(result.current.appPublisherProps.hasUnpublishedChanges).toBe(false)
expect(mockEventEmit).not.toHaveBeenCalled()
})
it('should refresh the latest published time after publishing a non-workflow app', async () => {
const { result } = renderHook(() => useConfiguration())
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
})
const previousPublishedAt = result.current.appPublisherProps.publishedAt ?? 0
expect(previousPublishedAt).toBe(1710000000000)
await act(async () => {
await result.current.appPublisherProps.onPublish!(undefined, result.current.featuresData)
})
expect(result.current.appPublisherProps.publishedAt).toBeGreaterThan(previousPublishedAt)
})
it('should block publishing when app release permission is missing', async () => {
mockAppPermissionKeys = [AppACLPermission.ViewLayout]
@@ -7,7 +7,7 @@ import type {
} from '@/models/debug'
import { clone } from 'es-toolkit/object'
import { produce } from 'immer'
import { useState } from 'react'
import { useCallback, useState } from 'react'
import {
checkHasContextBlock,
checkHasHistoryBlock,
@@ -30,6 +30,7 @@ type Param = {
completionParams: FormValue
setCompletionParams: (params: FormValue) => void
setStop: (stop: string[]) => void
onPublishConfigChange?: () => void
}
const useAdvancedPromptConfig = ({
@@ -43,13 +44,28 @@ const useAdvancedPromptConfig = ({
completionParams,
setCompletionParams,
setStop,
onPublishConfigChange,
}: Param) => {
const isAdvancedPrompt = promptMode === PromptMode.advanced
const [chatPromptConfig, setChatPromptConfig] = useState<ChatPromptConfig>(() =>
const [chatPromptConfig, doSetChatPromptConfig] = useState<ChatPromptConfig>(() =>
clone(DEFAULT_CHAT_PROMPT_CONFIG),
)
const [completionPromptConfig, setCompletionPromptConfig] = useState<CompletionPromptConfig>(() =>
clone(DEFAULT_COMPLETION_PROMPT_CONFIG),
const [completionPromptConfig, doSetCompletionPromptConfig] = useState<CompletionPromptConfig>(
() => clone(DEFAULT_COMPLETION_PROMPT_CONFIG),
)
const setChatPromptConfig = useCallback(
(config: ChatPromptConfig) => {
doSetChatPromptConfig(config)
onPublishConfigChange?.()
},
[onPublishConfigChange],
)
const setCompletionPromptConfig = useCallback(
(config: CompletionPromptConfig) => {
doSetCompletionPromptConfig(config)
onPublishConfigChange?.()
},
[onPublishConfigChange],
)
const currentAdvancedPrompt = (() => {
@@ -56,6 +56,28 @@ type DeletedTool = {
tool_name: string
}
const normalizeChatPromptConfig = (
chatPromptConfig: BackendModelConfig['chat_prompt_config'],
): NonNullable<BackendModelConfig['chat_prompt_config']> =>
chatPromptConfig?.prompt?.length ? chatPromptConfig : clone(DEFAULT_CHAT_PROMPT_CONFIG)
const normalizeCompletionPromptConfig = (
completionPromptConfig: BackendModelConfig['completion_prompt_config'],
): NonNullable<BackendModelConfig['completion_prompt_config']> =>
completionPromptConfig?.prompt && completionPromptConfig.conversation_histories_role
? completionPromptConfig
: clone(DEFAULT_COMPLETION_PROMPT_CONFIG)
export type ConfigurationPublishConfig = {
modelConfig: ModelConfig
completionParams: FormValue
promptMode: PromptMode
chatPromptConfig: NonNullable<BackendModelConfig['chat_prompt_config']>
completionPromptConfig: NonNullable<BackendModelConfig['completion_prompt_config']>
datasetConfigs: DatasetConfigs
externalDataToolsConfig: NonNullable<BackendModelConfig['external_data_tools']>
}
const buildPublishedModelConfig = ({
backendModelConfig,
collectionList,
@@ -99,6 +121,11 @@ const buildPublishedModelConfig = ({
backendModelConfig.dataset_query_variable,
),
},
prompt_type: backendModelConfig.prompt_type,
chat_prompt_config: normalizeChatPromptConfig(backendModelConfig.chat_prompt_config),
completion_prompt_config: normalizeCompletionPromptConfig(
backendModelConfig.completion_prompt_config,
),
more_like_this: backendModelConfig.more_like_this ?? { enabled: false },
opening_statement: backendModelConfig.opening_statement,
suggested_questions: backendModelConfig.suggested_questions ?? [],
@@ -158,16 +185,18 @@ const buildPublishedModelConfig = ({
export const buildPublishedConfig = ({
backendModelConfig,
collectionList,
datasetConfigs,
deletedTools,
mode,
nextDataSets,
}: {
backendModelConfig: BackendModelConfig
collectionList: Collection[]
datasetConfigs: DatasetConfigs
deletedTools?: DeletedTool[]
mode: AppModeEnum
nextDataSets: DataSet[]
}) => ({
}): ConfigurationPublishConfig => ({
modelConfig: buildPublishedModelConfig({
backendModelConfig,
collectionList,
@@ -176,6 +205,16 @@ export const buildPublishedConfig = ({
nextDataSets,
}),
completionParams: backendModelConfig.model.completion_params,
promptMode:
backendModelConfig.prompt_type === PromptMode.advanced
? PromptMode.advanced
: PromptMode.simple,
chatPromptConfig: normalizeChatPromptConfig(backendModelConfig.chat_prompt_config),
completionPromptConfig: normalizeCompletionPromptConfig(
backendModelConfig.completion_prompt_config,
),
datasetConfigs,
externalDataToolsConfig: backendModelConfig.external_data_tools ?? [],
})
export const buildConfigurationDatasetConfigs = ({
@@ -369,19 +408,22 @@ export const loadConfigurationState = async ({
nextDataSets = data
}
const datasetConfigs = buildConfigurationDatasetConfigs({
backendModelConfig,
currentRerankModel,
currentRerankProvider,
nextDataSets,
})
return {
annotationConfig: normalizeAnnotationConfig(backendModelConfig.annotation_reply),
backendModelConfig,
canReturnToSimpleMode: nextPromptMode !== PromptMode.advanced,
collectionList,
completionPromptConfig:
backendModelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG),
datasetConfigs: buildConfigurationDatasetConfigs({
backendModelConfig,
currentRerankModel,
currentRerankProvider,
nextDataSets,
}),
completionPromptConfig: normalizeCompletionPromptConfig(
backendModelConfig.completion_prompt_config,
),
datasetConfigs,
externalDataToolsConfig: backendModelConfig.external_data_tools ?? [],
mode: response.mode as AppModeEnum,
moreLikeThisConfig: backendModelConfig.more_like_this || { enabled: false },
@@ -390,6 +432,7 @@ export const loadConfigurationState = async ({
publishedConfig: buildPublishedConfig({
backendModelConfig,
collectionList,
datasetConfigs,
deletedTools: response.deleted_tools,
mode: response.mode as AppModeEnum,
nextDataSets,
@@ -407,11 +450,7 @@ export const loadConfigurationState = async ({
},
visionConfig: backendModelConfig.file_upload?.image,
citationConfig: backendModelConfig.retriever_resource || { enabled: false },
chatPromptConfig:
backendModelConfig.chat_prompt_config &&
backendModelConfig.chat_prompt_config.prompt?.length > 0
? backendModelConfig.chat_prompt_config
: clone(DEFAULT_CHAT_PROMPT_CONFIG),
chatPromptConfig: normalizeChatPromptConfig(backendModelConfig.chat_prompt_config),
introduction: backendModelConfig.opening_statement,
moderationConfig: backendModelConfig.sensitive_word_avoidance,
}
@@ -530,7 +569,6 @@ export const createPublishHandler =
({
appId,
chatPromptConfig,
citationConfig,
completionParamsState,
completionPromptConfig,
contextVar,
@@ -539,25 +577,19 @@ export const createPublishHandler =
datasetConfigs,
externalDataToolsConfig,
hasSetBlockStatus,
introduction,
isAdvancedMode,
isFunctionCall,
mode,
modelConfig,
moreLikeThisConfig,
promptEmpty,
promptMode,
resolvedModelModeType,
setCanReturnToSimpleMode,
setPublishedConfig,
speechToTextConfig,
suggestedQuestionsAfterAnswerConfig,
t: rawTranslate,
textToSpeechConfig,
}: {
appId: string
chatPromptConfig: BackendModelConfig['chat_prompt_config']
citationConfig: ModelConfig['retriever_resource']
completionParamsState: FormValue
completionPromptConfig: BackendModelConfig['completion_prompt_config']
contextVar?: string
@@ -566,21 +598,16 @@ export const createPublishHandler =
datasetConfigs: DatasetConfigs
externalDataToolsConfig: BackendModelConfig['external_data_tools']
hasSetBlockStatus: { history: boolean; query: boolean }
introduction: string
isAdvancedMode: boolean
isFunctionCall: boolean
mode: AppModeEnum
modelConfig: ModelConfig
moreLikeThisConfig: ModelConfig['more_like_this']
promptEmpty: boolean
promptMode: BackendModelConfig['prompt_type']
resolvedModelModeType: ModelModeType
setCanReturnToSimpleMode: (value: boolean) => void
setPublishedConfig: (config: { modelConfig: ModelConfig; completionParams: FormValue }) => void
speechToTextConfig: ModelConfig['speech_to_text']
suggestedQuestionsAfterAnswerConfig: ModelConfig['suggested_questions_after_answer']
setPublishedConfig: (config: ConfigurationPublishConfig) => void
t: SelectorTranslate<'appDebug' | 'common'>
textToSpeechConfig: ModelConfig['text_to_speech']
}) =>
async (
updateAppModelConfig: (params: { url: string; body: BackendModelConfig }) => Promise<unknown>,
@@ -643,18 +670,47 @@ export const createPublishHandler =
await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body })
const nextModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.opening_statement = introduction
draft.more_like_this = moreLikeThisConfig
draft.suggested_questions_after_answer = suggestedQuestionsAfterAnswerConfig
draft.speech_to_text = speechToTextConfig
draft.text_to_speech = textToSpeechConfig
draft.retriever_resource = citationConfig
draft.provider = body.model.provider
draft.model_id = body.model.name
draft.mode = body.model.mode
draft.configs.prompt_template = body.pre_prompt
draft.prompt_type = body.prompt_type
draft.chat_prompt_config = normalizeChatPromptConfig(body.chat_prompt_config)
draft.completion_prompt_config = normalizeCompletionPromptConfig(
body.completion_prompt_config,
)
draft.opening_statement = body.opening_statement
draft.more_like_this = body.more_like_this
draft.suggested_questions = body.suggested_questions ?? []
draft.suggested_questions_after_answer = body.suggested_questions_after_answer
draft.speech_to_text = body.speech_to_text
draft.text_to_speech = body.text_to_speech
draft.file_upload = body.file_upload ?? null
draft.retriever_resource = body.retriever_resource
draft.sensitive_word_avoidance = body.sensitive_word_avoidance
draft.external_data_tools = body.external_data_tools
draft.system_parameters = body.system_parameters
const publishedAgentConfig = body.agent_mode as ModelConfig['agentConfig']
draft.agentConfig = {
...draft.agentConfig,
...publishedAgentConfig,
max_iteration: publishedAgentConfig.max_iteration || draft.agentConfig.max_iteration,
}
draft.dataSets = dataSets
})
setPublishedConfig({
modelConfig: nextModelConfig,
completionParams: completionParamsState,
completionParams: body.model.completion_params,
promptMode:
body.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple,
chatPromptConfig: normalizeChatPromptConfig(body.chat_prompt_config),
completionPromptConfig: normalizeCompletionPromptConfig(body.completion_prompt_config),
datasetConfigs: {
...datasetConfigs,
datasets: body.dataset_configs.datasets,
},
externalDataToolsConfig: body.external_data_tools ?? [],
})
toast.success(t(($) => $['api.success'], { ns: 'common' }))
setCanReturnToSimpleMode(false)
@@ -1,5 +1,6 @@
'use client'
import type { ComponentProps } from 'react'
import type { ConfigurationPublishConfig } from './use-configuration-utils'
import type { AppPublisherPublishParams } from '@/app/components/app/app-publisher'
import type AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
@@ -32,6 +33,7 @@ import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { APP_PUBLISH_DRAFT_CHANGED } from '@/app/components/app/app-publisher/events'
import {
useDebugWithSingleOrMultipleModel,
useFormattingChangedDispatcher,
@@ -57,6 +59,7 @@ import {
DEFAULT_COMPLETION_PROMPT_CONFIG,
} from '@/config'
import { userProfileIdAtom } from '@/context/account-state'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { useProviderContext } from '@/context/provider-context'
import { currentWorkspaceAtom, currentWorkspaceLoadingAtom } from '@/context/workspace-state'
@@ -77,11 +80,6 @@ import {
loadConfigurationState,
} from './use-configuration-utils'
type PublishConfig = {
modelConfig: ModelConfig
completionParams: FormValue
}
type DebugConfigurationValue = ComponentProps<typeof ConfigContext.Provider>['value']
export type ConfigurationViewModel = {
@@ -104,6 +102,7 @@ export type ConfigurationViewModel = {
onCompletionParamsChange: (params: FormValue) => void
onConfirmUseGPT4: () => void
onEnableMultipleModelDebug: () => void
onFeatureStoreChange: OnFeaturesChange
onFeaturesChange: OnFeaturesChange
onHideDebugPanel: () => void
onModelChange: ComponentProps<typeof ModelParameterModal>['setModel']
@@ -141,7 +140,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
const setDetailSidebarMode = useSetDetailSidebarMode()
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const latestPublishedAt = useMemo(() => appDetail?.model_config?.updated_at, [appDetail])
const serverLatestPublishedAt = useMemo(() => appDetail?.model_config?.updated_at, [appDetail])
const appACLCapabilities = useMemo(
() =>
getAppACLCapabilities(appDetail?.permission_keys, {
@@ -157,36 +156,117 @@ export const useConfiguration = (): ConfigurationViewModel => {
const pathname = usePathname()
const matched = /\/app\/([^/]+)/.exec(pathname)
const appId = matched?.[1] || ''
const [publishedAtOverride, setPublishedAtOverride] = useState({
appId,
value: 0,
})
const latestPublishedAt =
publishedAtOverride.appId === appId
? Math.max(serverLatestPublishedAt || 0, publishedAtOverride.value)
: serverLatestPublishedAt
const [mode, setMode] = useState<AppModeEnum>(AppModeEnum.CHAT)
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)
const [publishedConfig, setPublishedConfig] = useState<ConfigurationPublishConfig | null>(null)
const [unpublishedChangesState, setUnpublishedChangesState] = useState({
appId,
value: false,
})
const hasUnpublishedChanges =
unpublishedChangesState.appId === appId && unpublishedChangesState.value
const [conversationId, setConversationId] = useState<string | null>('')
const { eventEmitter } = useEventEmitterContextContext()
const publishChangeTrackingAppIdRef = useRef('')
const dispatchPublishDraftChanged = useCallback(() => {
if (publishChangeTrackingAppIdRef.current !== appId) return
eventEmitter?.emit({
type: APP_PUBLISH_DRAFT_CHANGED,
instanceId: appId,
})
}, [appId, eventEmitter])
eventEmitter?.useSubscription((event) => {
if (
typeof event !== 'string' &&
event.type === APP_PUBLISH_DRAFT_CHANGED &&
event.instanceId === appId
)
setUnpublishedChangesState({ appId, value: true })
})
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const [isShowDebugPanel, { setTrue: showDebugPanel, setFalse: hideDebugPanel }] =
useBoolean(false)
const [introduction, setIntroduction] = useState('')
const [suggestedQuestions, setSuggestedQuestions] = useState<string[]>([])
const [introduction, doSetIntroduction] = useState('')
const setIntroduction = useCallback(
(value: string) => {
doSetIntroduction(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [suggestedQuestions, doSetSuggestedQuestions] = useState<string[]>([])
const setSuggestedQuestions = useCallback(
(value: string[]) => {
doSetSuggestedQuestions(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
prompt_template: '',
prompt_variables: [],
})
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig>({
const [moreLikeThisConfig, doSetMoreLikeThisConfig] = useState<MoreLikeThisConfig>({
enabled: false,
})
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] =
const setMoreLikeThisConfig = useCallback(
(value: MoreLikeThisConfig) => {
doSetMoreLikeThisConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [suggestedQuestionsAfterAnswerConfig, doSetSuggestedQuestionsAfterAnswerConfig] =
useState<MoreLikeThisConfig>({ enabled: false })
const [speechToTextConfig, setSpeechToTextConfig] = useState<MoreLikeThisConfig>({
const setSuggestedQuestionsAfterAnswerConfig = useCallback(
(value: MoreLikeThisConfig) => {
doSetSuggestedQuestionsAfterAnswerConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [speechToTextConfig, doSetSpeechToTextConfig] = useState<MoreLikeThisConfig>({
enabled: false,
})
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig>({
const setSpeechToTextConfig = useCallback(
(value: MoreLikeThisConfig) => {
doSetSpeechToTextConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [textToSpeechConfig, doSetTextToSpeechConfig] = useState<TextToSpeechConfig>({
enabled: false,
voice: '',
language: '',
})
const [citationConfig, setCitationConfig] = useState<MoreLikeThisConfig>({ enabled: false })
const setTextToSpeechConfig = useCallback(
(value: TextToSpeechConfig) => {
doSetTextToSpeechConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [citationConfig, doSetCitationConfig] = useState<MoreLikeThisConfig>({ enabled: false })
const setCitationConfig = useCallback(
(value: MoreLikeThisConfig) => {
doSetCitationConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [annotationConfig, doSetAnnotationConfig] = useState<AnnotationReplyConfig>({
id: '',
enabled: false,
@@ -205,8 +285,22 @@ export const useConfiguration = (): ConfigurationViewModel => {
[formattingChangedDispatcher],
)
const [moderationConfig, setModerationConfig] = useState<ModerationConfig>({ enabled: false })
const [externalDataToolsConfig, setExternalDataToolsConfig] = useState<ExternalDataTool[]>([])
const [moderationConfig, doSetModerationConfig] = useState<ModerationConfig>({ enabled: false })
const setModerationConfig = useCallback(
(value: ModerationConfig) => {
doSetModerationConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [externalDataToolsConfig, doSetExternalDataToolsConfig] = useState<ExternalDataTool[]>([])
const setExternalDataToolsConfig = useCallback(
(value: ExternalDataTool[]) => {
doSetExternalDataToolsConfig(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [inputs, setInputs] = useState<Inputs>({})
const [query, setQuery] = useState('')
const [completionParamsState, doSetCompletionParams] = useState<FormValue>({})
@@ -257,13 +351,18 @@ export const useConfiguration = (): ConfigurationViewModel => {
setTempStop([])
}
doSetCompletionParams(params)
dispatchPublishDraftChanged()
},
[getTempStop, setTempStop],
[dispatchPublishDraftChanged, getTempStop, setTempStop],
)
const setModelConfig = useCallback((newModelConfig: ModelConfig) => {
doSetModelConfig(newModelConfig)
}, [])
const setModelConfig = useCallback(
(newModelConfig: ModelConfig) => {
doSetModelConfig(newModelConfig)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const isAgent = mode === AppModeEnum.AGENT_CHAT
@@ -282,12 +381,23 @@ export const useConfiguration = (): ConfigurationViewModel => {
},
})
const datasetConfigsRef = useRef(datasetConfigs)
const setDatasetConfigs = useCallback((newDatasetConfigs: DatasetConfigs) => {
doSetDatasetConfigs(newDatasetConfigs)
datasetConfigsRef.current = newDatasetConfigs
}, [])
const setDatasetConfigs = useCallback(
(newDatasetConfigs: DatasetConfigs) => {
doSetDatasetConfigs(newDatasetConfigs)
datasetConfigsRef.current = newDatasetConfigs
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const [dataSets, setDataSets] = useState<DataSet[]>([])
const [dataSets, doSetDataSets] = useState<DataSet[]>([])
const setDataSets = useCallback(
(value: DataSet[]) => {
doSetDataSets(value)
dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const contextVar = modelConfig.configs.prompt_variables.find((item) => item.is_context_var)?.key
const hasSetContextVar = !!contextVar
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] =
@@ -301,30 +411,6 @@ export const useConfiguration = (): ConfigurationViewModel => {
const { currentModel: currentRerankModel, currentProvider: currentRerankProvider } =
useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const syncToPublishedConfig = useCallback(
(_publishedConfig: PublishConfig) => {
const publishedModelConfig = _publishedConfig.modelConfig
setModelConfig(publishedModelConfig)
setCompletionParams(_publishedConfig.completionParams)
setDataSets(publishedModelConfig.dataSets || [])
setIntroduction(publishedModelConfig.opening_statement || '')
setMoreLikeThisConfig(publishedModelConfig.more_like_this || { enabled: false })
setSuggestedQuestionsAfterAnswerConfig(
publishedModelConfig.suggested_questions_after_answer || { enabled: false },
)
setSpeechToTextConfig(publishedModelConfig.speech_to_text || { enabled: false })
setTextToSpeechConfig(
publishedModelConfig.text_to_speech || {
enabled: false,
voice: '',
language: '',
},
)
setCitationConfig(publishedModelConfig.retriever_resource || { enabled: false })
},
[setCompletionParams, setModelConfig],
)
const { isAPIKeySet } = useProviderContext()
const { currentModel: currModel } = useTextGenerationCurrentProviderAndModelAndModelList({
provider: modelConfig.provider,
@@ -361,9 +447,10 @@ export const useConfiguration = (): ConfigurationViewModel => {
detail: config.detail || Resolution.low,
transfer_methods: config.transfer_methods || [TransferMethod.local_file],
})
dispatchPublishDraftChanged()
if (!notNoticeFormattingChanged) formattingChangedDispatcher()
},
[formattingChangedDispatcher],
[dispatchPublishDraftChanged, formattingChangedDispatcher],
)
const {
@@ -389,8 +476,77 @@ export const useConfiguration = (): ConfigurationViewModel => {
completionParams: completionParamsState,
setCompletionParams,
setStop: setTempStop,
onPublishConfigChange: dispatchPublishDraftChanged,
})
const syncToPublishedConfig = useCallback(
(_publishedConfig: ConfigurationPublishConfig) => {
const trackedAppId = publishChangeTrackingAppIdRef.current
publishChangeTrackingAppIdRef.current = ''
try {
const publishedModelConfig = _publishedConfig.modelConfig
setModelConfig(publishedModelConfig)
setCompletionParams(_publishedConfig.completionParams)
doSetPromptMode(_publishedConfig.promptMode)
setCanReturnToSimpleMode(_publishedConfig.promptMode !== PromptMode.advanced)
setChatPromptConfig(_publishedConfig.chatPromptConfig)
setCompletionPromptConfig(_publishedConfig.completionPromptConfig)
setDataSets(publishedModelConfig.dataSets || [])
setDatasetConfigs(_publishedConfig.datasetConfigs)
setExternalDataToolsConfig(_publishedConfig.externalDataToolsConfig)
setIntroduction(publishedModelConfig.opening_statement || '')
setSuggestedQuestions(publishedModelConfig.suggested_questions || [])
setMoreLikeThisConfig(publishedModelConfig.more_like_this || { enabled: false })
setSuggestedQuestionsAfterAnswerConfig(
publishedModelConfig.suggested_questions_after_answer || { enabled: false },
)
setSpeechToTextConfig(publishedModelConfig.speech_to_text || { enabled: false })
setTextToSpeechConfig(
publishedModelConfig.text_to_speech || {
enabled: false,
voice: '',
language: '',
},
)
setCitationConfig(publishedModelConfig.retriever_resource || { enabled: false })
setModerationConfig(publishedModelConfig.sensitive_word_avoidance || { enabled: false })
const publishedVisionConfig = publishedModelConfig.file_upload?.image
handleSetVisionConfig(
{
enabled: publishedVisionConfig?.enabled || false,
number_limits: publishedVisionConfig?.number_limits || 2,
detail: publishedVisionConfig?.detail || Resolution.low,
transfer_methods: publishedVisionConfig?.transfer_methods || [
TransferMethod.local_file,
],
},
true,
)
} finally {
publishChangeTrackingAppIdRef.current = trackedAppId
}
},
[
handleSetVisionConfig,
setChatPromptConfig,
setCitationConfig,
setCompletionParams,
setCompletionPromptConfig,
setDataSets,
setDatasetConfigs,
setExternalDataToolsConfig,
setIntroduction,
setModelConfig,
setModerationConfig,
setMoreLikeThisConfig,
setSpeechToTextConfig,
setSuggestedQuestions,
setSuggestedQuestionsAfterAnswerConfig,
setTextToSpeechConfig,
],
)
const setPromptMode = useCallback(
async (nextMode: PromptMode) => {
if (nextMode === PromptMode.advanced) {
@@ -398,8 +554,9 @@ export const useConfiguration = (): ConfigurationViewModel => {
setCanReturnToSimpleMode(true)
}
doSetPromptMode(nextMode)
dispatchPublishDraftChanged()
},
[migrateToDefaultPrompt],
[dispatchPublishDraftChanged, migrateToDefaultPrompt],
)
const handleSelect = useCallback(
@@ -479,6 +636,12 @@ export const useConfiguration = (): ConfigurationViewModel => {
},
[formattingChangedDispatcher, setShowAppConfigureFeaturesModal],
)
const handleFeatureStoreChange = useCallback<OnFeaturesChange>(
(features) => {
if (features) dispatchPublishDraftChanged()
},
[dispatchPublishDraftChanged],
)
const handleAddPromptVariable = useCallback(
(variables: PromptVariable[]) => {
@@ -492,6 +655,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
)
useEffect(() => {
publishChangeTrackingAppIdRef.current = ''
void (async () => {
const configurationState = await loadConfigurationState({
appId,
@@ -502,35 +666,14 @@ export const useConfiguration = (): ConfigurationViewModel => {
setCollectionList(configurationState.collectionList)
setMode(configurationState.mode)
doSetPromptMode(configurationState.promptMode)
setDataSets(configurationState.nextDataSets)
setIntroduction(configurationState.introduction)
setSuggestedQuestions(configurationState.suggestedQuestions)
setMoreLikeThisConfig(configurationState.moreLikeThisConfig)
setSuggestedQuestionsAfterAnswerConfig(configurationState.suggestedQuestionsAfterAnswerConfig)
setSpeechToTextConfig(configurationState.speechToTextConfig)
setTextToSpeechConfig(configurationState.textToSpeechConfig)
setCitationConfig(configurationState.citationConfig)
setModerationConfig(configurationState.moderationConfig || { enabled: false })
setExternalDataToolsConfig(configurationState.externalDataToolsConfig)
setDatasetConfigs(configurationState.datasetConfigs)
if (configurationState.promptMode === PromptMode.advanced) {
setChatPromptConfig(configurationState.chatPromptConfig)
setCompletionPromptConfig(configurationState.completionPromptConfig as never)
setCanReturnToSimpleMode(false)
} else {
setCanReturnToSimpleMode(configurationState.canReturnToSimpleMode)
}
syncToPublishedConfig(configurationState.publishedConfig)
if (configurationState.annotationConfig)
setAnnotationConfig(configurationState.annotationConfig, true)
if (configurationState.visionConfig)
handleSetVisionConfig(configurationState.visionConfig, true)
syncToPublishedConfig(configurationState.publishedConfig as PublishConfig)
setPublishedConfig(configurationState.publishedConfig as PublishConfig)
setPublishedConfig(configurationState.publishedConfig)
setUnpublishedChangesState({ appId, value: false })
publishChangeTrackingAppIdRef.current = appId
setHasFetchedDetail(true)
})()
}, [appId])
@@ -569,11 +712,14 @@ export const useConfiguration = (): ConfigurationViewModel => {
params && 'model' in params && 'provider' in params && 'parameters' in params
? params
: undefined
const handlePublishedConfigChange = (config: ConfigurationPublishConfig) => {
setPublishedConfig(config)
if (modelAndParameter) syncToPublishedConfig(config)
}
return createPublishHandler({
const result = await createPublishHandler({
appId,
chatPromptConfig,
citationConfig,
completionParamsState,
completionPromptConfig,
contextVar,
@@ -582,28 +728,30 @@ export const useConfiguration = (): ConfigurationViewModel => {
datasetConfigs,
externalDataToolsConfig,
hasSetBlockStatus,
introduction,
isAdvancedMode,
isFunctionCall,
mode,
modelConfig,
moreLikeThisConfig,
promptEmpty,
promptMode,
resolvedModelModeType,
setCanReturnToSimpleMode,
setPublishedConfig,
speechToTextConfig,
suggestedQuestionsAfterAnswerConfig,
setPublishedConfig: handlePublishedConfigChange,
t,
textToSpeechConfig,
})(updateAppModelConfig, modelAndParameter, features)
if (result) {
setUnpublishedChangesState({ appId, value: false })
// The publish API currently returns only a result flag, so keep the summary current
// locally until app detail is refreshed with the server-side updated_at value.
setPublishedAtOverride({ appId, value: Math.floor(Date.now() / 1000) })
}
return result
},
[
appACLCapabilities.canReleaseAndVersion,
appId,
chatPromptConfig,
citationConfig,
completionParamsState,
completionPromptConfig,
contextVar,
@@ -612,21 +760,16 @@ export const useConfiguration = (): ConfigurationViewModel => {
datasetConfigs,
externalDataToolsConfig,
hasSetBlockStatus,
introduction,
isAdvancedMode,
isFunctionCall,
mode,
modelConfig,
moreLikeThisConfig,
promptEmpty,
promptMode,
resolvedModelModeType,
setCanReturnToSimpleMode,
setPublishedConfig,
speechToTextConfig,
suggestedQuestionsAfterAnswerConfig,
syncToPublishedConfig,
t,
textToSpeechConfig,
],
)
@@ -746,11 +889,16 @@ export const useConfiguration = (): ConfigurationViewModel => {
disabled: !appACLCapabilities.canReleaseAndVersion,
publishDisabled: cannotPublish || !appACLCapabilities.canReleaseAndVersion,
publishedAt: (latestPublishedAt || 0) * 1000,
hasUnpublishedChanges: !latestPublishedAt || hasUnpublishedChanges,
debugWithMultipleModel,
multipleModelConfigs,
onPublish,
publishedConfig: publishedConfig as PublishConfig,
resetAppConfig: () => publishedConfig && syncToPublishedConfig(publishedConfig),
publishedConfig: publishedConfig as ConfigurationPublishConfig,
resetAppConfig: () => {
if (!publishedConfig) return
syncToPublishedConfig(publishedConfig)
setUnpublishedChangesState({ appId, value: false })
},
},
contextValue,
featuresData,
@@ -773,6 +921,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
setShowUseGPT4Confirm(false)
},
onEnableMultipleModelDebug: handleDebugWithMultipleModelChange,
onFeatureStoreChange: handleFeatureStoreChange,
onFeaturesChange: handleFeaturesChange,
onHideDebugPanel: hideDebugPanel,
onModelChange: setModel,
@@ -151,6 +151,20 @@ describe('createFeaturesStore', () => {
expect(store.getState().features.moreLikeThis?.enabled).toBe(true)
expect(store.getState().features.opening?.enabled).toBe(true)
})
it('should notify publish tracking unless the update is silent', () => {
const onFeaturesChange = vi.fn()
const store = createFeaturesStore(undefined, onFeaturesChange)
const nextFeatures = {
moreLikeThis: { enabled: true },
}
store.getState().setFeatures(nextFeatures)
store.getState().setFeatures(nextFeatures, { silent: true })
expect(onFeaturesChange).toHaveBeenCalledOnce()
expect(onFeaturesChange).toHaveBeenCalledWith(nextFeatures)
})
})
describe('showFeaturesModal', () => {
+13 -2
View File
@@ -1,4 +1,5 @@
import type { FeaturesState, FeaturesStore } from './store'
import type { Features } from './types'
import { createContext, useRef } from 'react'
import { createFeaturesStore } from './store'
@@ -6,11 +7,21 @@ export const FeaturesContext = createContext<FeaturesStore | null>(null)
type FeaturesProviderProps = {
children: React.ReactNode
onFeaturesChange?: (features: Features) => void
} & Partial<FeaturesState>
export const FeaturesProvider = ({ children, ...props }: FeaturesProviderProps) => {
export const FeaturesProvider = ({
children,
onFeaturesChange,
...props
}: FeaturesProviderProps) => {
const storeRef = useRef<FeaturesStore | undefined>(undefined)
const onFeaturesChangeRef = useRef(onFeaturesChange)
onFeaturesChangeRef.current = onFeaturesChange
if (!storeRef.current) storeRef.current = createFeaturesStore(props)
if (!storeRef.current)
storeRef.current = createFeaturesStore(props, (features) =>
onFeaturesChangeRef.current?.(features),
)
return <FeaturesContext.Provider value={storeRef.current}>{children}</FeaturesContext.Provider>
}
@@ -35,8 +35,8 @@ const AnnotationReply = ({ disabled, onChange }: Props) => {
const newFeatures = produce(features, (draft) => {
draft.annotationReply = newConfig
})
setFeatures(newFeatures)
if (onChange) onChange(newFeatures)
setFeatures(newFeatures, { silent: true })
onChange?.()
},
[featuresStore, onChange],
)
+13 -3
View File
@@ -12,14 +12,21 @@ export type FeaturesState = {
}
type FeaturesAction = {
setFeatures: (features: Features) => void
setFeatures: (features: Features, options?: SetFeaturesOptions) => void
}
export type FeatureStoreState = FeaturesState & FeaturesAction & FeaturesModal
export type FeaturesStore = ReturnType<typeof createFeaturesStore>
export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
export type SetFeaturesOptions = {
silent?: boolean
}
export const createFeaturesStore = (
initProps?: Partial<FeaturesState>,
onFeaturesChange?: (features: Features) => void,
) => {
const DEFAULT_PROPS: FeaturesState = {
features: {
moreLikeThis: {
@@ -59,7 +66,10 @@ export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
return createStore<FeatureStoreState>()((set) => ({
...DEFAULT_PROPS,
...initProps,
setFeatures: (features) => set(() => ({ features })),
setFeatures: (features, options) => {
set(() => ({ features }))
if (!options?.silent) onFeaturesChange?.(features)
},
showFeaturesModal: false,
setShowFeaturesModal: (showFeaturesModal) => set(() => ({ showFeaturesModal })),
}))
@@ -51,6 +51,7 @@ const FeaturesTrigger = () => {
const { plan, isFetchedPlan } = useProviderContext()
const publishedAt = useStore((s) => s.publishedAt)
const draftUpdatedAt = useStore((s) => s.draftUpdatedAt)
const draftHash = useStore((s) => s.syncWorkflowDraftHash)
const toolPublished = useStore((s) => s.toolPublished)
const lastPublishedHasUserInput = useStore((s) => s.lastPublishedHasUserInput)
@@ -253,6 +254,7 @@ const FeaturesTrigger = () => {
{...{
publishedAt,
draftUpdatedAt,
draftHash,
disabled: nodesReadOnly || !hasWorkflowNodes || !canReleaseAndVersion,
toolPublished,
inputs: variables,
@@ -1,7 +1,7 @@
import { waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import { renderHook } from '@/test/console/render'
import { renderHookWithConsoleQuery as renderHook } from '@/test/console/query-data'
import { AppACLPermission } from '@/utils/permission'
import { useWorkflowInit } from '../use-workflow-init'
@@ -72,6 +72,10 @@ vi.mock('../use-workflow-template', () => ({
}))
vi.mock('@/service/use-workflow', () => ({
appWorkflowQueryOptions: (appId: string) => ({
queryKey: ['workflow', 'publish', appId],
queryFn: () => mockFetchPublishedWorkflow(`/apps/${appId}/workflows/publish`),
}),
useWorkflowConfig: (_url: string, onSuccess: (config: Record<string, unknown>) => void) => {
if (workflowConfigState.data) onSuccess(workflowConfigState.data)
return workflowConfigState
@@ -84,7 +88,6 @@ vi.mock('@/service/workflow', () => ({
fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args),
syncWorkflowDraft: (...args: unknown[]) => mockSyncWorkflowDraft(...args),
fetchNodesDefaultConfigs: (...args: unknown[]) => mockFetchNodesDefaultConfigs(...args),
fetchPublishedWorkflow: (...args: unknown[]) => mockFetchPublishedWorkflow(...args),
}))
const notExistError = () => ({
@@ -352,14 +355,45 @@ describe('useWorkflowInit', () => {
expect(result.current.isLoading).toBe(false)
})
it('should fall back to no published user input when preload requests fail', async () => {
it('should keep published metadata when loading node defaults fails', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
mockFetchWorkflowDraft.mockReset().mockResolvedValue(draftResponse)
mockFetchNodesDefaultConfigs.mockRejectedValue(new Error('preload failed'))
mockFetchPublishedWorkflow.mockResolvedValue({
created_at: 99,
graph: {
nodes: [{ id: 'start', data: { type: BlockEnum.Start } }],
edges: [{ source: 'start', target: 'end' }],
},
})
renderHook(() => useWorkflowInit())
await waitFor(() => {
expect(mockSetPublishedAt).toHaveBeenCalledWith(99)
expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true)
})
expect(consoleErrorSpy).toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
it('should keep node defaults when loading published metadata fails', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
mockFetchWorkflowDraft.mockReset().mockResolvedValue(draftResponse)
mockFetchNodesDefaultConfigs.mockResolvedValue([
{ type: 'start', config: { title: 'Start Config' } },
])
mockFetchPublishedWorkflow.mockRejectedValue(new Error('published workflow failed'))
renderHook(() => useWorkflowInit())
await waitFor(() => {
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({
nodesDefaultConfigs: {
start: { title: 'Start Config' },
},
})
expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(false)
})
@@ -1,6 +1,7 @@
import type { Edge, Node } from '@/app/components/workflow/types'
import type { FileUploadConfigResponse } from '@/models/common'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import { useQueryClient } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -8,13 +9,8 @@ import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { useWorkflowConfig } from '@/service/use-workflow'
import {
fetchNodesDefaultConfigs,
fetchPublishedWorkflow,
fetchWorkflowDraft,
syncWorkflowDraft,
} from '@/service/workflow'
import { appWorkflowQueryOptions, useWorkflowConfig } from '@/service/use-workflow'
import { fetchNodesDefaultConfigs, fetchWorkflowDraft, syncWorkflowDraft } from '@/service/workflow'
import { AppModeEnum } from '@/types/app'
import { getAppACLCapabilities } from '@/utils/permission'
import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas'
@@ -58,6 +54,7 @@ const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean
}
export const useWorkflowInit = () => {
const queryClient = useQueryClient()
const workflowStore = useWorkflowStore()
const { nodes: nodesTemplate, edges: edgesTemplate } = useWorkflowTemplate()
const appDetail = useAppStore((state) => state.appDetail)!
@@ -190,13 +187,13 @@ export const useWorkflowInit = () => {
}, [])
const handleFetchPreloadData = useCallback(async () => {
try {
const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(
`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`,
)
const publishedWorkflow = await fetchPublishedWorkflow(
`/apps/${appDetail?.id}/workflows/publish`,
)
const [nodesDefaultConfigsResult, publishedWorkflowResult] = await Promise.allSettled([
fetchNodesDefaultConfigs(`/apps/${appDetail.id}/workflows/default-workflow-block-configs`),
queryClient.fetchQuery(appWorkflowQueryOptions(appDetail.id)),
])
if (nodesDefaultConfigsResult.status === 'fulfilled') {
const nodesDefaultConfigsData = nodesDefaultConfigsResult.value
workflowStore.setState({
nodesDefaultConfigs: nodesDefaultConfigsData.reduce(
(acc, block) => {
@@ -206,16 +203,22 @@ export const useWorkflowInit = () => {
{} as Record<string, unknown>,
),
})
} else {
console.error(nodesDefaultConfigsResult.reason)
}
if (publishedWorkflowResult.status === 'fulfilled') {
const publishedWorkflow = publishedWorkflowResult.value
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at ?? 0)
const graph = publishedWorkflow?.graph
workflowStore
.getState()
.setLastPublishedHasUserInput(hasConnectedUserInput(graph?.nodes, graph?.edges))
} catch (e) {
console.error(e)
} else {
console.error(publishedWorkflowResult.reason)
workflowStore.getState().setLastPublishedHasUserInput(false)
}
}, [workflowStore, appDetail])
}, [workflowStore, appDetail, queryClient])
useEffect(() => {
handleFetchPreloadData()
@@ -11,6 +11,8 @@ const mockHandleLoadBackupDraft = vi.fn()
const mockHandleRefreshWorkflowDraft = vi.fn()
const mockHandleExportDSL = vi.fn()
const mockRestoreWorkflow = vi.fn()
const mockUpdateWorkflow = vi.fn()
const mockInvalidateAppWorkflow = vi.fn()
const mockSetCurrentVersion = vi.fn()
const mockSetShowWorkflowVersionHistoryPanel = vi.fn()
const mockWorkflowStoreSetState = vi.fn()
@@ -83,10 +85,11 @@ vi.mock('@/context/provider-context', () => ({
vi.mock('@/service/use-workflow', () => ({
useDeleteWorkflow: () => ({ mutateAsync: vi.fn() }),
useInvalidateAppWorkflow: () => mockInvalidateAppWorkflow,
useInvalidAllLastRun: () => vi.fn(),
useResetWorkflowVersionHistory: () => vi.fn(),
useRestoreWorkflow: () => ({ mutateAsync: mockRestoreWorkflow }),
useUpdateWorkflow: () => ({ mutateAsync: vi.fn() }),
useUpdateWorkflow: () => ({ mutateAsync: mockUpdateWorkflow }),
useWorkflowVersionHistory: () => ({
data: {
pages: [
@@ -128,10 +131,19 @@ vi.mock('../../../hooks/use-workflow-run', () => ({
}))
vi.mock('../../../hooks-store', () => ({
useHooksStore: () => ({
flowId: 'test-flow-id',
flowType: 'workflow',
}),
useHooksStore: (
selector: (state: {
accessControl: { canImportExportDSL: boolean }
configsMap: { flowId: string; flowType: string }
}) => unknown,
) =>
selector({
accessControl: { canImportExportDSL: true },
configsMap: {
flowId: 'app-1',
flowType: 'appFlow',
},
}),
}))
vi.mock('../../../collaboration/core/collaboration-manager', () => ({
@@ -180,7 +192,25 @@ vi.mock('../restore-confirm-modal', () => ({
}))
vi.mock('@/app/components/app/app-publisher/version-info-modal', () => ({
default: () => null,
default: ({
versionInfo,
onPublish,
}: {
versionInfo: VersionHistory
onPublish: (params: { id?: string; title: string; releaseNotes: string }) => Promise<void>
}) => (
<button
onClick={() =>
onPublish({
id: versionInfo.id,
title: 'Updated release',
releaseNotes: 'Updated notes',
})
}
>
submit version info
</button>
),
}))
vi.mock('../version-history-item', () => ({
@@ -213,6 +243,11 @@ vi.mock('../version-history-item', () => ({
>
{`export-${item.id}`}
</button>
<button
onClick={() => handleClickActionMenuItem(VersionHistoryContextMenuOptions.edit)}
>
{`edit-${item.id}`}
</button>
</>
)}
</div>
@@ -227,6 +262,7 @@ describe('VersionHistoryPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockRestoreWorkflow.mockResolvedValue(undefined)
mockUpdateWorkflow.mockResolvedValue(undefined)
mockCurrentVersion = null
mockPlanType = Plan.professional
mockEnableBilling = true
@@ -369,4 +405,35 @@ describe('VersionHistoryPanel', () => {
expect(mockSetCurrentVersion).not.toHaveBeenCalled()
expect(mockHandleRefreshWorkflowDraft).not.toHaveBeenCalled()
})
it('should refresh the published workflow after editing the latest app version', async () => {
mockUpdateWorkflow.mockImplementation(
async (
_params,
options?: {
onSuccess?: () => void
onSettled?: () => void
},
) => {
options?.onSuccess?.()
options?.onSettled?.()
},
)
const { VersionHistoryPanel } = await import('../index')
render(
<VersionHistoryPanel
latestVersionId="published-version-id"
restoreVersionUrl={(versionId) => `/apps/app-1/workflows/${versionId}/restore`}
updateVersionUrl={(versionId) => `/apps/app-1/workflows/${versionId}`}
/>,
)
fireEvent.click(screen.getByText('edit-published-version-id'))
fireEvent.click(screen.getByRole('button', { name: 'submit version info' }))
await waitFor(() => {
expect(mockInvalidateAppWorkflow).toHaveBeenCalledWith('app-1')
})
})
})
@@ -83,6 +83,23 @@ describe('VersionHistoryItem', () => {
// Published items should expose metadata and the hover context menu.
describe('Published Items', () => {
it('should show the mocked deployed environments', () => {
render(
<VersionHistoryItem
item={createVersionHistory()}
currentVersion={null}
latestVersionId="version-1"
onClick={vi.fn()}
handleClickActionMenuItem={vi.fn()}
canImportExportDSL
isLast={false}
/>,
)
expect(screen.getByText('Pre-release')).toBeInTheDocument()
expect(screen.getByText('QA')).toBeInTheDocument()
})
it('should open the context menu for a latest named version and forward restore', async () => {
const user = userEvent.setup()
const handleClickActionMenuItem = vi.fn()
@@ -116,6 +133,7 @@ describe('VersionHistoryItem', () => {
expect(screen.getByText('workflow.versionHistory.editVersionInfo')).toBeInTheDocument()
expect(screen.getByText('app.export')).toBeInTheDocument()
expect(screen.getByText('workflow.versionHistory.copyId')).toBeInTheDocument()
expect(screen.getByText('version-1')).toBeInTheDocument()
expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument()
const restoreItem = screen.getByText('workflow.common.restore').closest('.cursor-pointer')
@@ -43,6 +43,7 @@ describe('ActionMenu', () => {
renderActionMenu(
<ActionMenu
workflowId="version-1"
isNamedVersion
isShowDelete
canImportExportDSL
@@ -52,7 +53,10 @@ describe('ActionMenu', () => {
/>,
)
await user.click(screen.getByRole('button'))
const trigger = screen.getByRole('button', { name: 'common.operation.more' })
expect(trigger).not.toHaveAttribute('role')
await user.click(trigger)
await user.click(screen.getByText('workflow.common.restore'))
await user.click(screen.getByText('common.operation.delete'))
@@ -74,6 +78,7 @@ describe('ActionMenu', () => {
renderActionMenu(
<ActionMenu
workflowId="version-1"
isNamedVersion
isShowDelete
canImportExportDSL
@@ -6,6 +6,7 @@ describe('useActionMenu', () => {
it('returns restore, edit, export, copy and delete operations for app workflows', () => {
const { result } = renderWorkflowHook(() =>
useActionMenu({
workflowId: 'version-1',
isNamedVersion: true,
canImportExportDSL: true,
isShowDelete: false,
@@ -31,6 +32,7 @@ describe('useActionMenu', () => {
const { result } = renderWorkflowHook(
() =>
useActionMenu({
workflowId: 'version-1',
isNamedVersion: false,
canImportExportDSL: true,
isShowDelete: true,
@@ -57,6 +59,7 @@ describe('useActionMenu', () => {
{
key: VersionHistoryContextMenuOptions.copyId,
name: 'workflow.versionHistory.copyId',
description: 'version-1',
},
])
})
@@ -64,6 +67,7 @@ describe('useActionMenu', () => {
it('omits export when import/export DSL permission is missing', () => {
const { result } = renderWorkflowHook(() =>
useActionMenu({
workflowId: 'version-1',
isNamedVersion: true,
canImportExportDSL: false,
isShowDelete: false,
@@ -9,6 +9,7 @@ type ActionMenuItemProps = {
item: {
key: VersionHistoryContextMenuOptions
name: string
description?: string
showUpgrade?: boolean
}
onClick: (operation: VersionHistoryContextMenuOptions) => void
@@ -21,6 +22,7 @@ const ActionMenuItem: FC<ActionMenuItemProps> = ({ item, onClick, isDestructive
variant={isDestructive ? 'destructive' : 'default'}
className={cn(
'justify-between gap-x-3 px-2 py-1.5 whitespace-nowrap',
item.description && 'h-auto py-1',
isDestructive && 'data-highlighted:bg-state-destructive-hover',
)}
onClick={(event) => {
@@ -33,11 +35,20 @@ const ActionMenuItem: FC<ActionMenuItemProps> = ({ item, onClick, isDestructive
>
<div
className={cn(
'flex-1 system-md-regular whitespace-nowrap text-text-primary',
'min-w-0 flex-1 system-md-regular whitespace-nowrap text-text-primary',
item.description && 'flex flex-col gap-y-0.5 px-1 py-0.5 text-text-secondary',
isDestructive && 'text-inherit',
)}
>
{item.name}
<div className="w-full truncate">{item.name}</div>
{item.description && (
<div
className="w-full max-w-[152px] truncate system-2xs-regular text-text-tertiary"
title={item.description}
>
{item.description}
</div>
)}
</div>
{item.showUpgrade && (
<div data-upgrade-action className="shrink-0">
@@ -6,13 +6,14 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { RiMoreFill } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { VersionHistoryContextMenuOptions } from '../../../types'
import ActionMenuItem from './action-menu-item'
import useActionMenu from './use-action-menu'
export type ActionMenuProps = {
workflowId: string
isShowDelete: boolean
isNamedVersion: boolean
canImportExportDSL: boolean
@@ -24,21 +25,21 @@ export type ActionMenuProps = {
const ActionMenu: FC<ActionMenuProps> = (props: ActionMenuProps) => {
const { isShowDelete, handleClickActionMenuItem, open, setOpen } = props
const { deleteOperation, options } = useActionMenu(props)
const { t } = useTranslation()
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
nativeButton={false}
render={
<Button
nativeButton={false}
size="small"
className="px-1"
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
onClick={(e) => e.stopPropagation()}
/>
}
>
<RiMoreFill className="size-4" />
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-end"
@@ -54,7 +55,7 @@ const ActionMenu: FC<ActionMenuProps> = (props: ActionMenuProps) => {
))}
{isShowDelete && (
<>
<DropdownMenuSeparator className="my-0" />
<DropdownMenuSeparator className="my-1" />
<ActionMenuItem
item={deleteOperation}
isDestructive
@@ -7,7 +7,7 @@ import { useProviderContext } from '@/context/provider-context'
import { VersionHistoryContextMenuOptions } from '../../../types'
const useActionMenu = (props: ActionMenuProps) => {
const { isNamedVersion, canImportExportDSL } = props
const { workflowId, isNamedVersion, canImportExportDSL } = props
const { t } = useTranslation()
const pipelineId = useStore((s) => s.pipelineId)
const { plan, enableBilling } = useProviderContext()
@@ -47,9 +47,10 @@ const useActionMenu = (props: ActionMenuProps) => {
{
key: VersionHistoryContextMenuOptions.copyId,
name: t(($) => $['versionHistory.copyId'], { ns: 'workflow' }),
description: workflowId,
},
]
}, [canImportExportDSL, isNamedVersion, pipelineId, shouldShowUpgrade, shouldShowUpgrade, t])
}, [canImportExportDSL, isNamedVersion, pipelineId, shouldShowUpgrade, t, workflowId])
return {
deleteOperation,
@@ -16,11 +16,13 @@ import { useProviderContext } from '@/context/provider-context'
import {
useDeleteWorkflow,
useInvalidAllLastRun,
useInvalidateAppWorkflow,
useResetWorkflowVersionHistory,
useRestoreWorkflow,
useUpdateWorkflow,
useWorkflowVersionHistory,
} from '@/service/use-workflow'
import { FlowType } from '@/types/common'
import { useHooksStore } from '../../hooks-store'
import { useDSL } from '../../hooks/use-DSL'
import { useWorkflowRefreshDraft } from '../../hooks/use-workflow-refresh-draft'
@@ -76,6 +78,7 @@ export const VersionHistoryPanel = ({
const configsMap = useHooksStore((s) => s.configsMap)
const canImportExportDSL = useHooksStore((s) => s.accessControl.canImportExportDSL)
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
const invalidateAppWorkflow = useInvalidateAppWorkflow()
const { deleteAllInspectVars } = workflowStore.getState()
const { t } = useTranslation()
@@ -314,7 +317,12 @@ export const VersionHistoryPanel = ({
onSuccess: () => {
setEditModalOpen(false)
toast.success(t(($) => $['versionHistory.action.updateSuccess'], { ns: 'workflow' }))
resetWorkflowVersionHistory()
if (
id === latestVersionId &&
configsMap?.flowType === FlowType.appFlow &&
configsMap.flowId
)
invalidateAppWorkflow(configsMap.flowId)
},
onError: () => {
toast.error(t(($) => $['versionHistory.action.updateFailure'], { ns: 'workflow' }))
@@ -325,7 +333,15 @@ export const VersionHistoryPanel = ({
},
)
},
[t, updateWorkflow, resetWorkflowVersionHistory, updateVersionUrl],
[
configsMap?.flowId,
configsMap?.flowType,
invalidateAppWorkflow,
latestVersionId,
t,
updateWorkflow,
updateVersionUrl,
],
)
return (
@@ -5,6 +5,7 @@ import dayjs from 'dayjs'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge/index'
import { WorkflowVersion } from '../../types'
import ActionMenu from './action-menu'
@@ -19,6 +20,22 @@ type VersionHistoryItemProps = {
hideActionMenu?: boolean
}
type VersionEnvironment = {
id: string
name: string
}
const MOCK_ENVIRONMENTS: VersionEnvironment[] = [
{
id: 'mock-pre-release',
name: 'Pre-release',
},
{
id: 'mock-qa',
name: 'QA',
},
]
const formatVersion = (versionHistory: VersionHistory, latestVersionId: string): string => {
const { version, id } = versionHistory
if (version === WorkflowVersion.Draft) return WorkflowVersion.Draft
@@ -53,6 +70,8 @@ const VersionHistoryItem: React.FC<VersionHistoryItemProps> = ({
const isSelected = item.version === currentVersion?.version
const isDraft = formattedVersion === WorkflowVersion.Draft
const isLatest = formattedVersion === WorkflowVersion.Latest
// TODO: Replace with item.environments when the version history API exposes deployment data.
const deployedEnvironments = MOCK_ENVIRONMENTS
useEffect(() => {
if (isDraft) onClick(item)
@@ -87,7 +106,7 @@ const VersionHistoryItem: React.FC<VersionHistoryItemProps> = ({
{!isLast && (
<div className="absolute top-6 left-4 h-[calc(100%-0.75rem)] w-0.5 bg-divider-subtle" />
)}
<div className="flex h-5 w-[18px] shrink-0 items-center justify-center">
<div className="flex h-5 w-4.5 shrink-0 items-center justify-center">
<div
className={cn(
'size-2 rounded-lg border-2',
@@ -123,11 +142,25 @@ const VersionHistoryItem: React.FC<VersionHistoryItemProps> = ({
{`${formatTime(item.created_at)} · ${item.created_by.name}`}
</div>
)}
{!isDraft && deployedEnvironments.length > 0 && (
<div className="flex w-full flex-wrap content-start items-start gap-x-1 gap-y-2 pt-0.5">
{deployedEnvironments.map((environment) => (
<Badge
key={environment.id}
size="s"
className="h-4.5 shrink-0 bg-components-badge-bg-dimm py-0!"
>
{environment.name}
</Badge>
))}
</div>
)}
</div>
{/* Action Menu */}
{!hideActionMenu && !isDraft && isHovering && (
<div className="absolute top-1 right-1">
<ActionMenu
workflowId={item.id}
isShowDelete={!isLatest}
isNamedVersion={!!item.marked_name}
canImportExportDSL={canImportExportDSL}
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "توفر ملحقات API إدارة مركزية لواجهة برمجة التطبيقات، مما يبسط التكوين لسهولة الاستخدام عبر تطبيقات Dify.",
"apiBasedExtension.type": "النوع",
"apiBasedExtensionPage.description": "وسّع قدرات وحدات Dify عبر API — أضف إشرافًا مخصصًا على المحتوى أو أدوات بيانات خارجية إلى تطبيقاتك.",
"appMenus.accessPoint": "نقطة الوصول",
"appMenus.annotations": "التعليقات التوضيحية",
"appMenus.apiAccess": "وصول API",
"appMenus.apiAccessTip": "يمكن الوصول إلى قاعدة المعرفة هذه عبر واجهة برمجة تطبيقات الخدمة",
"appMenus.deploy": "نشر",
"appMenus.logs": "السجلات",
"appMenus.overview": "المراقبة",
"appMenus.promptEng": "تنسيق",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "رد",
"common.ImageUploadLegacyTip": "يمكنك الآن إنشاء متغيرات نوع الملف في نموذج البداية. لن ندعم ميزة تحميل الصور في المستقبل. ",
"common.accessAPIReference": "الوصول إلى مرجع API",
"common.accessPointDescription": "تهيئة تطبيق الويب وواجهة API وMCP والمشغلات",
"common.addBlock": "إضافة عقدة",
"common.addDescription": "إضافة وصف...",
"common.addFailureBranch": "إضافة فرع فشل",
@@ -168,6 +169,7 @@
"common.currentView": "العرض الحالي",
"common.currentWorkflow": "سير العمل الحالي",
"common.debugAndPreview": "معاينة",
"common.deployDescription": "نشر الإصدارات إلى البيئات",
"common.disconnect": "قطع الاتصال",
"common.draftSaveFailed": "فشل حفظ المسودة",
"common.duplicate": "تكرار",
@@ -216,11 +218,15 @@
"common.needConnectTip": "هذه الخطوة غير متصلة بأي شيء",
"common.needOutputNode": "يجب إضافة عقدة الإخراج",
"common.needStartNode": "يجب إضافة عقدة بدء واحدة على الأقل",
"common.noChanges": "لا توجد تغييرات",
"common.noHistory": "لا يوجد سجل",
"common.noVar": "لا يوجد متغير",
"common.notPublishedYet": "لم يتم النشر بعد",
"common.notRunning": "لم يتم التشغيل بعد",
"common.onFailure": "عند الفشل",
"common.openInExplore": "فتح في الاستكشاف",
"common.openWebApp": "فتح تطبيق الويب",
"common.openWebAppDescription": "افتح التطبيق المنشور في علامة تبويب جديدة",
"common.output": "إخراج",
"common.overwriteAndImport": "استبدال واستيراد",
"common.parallel": "توازي",
@@ -237,10 +243,12 @@
"common.processData": "معالجة البيانات",
"common.publish": "نشر",
"common.publishToMarketplace": "نشر على Marketplace",
"common.publishToMarketplaceDescription": "شارك هذا التطبيق مع المجتمع",
"common.publishToMarketplaceFailed": "فشل النشر على Marketplace",
"common.publishUpdate": "نشر التحديث",
"common.published": "منشور",
"common.publishedAt": "تم النشر في",
"common.publishedBy": "تم النشر {{time}} بواسطة {{author}}",
"common.publishingToMarketplace": "جارٍ النشر...",
"common.redo": "إعادة",
"common.restart": "إعادة تشغيل",
@@ -250,6 +258,7 @@
"common.runApp": "تشغيل التطبيق",
"common.runHistory": "سجل التشغيل",
"common.running": "جارٍ التشغيل",
"common.savedAt": "تم الحفظ {{time}}",
"common.scheduleTriggerRunFailed": "فشل تشغيل المشغل المجدول",
"common.searchVar": "بحث عن متغير",
"common.setVarValuePlaceholder": "تعيين متغير",
@@ -264,6 +273,7 @@
"common.tagBound": "عدد التطبيقات التي تستخدم هذه العلامة",
"common.undo": "تراجع",
"common.unpublished": "غير منشور",
"common.unpublishedChanges": "تغييرات غير منشورة",
"common.update": "تحديث",
"common.variableNamePlaceholder": "اسم المتغير",
"common.versionHistory": "سجل الإصدارات",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "فشل تصحيح أخطاء Webhook",
"common.webhookDebugRequestFailed": "فشل طلب تصحيح أخطاء Webhook",
"common.workflowAsTool": "سير العمل كأداة",
"common.workflowAsToolDescription": "استخدمه كأداة في تطبيقات أخرى",
"common.workflowAsToolDisabledHint": "انشر أحدث سير عمل وتأكد من وجود عقدة إدخال مستخدم متصلة قبل تكوينها كأداة.",
"common.workflowAsToolReady": "جاهز",
"common.workflowAsToolReconfigure": "إعادة التهيئة",
"common.workflowAsToolTip": "التكوين المطلوب للأداة بعد تحديث سير العمل.",
"common.workflowAsToolUpdateNeeded": "التحديث مطلوب",
"common.workflowProcess": "عملية سير العمل",
"common.workflowProcessFailed": "فشلت عملية سير العمل",
"common.workflowProcessPaused": "تم إيقاف عملية سير العمل مؤقتًا",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "الخاص بك فقط",
"versionHistory.filter.reset": "إعادة تعيين التصفية",
"versionHistory.latest": "الأحدث",
"versionHistory.nameIt": "سمّه",
"versionHistory.nameThisVersion": "تسمية هذا الإصدار",
"versionHistory.releaseNotesPlaceholder": "صف ما تغير",
"versionHistory.restorationTip": "بعد استعادة الإصدار، سيتم استبدال المسودة الحالية.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API-Erweiterungen bieten zentralisiertes API-Management und vereinfachen die Konfiguration für eine einfache Verwendung in Difys Anwendungen.",
"apiBasedExtension.type": "Typ",
"apiBasedExtensionPage.description": "Erweitere die Modulfunktionen von Dify per API — füge deinen Apps benutzerdefinierte Inhaltsmoderation oder externe Datentools hinzu.",
"appMenus.accessPoint": "Zugangspunkt",
"appMenus.annotations": "Anmerkungen",
"appMenus.apiAccess": "API-Zugriff",
"appMenus.apiAccessTip": "Diese Wissensdatenbank ist über die Service-API zugänglich",
"appMenus.deploy": "Bereitstellen",
"appMenus.logs": "Baumstämme",
"appMenus.overview": "Übersicht",
"appMenus.promptEng": "Orchestrieren",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Antworten",
"common.ImageUploadLegacyTip": "Sie können jetzt Dateitypvariablen im Startformular erstellen. Wir werden die Funktion zum Hochladen von Bildern in Zukunft nicht mehr unterstützen.",
"common.accessAPIReference": "API-Referenz aufrufen",
"common.accessPointDescription": "Web-App, API, MCP und Trigger konfigurieren",
"common.addBlock": "Knoten hinzufügen",
"common.addDescription": "Beschreibung hinzufügen...",
"common.addFailureBranch": "Fail-Branch hinzufügen",
@@ -168,6 +169,7 @@
"common.currentView": "Aktuelle Ansicht",
"common.currentWorkflow": "Aktueller Arbeitsablauf",
"common.debugAndPreview": "Vorschau",
"common.deployDescription": "Versionen in Umgebungen bereitstellen",
"common.disconnect": "Trennen",
"common.draftSaveFailed": "Entwurf konnte nicht gespeichert werden",
"common.duplicate": "Duplizieren",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Dieser Schritt ist mit nichts verbunden",
"common.needOutputNode": "Der Ausgabeknoten muss hinzugefügt werden",
"common.needStartNode": "Es muss mindestens ein Startknoten hinzugefügt werden",
"common.noChanges": "Keine Änderungen",
"common.noHistory": "Keine Geschichte",
"common.noVar": "Keine Variable",
"common.notPublishedYet": "Noch nicht veröffentlicht",
"common.notRunning": "Noch nicht ausgeführt",
"common.onFailure": "Bei Ausfall",
"common.openInExplore": "In Explore öffnen",
"common.openWebApp": "Web-App öffnen",
"common.openWebAppDescription": "Die veröffentlichte App in einem neuen Tab öffnen",
"common.output": "Ausgabe",
"common.overwriteAndImport": "Überschreiben und Importieren",
"common.parallel": "PARALLEL",
@@ -237,10 +243,12 @@
"common.processData": "Daten verarbeiten",
"common.publish": "Veröffentlichen",
"common.publishToMarketplace": "Im Marketplace veröffentlichen",
"common.publishToMarketplaceDescription": "Diese App mit der Community teilen",
"common.publishToMarketplaceFailed": "Veröffentlichung im Marketplace fehlgeschlagen",
"common.publishUpdate": "Update veröffentlichen",
"common.published": "Veröffentlicht",
"common.publishedAt": "Veröffentlicht am",
"common.publishedBy": "Veröffentlicht {{time}} von {{author}}",
"common.publishingToMarketplace": "Wird veröffentlicht...",
"common.redo": "Wiederholen",
"common.restart": "Neustarten",
@@ -250,6 +258,7 @@
"common.runApp": "App ausführen",
"common.runHistory": "Ausführungsverlauf",
"common.running": "Wird ausgeführt",
"common.savedAt": "Gespeichert {{time}}",
"common.scheduleTriggerRunFailed": "Geplanter Trigger-Lauf fehlgeschlagen",
"common.searchVar": "Variable suchen",
"common.setVarValuePlaceholder": "Variable setzen",
@@ -264,6 +273,7 @@
"common.tagBound": "Anzahl der Apps, die dieses Tag verwenden",
"common.undo": "Rückgängig",
"common.unpublished": "Unveröffentlicht",
"common.unpublishedChanges": "Unveröffentlichte Änderungen",
"common.update": "Aktualisieren",
"common.variableNamePlaceholder": "Variablenname",
"common.versionHistory": "Versionsverlauf",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook-Debugging fehlgeschlagen",
"common.webhookDebugRequestFailed": "Webhook-Debugging-Anfrage fehlgeschlagen",
"common.workflowAsTool": "Workflow als Tool",
"common.workflowAsToolDescription": "Als Tool in anderen Apps verwenden",
"common.workflowAsToolDisabledHint": "Veröffentlichen Sie den neuesten Workflow und stellen Sie sicher, dass ein verbundener User-Input-Knoten vorhanden ist, bevor Sie ihn als Werkzeug konfigurieren.",
"common.workflowAsToolReady": "Bereit",
"common.workflowAsToolReconfigure": "Neu konfigurieren",
"common.workflowAsToolTip": "Nach dem Workflow-Update ist eine Neukonfiguration des Tools erforderlich.",
"common.workflowAsToolUpdateNeeded": "Aktualisierung erforderlich",
"common.workflowProcess": "Arbeitsablauf",
"common.workflowProcessFailed": "Arbeitsablauf fehlgeschlagen",
"common.workflowProcessPaused": "Arbeitsablauf pausiert",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Nur dein",
"versionHistory.filter.reset": "Filter zurücksetzen",
"versionHistory.latest": "Neueste",
"versionHistory.nameIt": "Benennen",
"versionHistory.nameThisVersion": "Nennen Sie diese Version",
"versionHistory.releaseNotesPlaceholder": "Beschreibe, was sich geändert hat.",
"versionHistory.restorationTip": "Nach der Wiederherstellung der Version wird der aktuelle Entwurf überschrieben.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Custom Endpoints provide centralized API management, simplifying configuration for easy use across Dify's applications.",
"apiBasedExtension.type": "Type",
"apiBasedExtensionPage.description": "Extend Dify's module capabilities via API — add custom content moderation or external data tools to your apps.",
"appMenus.accessPoint": "Access Point",
"appMenus.annotations": "Annotations",
"appMenus.apiAccess": "API Access",
"appMenus.apiAccessTip": "This knowledge base is accessible via the Service API",
"appMenus.deploy": "Deploy",
"appMenus.logs": "Logs",
"appMenus.overview": "Monitoring",
"appMenus.promptEng": "Orchestrate",
+17 -2
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "You can now create file type variables in the start form. We will no longer support the image upload feature in the future. ",
"common.accessAPIReference": "Access API Reference",
"common.accessPointDescription": "Configure web app, API, MCP & Trigger",
"common.addBlock": "Add Node",
"common.addDescription": "Add description...",
"common.addFailureBranch": "Add Fail Branch",
@@ -160,7 +161,7 @@
"common.clipboardVersionCompatibilityWarning": "This content was copied from a different Dify app version. Some parts may be incompatible.",
"common.commentMode": "Comment Mode",
"common.configure": "Configure",
"common.configureRequired": "Configure Required",
"common.configureRequired": "Needs Setup",
"common.conversationLog": "Conversation Log",
"common.copy": "Copy",
"common.currentDraft": "Current Draft",
@@ -168,6 +169,7 @@
"common.currentView": "Current View",
"common.currentWorkflow": "Current Workflow",
"common.debugAndPreview": "Preview",
"common.deployDescription": "Push versions to environments",
"common.disconnect": "Disconnect",
"common.draftSaveFailed": "Draft save failed",
"common.duplicate": "Duplicate",
@@ -216,11 +218,15 @@
"common.needConnectTip": "This step is not connected to anything",
"common.needOutputNode": "The Output node must be added",
"common.needStartNode": "At least one start node must be added",
"common.noChanges": "No changes",
"common.noHistory": "No History",
"common.noVar": "No variable",
"common.notPublishedYet": "Not published yet",
"common.notRunning": "Not running yet",
"common.onFailure": "On Failure",
"common.openInExplore": "Open in Explore",
"common.openWebApp": "Open web app",
"common.openWebAppDescription": "Open the published app in a new tab",
"common.output": "Output",
"common.overwriteAndImport": "Overwrite and Import",
"common.parallel": "PARALLEL",
@@ -237,10 +243,12 @@
"common.processData": "Process Data",
"common.publish": "Publish",
"common.publishToMarketplace": "Publish to Marketplace",
"common.publishToMarketplaceDescription": "Share this app with the community",
"common.publishToMarketplaceFailed": "Failed to publish to Marketplace",
"common.publishUpdate": "Publish Update",
"common.published": "Published",
"common.publishedAt": "Published",
"common.publishedBy": "Published {{time}} by {{author}}",
"common.publishingToMarketplace": "Publishing...",
"common.redo": "Redo",
"common.restart": "Restart",
@@ -250,6 +258,7 @@
"common.runApp": "Run App",
"common.runHistory": "Run History",
"common.running": "Running",
"common.savedAt": "saved {{time}}",
"common.scheduleTriggerRunFailed": "Schedule trigger run failed",
"common.searchVar": "Search variable",
"common.setVarValuePlaceholder": "Set variable",
@@ -264,6 +273,7 @@
"common.tagBound": "Number of apps using this tag",
"common.undo": "Undo",
"common.unpublished": "Unpublished",
"common.unpublishedChanges": "Unpublished changes",
"common.update": "Update",
"common.variableNamePlaceholder": "Variable name",
"common.versionHistory": "Version History",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook debug failed",
"common.webhookDebugRequestFailed": "Webhook debug request failed",
"common.workflowAsTool": "Workflow as Tool",
"common.workflowAsToolDescription": "Use as a tool in other apps",
"common.workflowAsToolDisabledHint": "Publish the latest workflow and ensure a connected User Input node before configuring it as a tool.",
"common.workflowAsToolTip": "Tool reconfiguration is required after the workflow update.",
"common.workflowAsToolReady": "Ready",
"common.workflowAsToolReconfigure": "Reconfigure",
"common.workflowAsToolTip": "The workflow changed. Reconfigure to apply the updates.",
"common.workflowAsToolUpdateNeeded": "Update Needed",
"common.workflowProcess": "Workflow Process",
"common.workflowProcessFailed": "Workflow Process failed",
"common.workflowProcessPaused": "Workflow Process paused",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Only yours",
"versionHistory.filter.reset": "Reset Filter",
"versionHistory.latest": "Latest",
"versionHistory.nameIt": "Name it",
"versionHistory.nameThisVersion": "Name this version",
"versionHistory.releaseNotesPlaceholder": "Describe what changed",
"versionHistory.restorationTip": "After version restoration, the current draft will be overwritten.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Las extensiones basadas en API proporcionan una gestión centralizada de API, simplificando la configuración para su fácil uso en las aplicaciones de Dify.",
"apiBasedExtension.type": "Tipo",
"apiBasedExtensionPage.description": "Amplía las capacidades de los módulos de Dify mediante API: añade moderación de contenido personalizada o herramientas de datos externas a tus apps.",
"appMenus.accessPoint": "Punto de acceso",
"appMenus.annotations": "Anotaciones",
"appMenus.apiAccess": "Acceso API",
"appMenus.apiAccessTip": "Esta base de conocimiento es accesible a través de la API de servicio",
"appMenus.deploy": "Desplegar",
"appMenus.logs": "Registros",
"appMenus.overview": "Monitoreo",
"appMenus.promptEng": "Orquestar",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Responder",
"common.ImageUploadLegacyTip": "Ahora puede crear variables de tipo de archivo en el formulario de inicio. Ya no admitiremos la función de carga de imágenes en el futuro.",
"common.accessAPIReference": "Acceder a la referencia de la API",
"common.accessPointDescription": "Configurar la aplicación web, la API, MCP y los activadores",
"common.addBlock": "Agregar nodo",
"common.addDescription": "Agregar descripción...",
"common.addFailureBranch": "Agregar rama de error",
@@ -168,6 +169,7 @@
"common.currentView": "Vista actual",
"common.currentWorkflow": "Flujo de trabajo actual",
"common.debugAndPreview": "Vista previa",
"common.deployDescription": "Implementar versiones en entornos",
"common.disconnect": "Desconectar",
"common.draftSaveFailed": "Error al guardar el borrador",
"common.duplicate": "Duplicar",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Este paso no está conectado a nada",
"common.needOutputNode": "Se debe agregar el nodo de Salida",
"common.needStartNode": "Se debe añadir al menos un nodo de inicio",
"common.noChanges": "Sin cambios",
"common.noHistory": "Sin historia",
"common.noVar": "Sin variable",
"common.notPublishedYet": "Aún no publicado",
"common.notRunning": "Aún no se está ejecutando",
"common.onFailure": "Sobre el fracaso",
"common.openInExplore": "Abrir en Explorar",
"common.openWebApp": "Abrir aplicación web",
"common.openWebAppDescription": "Abrir la aplicación publicada en una pestaña nueva",
"common.output": "Salida",
"common.overwriteAndImport": "Sobrescribir e importar",
"common.parallel": "PARALELO",
@@ -237,10 +243,12 @@
"common.processData": "Procesar datos",
"common.publish": "Publicar",
"common.publishToMarketplace": "Publicar en Marketplace",
"common.publishToMarketplaceDescription": "Compartir esta aplicación con la comunidad",
"common.publishToMarketplaceFailed": "Error al publicar en Marketplace",
"common.publishUpdate": "Publicar actualización",
"common.published": "Publicado",
"common.publishedAt": "Publicado el",
"common.publishedBy": "Publicado {{time}} por {{author}}",
"common.publishingToMarketplace": "Publicando...",
"common.redo": "Rehacer",
"common.restart": "Reiniciar",
@@ -250,6 +258,7 @@
"common.runApp": "Ejecutar aplicación",
"common.runHistory": "Historial de ejecución",
"common.running": "Ejecutando",
"common.savedAt": "Guardado {{time}}",
"common.scheduleTriggerRunFailed": "Falló la ejecución del disparador programado",
"common.searchVar": "Buscar variable",
"common.setVarValuePlaceholder": "Establecer variable",
@@ -264,6 +273,7 @@
"common.tagBound": "Número de aplicaciones que utilizan esta etiqueta",
"common.undo": "Deshacer",
"common.unpublished": "No publicado",
"common.unpublishedChanges": "Cambios sin publicar",
"common.update": "Actualizar",
"common.variableNamePlaceholder": "Nombre de la variable",
"common.versionHistory": "Historial de versiones",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Falló la depuración del Webhook",
"common.webhookDebugRequestFailed": "Falló la solicitud de depuración del Webhook",
"common.workflowAsTool": "Flujo de trabajo como herramienta",
"common.workflowAsToolDescription": "Usarlo como herramienta en otras aplicaciones",
"common.workflowAsToolDisabledHint": "Publica el flujo de trabajo más reciente y asegúrate de que haya un nodo de Entrada de Usuario conectado antes de configurarlo como una herramienta.",
"common.workflowAsToolReady": "Listo",
"common.workflowAsToolReconfigure": "Reconfigurar",
"common.workflowAsToolTip": "Se requiere la reconfiguración de la herramienta después de la actualización del flujo de trabajo.",
"common.workflowAsToolUpdateNeeded": "Actualización necesaria",
"common.workflowProcess": "Proceso de flujo de trabajo",
"common.workflowProcessFailed": "Proceso de flujo de trabajo fallido",
"common.workflowProcessPaused": "Proceso de flujo de trabajo pausado",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Solo tuyo",
"versionHistory.filter.reset": "Restablecer filtro",
"versionHistory.latest": "Último",
"versionHistory.nameIt": "Nombrar",
"versionHistory.nameThisVersion": "Nombra esta versión",
"versionHistory.releaseNotesPlaceholder": "Describe lo que cambió",
"versionHistory.restorationTip": "Después de la restauración de la versión, el borrador actual será sobrescrito.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "افزونه‌های مبتنی بر API مدیریت متمرکز API را فراهم می‌کنند و پیکربندی را برای استفاده آسان در برنامه‌های Dify ساده می‌کنند.",
"apiBasedExtension.type": "نوع",
"apiBasedExtensionPage.description": "قابلیت‌های ماژول‌های Dify را از طریق API گسترش دهید — بازبینی محتوای سفارشی یا ابزارهای داده خارجی را به برنامه‌های خود اضافه کنید.",
"appMenus.accessPoint": "نقطه دسترسی",
"appMenus.annotations": "یادداشت‌ها",
"appMenus.apiAccess": "دسترسی API",
"appMenus.apiAccessTip": "این پایگاه دانش از طریق API سرویس قابل دسترسی است",
"appMenus.deploy": "استقرار",
"appMenus.logs": "گزارش‌ها",
"appMenus.overview": "نظارت",
"appMenus.promptEng": "هماهنگ‌سازی",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "پاسخ",
"common.ImageUploadLegacyTip": "اکنون می‌توانید متغیرهای نوع فایل را در فرم شروع ایجاد کنید. پشتیبانی از ویژگی قدیمی آپلود تصویر به‌زودی متوقف خواهد شد.",
"common.accessAPIReference": "دسترسی به مستندات API",
"common.accessPointDescription": "برنامه وب، API، MCP و محرک‌ها را پیکربندی کنید",
"common.addBlock": "افزودن گره",
"common.addDescription": "افزودن توضیحات...",
"common.addFailureBranch": "افزودن شاخه شکست",
@@ -168,6 +169,7 @@
"common.currentView": "نمای فعلی",
"common.currentWorkflow": "گردش کار فعلی",
"common.debugAndPreview": "اشکال‌زدایی و پیش‌نمایش",
"common.deployDescription": "نسخه‌ها را در محیط‌ها مستقر کنید",
"common.disconnect": "قطع اتصال",
"common.draftSaveFailed": "ذخیره پیش‌نویس ناموفق بود",
"common.duplicate": "تکثیر",
@@ -216,11 +218,15 @@
"common.needConnectTip": "این مرحله به هیچ گره‌ای متصل نیست",
"common.needOutputNode": "باید گره خروجی اضافه شود",
"common.needStartNode": "حداقل یک گره شروع باید اضافه شود",
"common.noChanges": "بدون تغییر",
"common.noHistory": "بدون تاریخچه",
"common.noVar": "بدون متغیر",
"common.notPublishedYet": "هنوز منتشر نشده",
"common.notRunning": "هنوز اجرا نشده",
"common.onFailure": "در صورت شکست",
"common.openInExplore": "باز کردن در کاوش",
"common.openWebApp": "باز کردن برنامه وب",
"common.openWebAppDescription": "برنامه منتشرشده را در یک زبانه جدید باز کنید",
"common.output": "خروجی",
"common.overwriteAndImport": "بازنویسی و وارد کردن",
"common.parallel": "موازی",
@@ -237,10 +243,12 @@
"common.processData": "پردازش داده‌ها",
"common.publish": "انتشار",
"common.publishToMarketplace": "انتشار در Marketplace",
"common.publishToMarketplaceDescription": "این برنامه را با جامعه به اشتراک بگذارید",
"common.publishToMarketplaceFailed": "انتشار در Marketplace ناموفق بود",
"common.publishUpdate": "انتشار به‌روزرسانی",
"common.published": "منتشر شده",
"common.publishedAt": "منتشر شده در",
"common.publishedBy": "منتشرشده {{time}} توسط {{author}}",
"common.publishingToMarketplace": "در حال انتشار...",
"common.redo": "بازانجام",
"common.restart": "راه‌اندازی مجدد",
@@ -250,6 +258,7 @@
"common.runApp": "اجرای برنامه",
"common.runHistory": "تاریخچه اجرا",
"common.running": "در حال اجرا",
"common.savedAt": "ذخیره شد {{time}}",
"common.scheduleTriggerRunFailed": "اجرای تریگر زمان‌بندی شده ناموفق بود",
"common.searchVar": "جستجوی متغیر",
"common.setVarValuePlaceholder": "تنظیم متغیر",
@@ -264,6 +273,7 @@
"common.tagBound": "تعداد برنامه‌هایی که از این برچسب استفاده می‌کنند",
"common.undo": "بازگردانی",
"common.unpublished": "منتشر نشده",
"common.unpublishedChanges": "تغییرات منتشرنشده",
"common.update": "به‌روزرسانی",
"common.variableNamePlaceholder": "نام متغیر",
"common.versionHistory": "تاریخچه نسخه",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "اشکال‌زدایی Webhook ناموفق بود",
"common.webhookDebugRequestFailed": "درخواست اشکال‌زدایی Webhook ناموفق بود",
"common.workflowAsTool": "گردش کار به عنوان ابزار",
"common.workflowAsToolDescription": "استفاده به‌عنوان ابزار در برنامه‌های دیگر",
"common.workflowAsToolDisabledHint": "برای تنظیم به عنوان ابزار، ابتدا گردش کار را منتشر کنید و مطمئن شوید که گره ورودی کاربر متصل است.",
"common.workflowAsToolReady": "آماده",
"common.workflowAsToolReconfigure": "پیکربندی مجدد",
"common.workflowAsToolTip": "پس از به‌روزرسانی گردش کار، پیکربندی مجدد ابزار الزامی است.",
"common.workflowAsToolUpdateNeeded": "نیاز به به‌روزرسانی",
"common.workflowProcess": "فرآیند گردش کار",
"common.workflowProcessFailed": "فرآیند گردش کار ناموفق بود",
"common.workflowProcessPaused": "فرآیند گردش کار متوقف شده است",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "فقط نسخه‌های شما",
"versionHistory.filter.reset": "بازنشانی فیلتر",
"versionHistory.latest": "آخرین",
"versionHistory.nameIt": "نام‌گذاری",
"versionHistory.nameThisVersion": "نام‌گذاری این نسخه",
"versionHistory.releaseNotesPlaceholder": "شرح دهید چه چیزی تغییر کرده است",
"versionHistory.restorationTip": "پس از بازیابی نسخه، پیش‌نویس فعلی بازنویسی خواهد شد.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Les extensions API fournissent une gestion centralisée des API, simplifiant la configuration pour une utilisation facile à travers les applications de Dify.",
"apiBasedExtension.type": "Tapez",
"apiBasedExtensionPage.description": "Étendez les capacités des modules de Dify via API — ajoutez une modération de contenu personnalisée ou des outils de données externes à vos apps.",
"appMenus.accessPoint": "Point daccès",
"appMenus.annotations": "Annotations",
"appMenus.apiAccess": "Accès API",
"appMenus.apiAccessTip": "Cette base de connaissances est accessible via l'API de service",
"appMenus.deploy": "Déployer",
"appMenus.logs": "Journaux",
"appMenus.overview": "Surveillance",
"appMenus.promptEng": "Orchestrer",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Répondre",
"common.ImageUploadLegacyTip": "Vous pouvez désormais créer des variables de type de fichier dans le formulaire de démarrage. À lavenir, nous ne prendrons plus en charge la fonctionnalité de téléchargement dimages.",
"common.accessAPIReference": "Accéder à la référence API",
"common.accessPointDescription": "Configurer lapplication web, lAPI, MCP et les déclencheurs",
"common.addBlock": "Ajouter un nœud",
"common.addDescription": "Ajouter une description...",
"common.addFailureBranch": "Ajouter une branche d’échec",
@@ -168,6 +169,7 @@
"common.currentView": "Vue actuelle",
"common.currentWorkflow": "Flux de travail actuel",
"common.debugAndPreview": "Aperçu",
"common.deployDescription": "Déployer les versions dans les environnements",
"common.disconnect": "Déconnecter",
"common.draftSaveFailed": "Échec de lenregistrement du brouillon",
"common.duplicate": "Dupliquer",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Cette étape n'est connectée à rien",
"common.needOutputNode": "Le nœud de sortie doit être ajouté",
"common.needStartNode": "Au moins un nœud de départ doit être ajouté",
"common.noChanges": "Aucune modification",
"common.noHistory": "Pas dhistoire",
"common.noVar": "Pas de variable",
"common.notPublishedYet": "Pas encore publié",
"common.notRunning": "Pas encore en cours d'exécution",
"common.onFailure": "Sur l’échec",
"common.openInExplore": "Ouvrir dans Explorer",
"common.openWebApp": "Ouvrir lapplication web",
"common.openWebAppDescription": "Ouvrir lapplication publiée dans un nouvel onglet",
"common.output": "Sortie",
"common.overwriteAndImport": "Écraser et importer",
"common.parallel": "PARALLÈLE",
@@ -237,10 +243,12 @@
"common.processData": "Traiter les données",
"common.publish": "Publier",
"common.publishToMarketplace": "Publier sur le Marketplace",
"common.publishToMarketplaceDescription": "Partager cette application avec la communauté",
"common.publishToMarketplaceFailed": "Échec de la publication sur le Marketplace",
"common.publishUpdate": "Publier une mise à jour",
"common.published": "Publié",
"common.publishedAt": "Publié le",
"common.publishedBy": "Publié {{time}} par {{author}}",
"common.publishingToMarketplace": "Publication en cours...",
"common.redo": "Réexécuter",
"common.restart": "Redémarrer",
@@ -250,6 +258,7 @@
"common.runApp": "Exécuter l'application",
"common.runHistory": "Historique des exécutions",
"common.running": "En cours d'exécution",
"common.savedAt": "Enregistré {{time}}",
"common.scheduleTriggerRunFailed": "Échec de l'exécution du déclencheur planifié",
"common.searchVar": "Rechercher une variable",
"common.setVarValuePlaceholder": "Définir la valeur de la variable",
@@ -264,6 +273,7 @@
"common.tagBound": "Nombre d'applications utilisant cette étiquette",
"common.undo": "Défaire",
"common.unpublished": "Non publié",
"common.unpublishedChanges": "Modifications non publiées",
"common.update": "Mettre à jour",
"common.variableNamePlaceholder": "Nom de la variable",
"common.versionHistory": "Historique des versions",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Échec du débogage du Webhook",
"common.webhookDebugRequestFailed": "Échec de la demande de débogage du Webhook",
"common.workflowAsTool": "Flux de travail en tant qu'outil",
"common.workflowAsToolDescription": "Lutiliser comme outil dans dautres applications",
"common.workflowAsToolDisabledHint": "Publiez le dernier flux de travail et assurez-vous qu'un nœud d'entrée utilisateur est connecté avant de le configurer comme outil.",
"common.workflowAsToolReady": "Prêt",
"common.workflowAsToolReconfigure": "Reconfigurer",
"common.workflowAsToolTip": "Reconfiguration de l'outil requise après la mise à jour du flux de travail.",
"common.workflowAsToolUpdateNeeded": "Mise à jour requise",
"common.workflowProcess": "Processus de flux de travail",
"common.workflowProcessFailed": "Processus de flux de travail échoué",
"common.workflowProcessPaused": "Processus de flux de travail en pause",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Rien que le tien",
"versionHistory.filter.reset": "Réinitialiser le filtre",
"versionHistory.latest": "Dernier",
"versionHistory.nameIt": "Nommer",
"versionHistory.nameThisVersion": "Nommez cette version",
"versionHistory.releaseNotesPlaceholder": "Décrivez ce qui a changé",
"versionHistory.restorationTip": "Après la restauration de la version, le brouillon actuel sera écrasé.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "एपीआई एक्सटेंशन केंद्रीकृत एपीआई प्रबंधन प्रदान करते हैं, जो Dify के अनुप्रयोगों में आसान उपयोग के लिए कॉन्फ़िगरेशन को सरल बनाते हैं।",
"apiBasedExtension.type": "प्रकार",
"apiBasedExtensionPage.description": "API के ज़रिये Dify की मॉड्यूल क्षमताओं को बढ़ाएँ — अपने ऐप्स में कस्टम कंटेंट मॉडरेशन या बाहरी डेटा टूल जोड़ें।",
"appMenus.accessPoint": "एक्सेस पॉइंट",
"appMenus.annotations": "एनोटेशन",
"appMenus.apiAccess": "API एक्सेस",
"appMenus.apiAccessTip": "यह ज्ञान आधार सेवा API के माध्यम से सुलभ है",
"appMenus.deploy": "डिप्लॉय करें",
"appMenus.logs": "लॉग्स",
"appMenus.overview": "निगरानी",
"appMenus.promptEng": "समन्वय करें",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "जवाब दें",
"common.ImageUploadLegacyTip": "अब आप प्रारंभ प्रपत्र में फ़ाइल प्रकार चर बना सकते हैं। हम अब भविष्य में छवि अपलोड सुविधा का समर्थन नहीं करेंगे।",
"common.accessAPIReference": "एपीआई संदर्भ तक पहुंचें",
"common.accessPointDescription": "वेब ऐप, API, MCP और ट्रिगर कॉन्फ़िगर करें",
"common.addBlock": "नोड जोड़ें",
"common.addDescription": "विवरण जोड़ें...",
"common.addFailureBranch": "असफल शाखा जोड़ें",
@@ -168,6 +169,7 @@
"common.currentView": "वर्तमान दृश्य",
"common.currentWorkflow": "वर्तमान कार्यप्रवाह",
"common.debugAndPreview": "पूर्वावलोकन",
"common.deployDescription": "वर्ज़न को एनवायरनमेंट में डिप्लॉय करें",
"common.disconnect": "अलग करना",
"common.draftSaveFailed": "ड्राफ़्ट सहेजना विफल रहा",
"common.duplicate": "डुप्लिकेट करें",
@@ -216,11 +218,15 @@
"common.needConnectTip": "यह चरण किसी से जुड़ा नहीं है",
"common.needOutputNode": "आउटपुट नोड जोड़ा जाना चाहिए",
"common.needStartNode": "कम से कम एक प्रारंभ नोड जोड़ा जाना चाहिए",
"common.noChanges": "कोई बदलाव नहीं",
"common.noHistory": "कोई इतिहास नहीं",
"common.noVar": "कोई वेरिएबल नहीं",
"common.notPublishedYet": "अभी तक प्रकाशित नहीं",
"common.notRunning": "अभी तक नहीं चल रहा",
"common.onFailure": "असफलता पर",
"common.openInExplore": "एक्सप्लोर में खोलें",
"common.openWebApp": "वेब ऐप खोलें",
"common.openWebAppDescription": "प्रकाशित ऐप को नए टैब में खोलें",
"common.output": "आउटपुट",
"common.overwriteAndImport": "अधिलेखित और आयात",
"common.parallel": "समानांतर",
@@ -237,10 +243,12 @@
"common.processData": "डेटा प्रोसेस करें",
"common.publish": "प्रकाशित करें",
"common.publishToMarketplace": "Marketplace पर प्रकाशित करें",
"common.publishToMarketplaceDescription": "इस ऐप को समुदाय के साथ साझा करें",
"common.publishToMarketplaceFailed": "Marketplace पर प्रकाशित करने में विफल",
"common.publishUpdate": "अपडेट प्रकाशित करें",
"common.published": "प्रकाशित",
"common.publishedAt": "प्रकाशित",
"common.publishedBy": "{{author}} द्वारा {{time}} प्रकाशित",
"common.publishingToMarketplace": "प्रकाशित हो रहा है...",
"common.redo": "फिर से करें",
"common.restart": "पुनः आरंभ करें",
@@ -250,6 +258,7 @@
"common.runApp": "ऐप चलाएं",
"common.runHistory": "रन इतिहास",
"common.running": "चल रहा है",
"common.savedAt": "{{time}} सहेजा गया",
"common.scheduleTriggerRunFailed": "शेड्यूल ट्रिगर रन विफल",
"common.searchVar": "वेरिएबल खोजें",
"common.setVarValuePlaceholder": "वेरिएबल सेट करें",
@@ -264,6 +273,7 @@
"common.tagBound": "इस टैग का उपयोग करने वाले ऐप्स की संख्या",
"common.undo": "पूर्ववत करें",
"common.unpublished": "अप्रकाशित",
"common.unpublishedChanges": "अप्रकाशित परिवर्तन",
"common.update": "अपडेट करें",
"common.variableNamePlaceholder": "वेरिएबल नाम",
"common.versionHistory": "संस्करण इतिहास",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook डीबग विफल",
"common.webhookDebugRequestFailed": "Webhook डीबग अनुरोध विफल",
"common.workflowAsTool": "टूल के रूप में कार्यप्रवाह",
"common.workflowAsToolDescription": "इसे अन्य ऐप्स में टूल के रूप में उपयोग करें",
"common.workflowAsToolDisabledHint": "सबसे नया वर्कफ़्लो प्रकाशित करें और इसे टूल के रूप में कॉन्फ़िगर करने से पहले एक कनेक्टेड यूज़र इनपुट नोड सुनिश्चित करें।",
"common.workflowAsToolReady": "तैयार",
"common.workflowAsToolReconfigure": "फिर से कॉन्फ़िगर करें",
"common.workflowAsToolTip": "कार्यप्रवाह अपडेट के बाद टूल पुनः कॉन्फ़िगरेशन आवश्यक है।",
"common.workflowAsToolUpdateNeeded": "अपडेट आवश्यक",
"common.workflowProcess": "कार्यप्रवाह प्रक्रिया",
"common.workflowProcessFailed": "कार्यप्रवाह प्रक्रिया विफल रही",
"common.workflowProcessPaused": "कार्यप्रवाह प्रक्रिया रुकी हुई है",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "बस तुम्हारा",
"versionHistory.filter.reset": "फिल्टर रीसेट करें",
"versionHistory.latest": "लेटेस्ट",
"versionHistory.nameIt": "नाम दें",
"versionHistory.nameThisVersion": "इस संस्करण का नाम दें",
"versionHistory.releaseNotesPlaceholder": "बताइए कि क्या बदला",
"versionHistory.restorationTip": "संस्करण पुनर्स्थापन के बाद, वर्तमान ड्राफ्ट अधिलेखित किया जाएगा।",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Ekstensi API menyediakan manajemen API terpusat, menyederhanakan konfigurasi agar mudah digunakan di seluruh aplikasi Dify.",
"apiBasedExtension.type": "Jenis",
"apiBasedExtensionPage.description": "Perluas kemampuan modul Dify melalui API — tambahkan moderasi konten khusus atau alat data eksternal ke aplikasi Anda.",
"appMenus.accessPoint": "Titik Akses",
"appMenus.annotations": "Anotasi",
"appMenus.apiAccess": "Akses API",
"appMenus.apiAccessTip": "Basis pengetahuan ini dapat diakses melalui API layanan",
"appMenus.deploy": "Terapkan",
"appMenus.logs": "Log",
"appMenus.overview": "Pemantauan",
"appMenus.promptEng": "Mengatur",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Balas",
"common.ImageUploadLegacyTip": "Anda sekarang dapat membuat variabel jenis file di formulir awal. Kami tidak akan lagi mendukung fitur unggah gambar di masa mendatang.",
"common.accessAPIReference": "Referensi API Akses",
"common.accessPointDescription": "Konfigurasikan aplikasi web, API, MCP & pemicu",
"common.addBlock": "Tambahkan Node",
"common.addDescription": "Tambahkan deskripsi...",
"common.addFailureBranch": "Tambahkan Cabang Gagal",
@@ -168,6 +169,7 @@
"common.currentView": "Tampilan Saat Ini",
"common.currentWorkflow": "Alur Kerja Saat Ini",
"common.debugAndPreview": "Pratayang",
"common.deployDescription": "Deploy versi ke lingkungan",
"common.disconnect": "Lepaskan",
"common.draftSaveFailed": "Gagal menyimpan draf",
"common.duplicate": "Duplikat",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Langkah ini tidak terhubung ke apa pun",
"common.needOutputNode": "Node Output harus ditambahkan",
"common.needStartNode": "Setidaknya satu node awal harus ditambahkan",
"common.noChanges": "Tidak ada perubahan",
"common.noHistory": "Tidak Ada Sejarah",
"common.noVar": "Tidak ada variabel",
"common.notPublishedYet": "Belum diterbitkan",
"common.notRunning": "Belum berjalan",
"common.onFailure": "Pada Kegagalan",
"common.openInExplore": "Buka di Jelajahi",
"common.openWebApp": "Buka aplikasi web",
"common.openWebAppDescription": "Buka aplikasi yang dipublikasikan di tab baru",
"common.output": "Hasil",
"common.overwriteAndImport": "Menimpa dan Mengimpor",
"common.parallel": "SEJAJAR",
@@ -237,10 +243,12 @@
"common.processData": "Proses Data",
"common.publish": "Menerbitkan",
"common.publishToMarketplace": "Publikasikan ke Marketplace",
"common.publishToMarketplaceDescription": "Bagikan aplikasi ini dengan komunitas",
"common.publishToMarketplaceFailed": "Gagal mempublikasikan ke Marketplace",
"common.publishUpdate": "Publikasikan Pembaruan",
"common.published": "Diterbitkan",
"common.publishedAt": "Diterbitkan",
"common.publishedBy": "Diterbitkan {{time}} oleh {{author}}",
"common.publishingToMarketplace": "Mempublikasikan...",
"common.redo": "Ulangi",
"common.restart": "Restart",
@@ -250,6 +258,7 @@
"common.runApp": "Jalankan Aplikasi",
"common.runHistory": "Jalankan Riwayat",
"common.running": "Menjalankan",
"common.savedAt": "Disimpan {{time}}",
"common.scheduleTriggerRunFailed": "Jalankan trigger terjadwal gagal",
"common.searchVar": "Variabel pencarian",
"common.setVarValuePlaceholder": "Atur variabel",
@@ -264,6 +273,7 @@
"common.tagBound": "Jumlah aplikasi yang menggunakan tag ini",
"common.undo": "Urungkan",
"common.unpublished": "Tidak diterbitkan",
"common.unpublishedChanges": "Perubahan belum diterbitkan",
"common.update": "Pemutakhiran",
"common.variableNamePlaceholder": "Nama variabel",
"common.versionHistory": "Riwayat versi",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Debug Webhook gagal",
"common.webhookDebugRequestFailed": "Permintaan debug Webhook gagal",
"common.workflowAsTool": "Alur Kerja sebagai Alat",
"common.workflowAsToolDescription": "Gunakan sebagai alat di aplikasi lain",
"common.workflowAsToolDisabledHint": "Terbitkan alur kerja terbaru dan pastikan ada node Input Pengguna yang terhubung sebelum mengonfigurasinya sebagai alat.",
"common.workflowAsToolReady": "Siap",
"common.workflowAsToolReconfigure": "Konfigurasi ulang",
"common.workflowAsToolTip": "Konfigurasi ulang alat diperlukan setelah pembaruan alur kerja.",
"common.workflowAsToolUpdateNeeded": "Perlu diperbarui",
"common.workflowProcess": "Proses Alur Kerja",
"common.workflowProcessFailed": "Proses Alur Kerja gagal",
"common.workflowProcessPaused": "Proses Alur Kerja dijeda",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Hanya milik Anda",
"versionHistory.filter.reset": "Atur Ulang Filter",
"versionHistory.latest": "Terbaru",
"versionHistory.nameIt": "Beri nama",
"versionHistory.nameThisVersion": "Beri nama versi ini",
"versionHistory.releaseNotesPlaceholder": "Menjelaskan apa yang berubah",
"versionHistory.restorationTip": "Setelah pemulihan versi, draf saat ini akan ditimpa.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Le estensioni API forniscono una gestione centralizzata delle API, semplificando la configurazione per un facile utilizzo nelle applicazioni di Dify.",
"apiBasedExtension.type": "Tipo",
"apiBasedExtensionPage.description": "Estendi le capacità dei moduli di Dify tramite API — aggiungi moderazione dei contenuti personalizzata o strumenti dati esterni alle tue app.",
"appMenus.accessPoint": "Punto di accesso",
"appMenus.annotations": "Annotazioni",
"appMenus.apiAccess": "Accesso API",
"appMenus.apiAccessTip": "Questa base di conoscenza è accessibile tramite l'API del servizio",
"appMenus.deploy": "Distribuisci",
"appMenus.logs": "Log",
"appMenus.overview": "Monitoraggio",
"appMenus.promptEng": "Orchestrazione",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Rispondi",
"common.ImageUploadLegacyTip": "Ora è possibile creare variabili di tipo file nel modulo iniziale. In futuro non supporteremo più la funzione di caricamento delle immagini.",
"common.accessAPIReference": "Accedi alla Riferimento API",
"common.accessPointDescription": "Configura app web, API, MCP e trigger",
"common.addBlock": "Aggiungi nodo",
"common.addDescription": "Aggiungi descrizione...",
"common.addFailureBranch": "Aggiungi ramo non riuscito",
@@ -168,6 +169,7 @@
"common.currentView": "Vista corrente",
"common.currentWorkflow": "Flusso di lavoro corrente",
"common.debugAndPreview": "Anteprima",
"common.deployDescription": "Distribuisci le versioni negli ambienti",
"common.disconnect": "Disconnettere",
"common.draftSaveFailed": "Salvataggio della bozza non riuscito",
"common.duplicate": "Duplica",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Questo passaggio non è collegato a nulla",
"common.needOutputNode": "Deve essere aggiunto il nodo di uscita",
"common.needStartNode": "Deve essere aggiunto almeno un nodo iniziale",
"common.noChanges": "Nessuna modifica",
"common.noHistory": "Nessuna storia",
"common.noVar": "Nessuna variabile",
"common.notPublishedYet": "Non ancora pubblicato",
"common.notRunning": "Non ancora in esecuzione",
"common.onFailure": "In caso di guasto",
"common.openInExplore": "Apri in Esplora",
"common.openWebApp": "Apri app web",
"common.openWebAppDescription": "Apri lapp pubblicata in una nuova scheda",
"common.output": "Output",
"common.overwriteAndImport": "Sovrascrivi e Importa",
"common.parallel": "PARALLELO",
@@ -237,10 +243,12 @@
"common.processData": "Elabora Dati",
"common.publish": "Pubblica",
"common.publishToMarketplace": "Pubblica sul Marketplace",
"common.publishToMarketplaceDescription": "Condividi questa app con la community",
"common.publishToMarketplaceFailed": "Pubblicazione sul Marketplace non riuscita",
"common.publishUpdate": "Pubblica aggiornamento",
"common.published": "Pubblicato",
"common.publishedAt": "Pubblicato",
"common.publishedBy": "Pubblicato {{time}} da {{author}}",
"common.publishingToMarketplace": "Pubblicazione...",
"common.redo": "Ripeti",
"common.restart": "Riavvia",
@@ -250,6 +258,7 @@
"common.runApp": "Esegui App",
"common.runHistory": "Cronologia esecuzioni",
"common.running": "In esecuzione",
"common.savedAt": "Salvato {{time}}",
"common.scheduleTriggerRunFailed": "Esecuzione del trigger pianificato non riuscita",
"common.searchVar": "Cerca variabile",
"common.setVarValuePlaceholder": "Imposta variabile",
@@ -264,6 +273,7 @@
"common.tagBound": "Numero di app che utilizzano questo tag",
"common.undo": "Annulla",
"common.unpublished": "Non pubblicato",
"common.unpublishedChanges": "Modifiche non pubblicate",
"common.update": "Aggiorna",
"common.variableNamePlaceholder": "Nome variabile",
"common.versionHistory": "Cronologia delle versioni",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Debug del Webhook non riuscito",
"common.webhookDebugRequestFailed": "Richiesta di debug del Webhook non riuscita",
"common.workflowAsTool": "Flusso di lavoro come Strumento",
"common.workflowAsToolDescription": "Usalo come strumento in altre app",
"common.workflowAsToolDisabledHint": "Pubblica il flusso di lavoro più recente e assicurati che ci sia un nodo di Input Utente collegato prima di configurarlo come strumento.",
"common.workflowAsToolReady": "Pronto",
"common.workflowAsToolReconfigure": "Riconfigura",
"common.workflowAsToolTip": "È richiesta una nuova configurazione dello strumento dopo l'aggiornamento del flusso di lavoro.",
"common.workflowAsToolUpdateNeeded": "Aggiornamento necessario",
"common.workflowProcess": "Processo di flusso di lavoro",
"common.workflowProcessFailed": "Processo di flusso di lavoro non riuscito",
"common.workflowProcessPaused": "Processo di flusso di lavoro in pausa",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Solo tuo",
"versionHistory.filter.reset": "Ripristina filtro",
"versionHistory.latest": "Ultimo",
"versionHistory.nameIt": "Assegna un nome",
"versionHistory.nameThisVersion": "Chiamare questa versione",
"versionHistory.releaseNotesPlaceholder": "Descrivi cosa è cambiato",
"versionHistory.restorationTip": "Dopo il ripristino della versione, la bozza attuale verrà sovrascritta.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API 拡張機能は、Dify のアプリケーション全体での簡単な使用のための設定を簡素化し、集中的な API 管理を提供します。",
"apiBasedExtension.type": "タイプ",
"apiBasedExtensionPage.description": "API を通じて Dify のモジュール機能を拡張——カスタムのコンテンツモデレーションや外部データツールをアプリに追加できます。",
"appMenus.accessPoint": "アクセスポイント",
"appMenus.annotations": "注釈",
"appMenus.apiAccess": "API アクセス",
"appMenus.apiAccessTip": "このナレッジベースはサービスAPIを介してアクセス可能です",
"appMenus.deploy": "デプロイ",
"appMenus.logs": "ログ",
"appMenus.overview": "監視",
"appMenus.promptEng": "オーケストレート",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "返信",
"common.ImageUploadLegacyTip": "開始フォームでファイル型変数が作成可能になりました。画像アップロード機能は今後サポート終了となります。",
"common.accessAPIReference": "API リファレンス",
"common.accessPointDescription": "Web アプリ、API、MCP、トリガーを設定します",
"common.addBlock": "ブロックを追加",
"common.addDescription": "説明を追加...",
"common.addFailureBranch": "失敗ブランチを追加",
@@ -168,6 +169,7 @@
"common.currentView": "現在のビュー",
"common.currentWorkflow": "現在のワークフロー",
"common.debugAndPreview": "プレビュー",
"common.deployDescription": "バージョンを環境にデプロイします",
"common.disconnect": "接続解除",
"common.draftSaveFailed": "下書きの保存に失敗しました",
"common.duplicate": "複製",
@@ -216,11 +218,15 @@
"common.needConnectTip": "接続されていないステップがあります",
"common.needOutputNode": "出力ノードを追加する必要があります",
"common.needStartNode": "少なくとも1つのスタートノードを追加する必要があります",
"common.noChanges": "変更なし",
"common.noHistory": "履歴がありません",
"common.noVar": "変数がありません",
"common.notPublishedYet": "まだ公開されていません",
"common.notRunning": "まだ実行されていません",
"common.onFailure": "失敗時",
"common.openInExplore": "探索ページで開く",
"common.openWebApp": "Web アプリを開く",
"common.openWebAppDescription": "公開済みアプリを新しいタブで開きます",
"common.output": "出力",
"common.overwriteAndImport": "上書きしてインポート",
"common.parallel": "並列",
@@ -237,10 +243,12 @@
"common.processData": "データ処理",
"common.publish": "公開する",
"common.publishToMarketplace": "マーケットプレイスに公開",
"common.publishToMarketplaceDescription": "このアプリをコミュニティと共有します",
"common.publishToMarketplaceFailed": "マーケットプレイスへの公開に失敗しました",
"common.publishUpdate": "更新を公開",
"common.published": "公開済み",
"common.publishedAt": "公開日時",
"common.publishedBy": "{{time}}に{{author}}が公開",
"common.publishingToMarketplace": "公開中...",
"common.redo": "やり直し",
"common.restart": "再起動",
@@ -250,6 +258,7 @@
"common.runApp": "アプリを実行",
"common.runHistory": "実行履歴",
"common.running": "実行中",
"common.savedAt": "{{time}} に保存",
"common.scheduleTriggerRunFailed": "スケジュールトリガーの実行に失敗しました",
"common.searchVar": "変数を検索",
"common.setVarValuePlaceholder": "変数値を設定",
@@ -264,6 +273,7 @@
"common.tagBound": "このタグを使用しているアプリの数",
"common.undo": "元に戻す",
"common.unpublished": "未公開",
"common.unpublishedChanges": "未公開の変更",
"common.update": "更新",
"common.variableNamePlaceholder": "変数名を入力",
"common.versionHistory": "バージョン履歴",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhookデバッグに失敗しました",
"common.webhookDebugRequestFailed": "Webhookデバッグリクエストに失敗しました",
"common.workflowAsTool": "ワークフローをツールとして利用",
"common.workflowAsToolDescription": "他のアプリでツールとして使用します",
"common.workflowAsToolDisabledHint": "最新のワークフローを公開し、接続済みの User Input ノードを用意してからツールとして設定してください。",
"common.workflowAsToolReady": "準備完了",
"common.workflowAsToolReconfigure": "再設定",
"common.workflowAsToolTip": "ワークフロー更新後はツールの再設定が必要です",
"common.workflowAsToolUpdateNeeded": "更新が必要",
"common.workflowProcess": "ワークフロー処理",
"common.workflowProcessFailed": "ワークフロー処理が失敗しました",
"common.workflowProcessPaused": "ワークフロー処理は一時停止中です",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "自分のみ",
"versionHistory.filter.reset": "リセット",
"versionHistory.latest": "最新版",
"versionHistory.nameIt": "名前を付ける",
"versionHistory.nameThisVersion": "バージョン名を付ける",
"versionHistory.releaseNotesPlaceholder": "変更内容を入力してください",
"versionHistory.restorationTip": "バージョンを復元すると、現在の下書きが上書きされます",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API 기반 확장은 Dify 애플리케이션 전체에서 간편한 사용을 위한 설정을 단순화하고 집중적인 API 관리를 제공합니다.",
"apiBasedExtension.type": "유형",
"apiBasedExtensionPage.description": "API를 통해 Dify의 모듈 기능을 확장하고, 앱에 맞춤형 콘텐츠 검수 또는 외부 데이터 도구를 추가하세요.",
"appMenus.accessPoint": "액세스 지점",
"appMenus.annotations": "어노테이션",
"appMenus.apiAccess": "API 액세스",
"appMenus.apiAccessTip": "이 지식 베이스는 서비스 API를 통해 액세스할 수 있습니다",
"appMenus.deploy": "배포",
"appMenus.logs": "로그",
"appMenus.overview": "모니터링",
"appMenus.promptEng": "오케스트레이트",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "답글",
"common.ImageUploadLegacyTip": "이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.",
"common.accessAPIReference": "API 참조 접근",
"common.accessPointDescription": "웹 앱, API, MCP 및 트리거를 구성합니다",
"common.addBlock": "노드 추가",
"common.addDescription": "설명 추가...",
"common.addFailureBranch": "실패 분기 추가",
@@ -168,6 +169,7 @@
"common.currentView": "현재 보기",
"common.currentWorkflow": "현재 워크플로",
"common.debugAndPreview": "미리보기",
"common.deployDescription": "버전을 환경에 배포합니다",
"common.disconnect": "연결 해제",
"common.draftSaveFailed": "초안 저장에 실패했습니다",
"common.duplicate": "복제",
@@ -216,11 +218,15 @@
"common.needConnectTip": "이 단계는 아무것도 연결되어 있지 않습니다",
"common.needOutputNode": "출력 노드를 추가해야 합니다",
"common.needStartNode": "적어도 하나의 시작 노드를 추가해야 합니다",
"common.noChanges": "변경 사항 없음",
"common.noHistory": "이력 없음",
"common.noVar": "변수 없음",
"common.notPublishedYet": "아직 게시되지 않음",
"common.notRunning": "아직 실행되지 않음",
"common.onFailure": "실패 시",
"common.openInExplore": "Explore 에서 열기",
"common.openWebApp": "웹 앱 열기",
"common.openWebAppDescription": "게시된 앱을 새 탭에서 엽니다",
"common.output": "출력",
"common.overwriteAndImport": "덮어쓰기 및 가져오기",
"common.parallel": "병렬",
@@ -237,10 +243,12 @@
"common.processData": "데이터 처리",
"common.publish": "게시하기",
"common.publishToMarketplace": "마켓플레이스에 게시",
"common.publishToMarketplaceDescription": "이 앱을 커뮤니티와 공유합니다",
"common.publishToMarketplaceFailed": "마켓플레이스 게시 실패",
"common.publishUpdate": "업데이트 게시",
"common.published": "게시됨",
"common.publishedAt": "발행일",
"common.publishedBy": "{{author}}님이 {{time}}에 게시",
"common.publishingToMarketplace": "게시 중...",
"common.redo": "다시 실행",
"common.restart": "재시작",
@@ -250,6 +258,7 @@
"common.runApp": "앱 실행",
"common.runHistory": "실행 기록",
"common.running": "실행 중",
"common.savedAt": "{{time}}에 저장",
"common.scheduleTriggerRunFailed": "예약된 트리거 실행 실패",
"common.searchVar": "변수 검색",
"common.setVarValuePlaceholder": "변수 값 설정",
@@ -264,6 +273,7 @@
"common.tagBound": "이 태그를 사용하는 앱 수",
"common.undo": "실행 취소",
"common.unpublished": "게시되지 않음",
"common.unpublishedChanges": "게시되지 않은 변경 사항",
"common.update": "업데이트",
"common.variableNamePlaceholder": "변수 이름",
"common.versionHistory": "버전 기록",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook 디버그 실패",
"common.webhookDebugRequestFailed": "Webhook 디버그 요청 실패",
"common.workflowAsTool": "도구로서의 워크플로우",
"common.workflowAsToolDescription": "다른 앱에서 도구로 사용합니다",
"common.workflowAsToolDisabledHint": "최신 워크플로를 게시하고 도구로 구성하기 전에 연결된 사용자 입력 노드가 있는지 확인하세요.",
"common.workflowAsToolReady": "준비됨",
"common.workflowAsToolReconfigure": "재구성",
"common.workflowAsToolTip": "워크플로우 업데이트 후 도구 재구성이 필요합니다.",
"common.workflowAsToolUpdateNeeded": "업데이트 필요",
"common.workflowProcess": "워크플로우 과정",
"common.workflowProcessFailed": "워크플로우 과정 실패",
"common.workflowProcessPaused": "워크플로우 과정 일시 중지됨",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "내 버전만",
"versionHistory.filter.reset": "필터 재설정",
"versionHistory.latest": "최신",
"versionHistory.nameIt": "이름 지정",
"versionHistory.nameThisVersion": "이름 바꾸기",
"versionHistory.releaseNotesPlaceholder": "변경된 내용을 설명하세요.",
"versionHistory.restorationTip": "버전 복원 후 현재 초안이 덮어쓰여질 것입니다.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API extensions provide centralized API management, simplifying configuration for easy use across Dify's applications.",
"apiBasedExtension.type": "Type",
"apiBasedExtensionPage.description": "Breid de modulemogelijkheden van Dify uit via API — voeg aangepaste contentmoderatie of externe datatools toe aan je apps.",
"appMenus.accessPoint": "Toegangspunt",
"appMenus.annotations": "Annotations",
"appMenus.apiAccess": "API Access",
"appMenus.apiAccessTip": "This knowledge base is accessible via the Service API",
"appMenus.deploy": "Implementeren",
"appMenus.logs": "Logs",
"appMenus.overview": "Monitoring",
"appMenus.promptEng": "Orchestrate",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Antwoord",
"common.ImageUploadLegacyTip": "You can now create file type variables in the start form. We will no longer support the image upload feature in the future. ",
"common.accessAPIReference": "Access API Reference",
"common.accessPointDescription": "Webapp, API, MCP en triggers configureren",
"common.addBlock": "Add Node",
"common.addDescription": "Add description...",
"common.addFailureBranch": "Add Fail Branch",
@@ -168,6 +169,7 @@
"common.currentView": "Current View",
"common.currentWorkflow": "Current Workflow",
"common.debugAndPreview": "Preview",
"common.deployDescription": "Versies naar omgevingen implementeren",
"common.disconnect": "Disconnect",
"common.draftSaveFailed": "Opslaan van concept mislukt",
"common.duplicate": "Duplicate",
@@ -216,11 +218,15 @@
"common.needConnectTip": "This step is not connected to anything",
"common.needOutputNode": "The Output node must be added",
"common.needStartNode": "At least one start node must be added",
"common.noChanges": "Geen wijzigingen",
"common.noHistory": "No History",
"common.noVar": "No variable",
"common.notPublishedYet": "Nog niet gepubliceerd",
"common.notRunning": "Not running yet",
"common.onFailure": "On Failure",
"common.openInExplore": "Open in Explore",
"common.openWebApp": "Webapp openen",
"common.openWebAppDescription": "De gepubliceerde app in een nieuw tabblad openen",
"common.output": "Output",
"common.overwriteAndImport": "Overwrite and Import",
"common.parallel": "PARALLEL",
@@ -237,10 +243,12 @@
"common.processData": "Process Data",
"common.publish": "Publish",
"common.publishToMarketplace": "Publiceren op Marketplace",
"common.publishToMarketplaceDescription": "Deze app delen met de community",
"common.publishToMarketplaceFailed": "Publiceren op Marketplace mislukt",
"common.publishUpdate": "Publish Update",
"common.published": "Published",
"common.publishedAt": "Published",
"common.publishedBy": "{{time}} gepubliceerd door {{author}}",
"common.publishingToMarketplace": "Publiceren...",
"common.redo": "Redo",
"common.restart": "Restart",
@@ -250,6 +258,7 @@
"common.runApp": "Run App",
"common.runHistory": "Run History",
"common.running": "Running",
"common.savedAt": "Opgeslagen {{time}}",
"common.scheduleTriggerRunFailed": "Geplande trigger-uitvoering mislukt",
"common.searchVar": "Search variable",
"common.setVarValuePlaceholder": "Set variable",
@@ -264,6 +273,7 @@
"common.tagBound": "Number of apps using this tag",
"common.undo": "Undo",
"common.unpublished": "Unpublished",
"common.unpublishedChanges": "Niet-gepubliceerde wijzigingen",
"common.update": "Update",
"common.variableNamePlaceholder": "Variable name",
"common.versionHistory": "Version History",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook-debugging mislukt",
"common.webhookDebugRequestFailed": "Webhook-debugverzoek mislukt",
"common.workflowAsTool": "Workflow as Tool",
"common.workflowAsToolDescription": "Als tool in andere apps gebruiken",
"common.workflowAsToolDisabledHint": "Publish the latest workflow and ensure a connected User Input node before configuring it as a tool.",
"common.workflowAsToolReady": "Gereed",
"common.workflowAsToolReconfigure": "Opnieuw configureren",
"common.workflowAsToolTip": "Tool reconfiguration is required after the workflow update.",
"common.workflowAsToolUpdateNeeded": "Update vereist",
"common.workflowProcess": "Workflow Process",
"common.workflowProcessFailed": "Workflowproces mislukt",
"common.workflowProcessPaused": "Workflowproces gepauzeerd",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Only yours",
"versionHistory.filter.reset": "Reset Filter",
"versionHistory.latest": "Latest",
"versionHistory.nameIt": "Naam geven",
"versionHistory.nameThisVersion": "Name this version",
"versionHistory.releaseNotesPlaceholder": "Describe what changed",
"versionHistory.restorationTip": "After version restoration, the current draft will be overwritten.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Rozszerzenia oparte na interfejsie API zapewniają scentralizowane zarządzanie interfejsami API, upraszczając konfigurację dla łatwego użytkowania w aplikacjach Dify.",
"apiBasedExtension.type": "Typ",
"apiBasedExtensionPage.description": "Rozszerz możliwości modułów Dify przez API — dodaj do aplikacji niestandardową moderację treści lub zewnętrzne narzędzia danych.",
"appMenus.accessPoint": "Punkt dostępu",
"appMenus.annotations": "Adnotacje",
"appMenus.apiAccess": "Dostęp API",
"appMenus.apiAccessTip": "Ta baza wiedzy jest dostępna przez API usługi",
"appMenus.deploy": "Wdróż",
"appMenus.logs": "Logi",
"appMenus.overview": "Monitorowanie",
"appMenus.promptEng": "Orkiestracja",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Teraz można tworzyć zmienne typu pliku w formularzu startowym. W przyszłości nie będziemy już obsługiwać funkcji przesyłania obrazów.",
"common.accessAPIReference": "Uzyskaj dostęp do dokumentacji API",
"common.accessPointDescription": "Skonfiguruj aplikację internetową, API, MCP i wyzwalacze",
"common.addBlock": "Dodaj węzeł",
"common.addDescription": "Dodaj opis...",
"common.addFailureBranch": "Dodawanie gałęzi niepowodzenia",
@@ -168,6 +169,7 @@
"common.currentView": "Bieżący widok",
"common.currentWorkflow": "Bieżący przepływ pracy",
"common.debugAndPreview": "Podgląd",
"common.deployDescription": "Wdrażaj wersje w środowiskach",
"common.disconnect": "Odłączyć",
"common.draftSaveFailed": "Nie udało się zapisać wersji roboczej",
"common.duplicate": "Duplikuj",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Ten krok nie jest połączony z niczym",
"common.needOutputNode": "Należy dodać węzeł wyjściowy",
"common.needStartNode": "Należy dodać co najmniej jeden węzeł początkowy",
"common.noChanges": "Brak zmian",
"common.noHistory": "Brak historii",
"common.noVar": "Brak zmiennej",
"common.notPublishedYet": "Jeszcze nie opublikowano",
"common.notRunning": "Jeszcze nie uruchomiono",
"common.onFailure": "W przypadku niepowodzenia",
"common.openInExplore": "Otwieranie w obszarze Eksploruj",
"common.openWebApp": "Otwórz aplikację internetową",
"common.openWebAppDescription": "Otwórz opublikowaną aplikację w nowej karcie",
"common.output": "Wyjście",
"common.overwriteAndImport": "Nadpisywanie i importowanie",
"common.parallel": "RÓWNOLEGŁY",
@@ -237,10 +243,12 @@
"common.processData": "Przetwórz dane",
"common.publish": "Opublikuj",
"common.publishToMarketplace": "Publikuj na Marketplace",
"common.publishToMarketplaceDescription": "Udostępnij tę aplikację społeczności",
"common.publishToMarketplaceFailed": "Nie udało się opublikować na Marketplace",
"common.publishUpdate": "Opublikuj aktualizację",
"common.published": "Opublikowane",
"common.publishedAt": "Opublikowane",
"common.publishedBy": "Opublikowano {{time}} przez {{author}}",
"common.publishingToMarketplace": "Publikowanie...",
"common.redo": "Ponów",
"common.restart": "Uruchom ponownie",
@@ -250,6 +258,7 @@
"common.runApp": "Uruchom aplikację",
"common.runHistory": "Historia uruchomień",
"common.running": "Uruchamianie",
"common.savedAt": "Zapisano {{time}}",
"common.scheduleTriggerRunFailed": "Niepowodzenie uruchomienia zaplanowanego wyzwalacza",
"common.searchVar": "Szukaj zmiennej",
"common.setVarValuePlaceholder": "Ustaw zmienną",
@@ -264,6 +273,7 @@
"common.tagBound": "Liczba aplikacji korzystających z tego tagu",
"common.undo": "Cofnij",
"common.unpublished": "Nieopublikowane",
"common.unpublishedChanges": "Nieopublikowane zmiany",
"common.update": "Aktualizuj",
"common.variableNamePlaceholder": "Nazwa zmiennej",
"common.versionHistory": "Historia wersji",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Niepowodzenie debugowania Webhook",
"common.webhookDebugRequestFailed": "Niepowodzenie żądania debugowania Webhook",
"common.workflowAsTool": "Przepływ pracy jako narzędzie",
"common.workflowAsToolDescription": "Używaj jako narzędzia w innych aplikacjach",
"common.workflowAsToolDisabledHint": "Opublikuj najnowszy przepływ pracy i upewnij się, że węzeł Wprowadzanie danych użytkownika jest połączony przed skonfigurowaniem go jako narzędzie.",
"common.workflowAsToolReady": "Gotowe",
"common.workflowAsToolReconfigure": "Skonfiguruj ponownie",
"common.workflowAsToolTip": "Wymagana rekonfiguracja narzędzia po aktualizacji przepływu pracy.",
"common.workflowAsToolUpdateNeeded": "Wymagana aktualizacja",
"common.workflowProcess": "Proces przepływu pracy",
"common.workflowProcessFailed": "Proces przepływu pracy nie powiódł się",
"common.workflowProcessPaused": "Proces przepływu pracy wstrzymany",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Tylko twój",
"versionHistory.filter.reset": "Resetuj filtr",
"versionHistory.latest": "Najnowszy",
"versionHistory.nameIt": "Nazwij",
"versionHistory.nameThisVersion": "Nazwij tę wersję",
"versionHistory.releaseNotesPlaceholder": "Opisz, co się zmieniło",
"versionHistory.restorationTip": "Po przywróceniu wersji bieżący szkic zostanie nadpisany.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "As extensões de API fornecem gerenciamento centralizado de API, simplificando a configuração para uso fácil em todos os aplicativos da Dify.",
"apiBasedExtension.type": "Tipo",
"apiBasedExtensionPage.description": "Amplie os recursos dos módulos do Dify via API — adicione moderação de conteúdo personalizada ou ferramentas de dados externas aos seus apps.",
"appMenus.accessPoint": "Ponto de acesso",
"appMenus.annotations": "Anotações",
"appMenus.apiAccess": "Acesso à API",
"appMenus.apiAccessTip": "Esta base de conhecimento é acessível via API de serviço",
"appMenus.deploy": "Implantar",
"appMenus.logs": "Logs",
"appMenus.overview": "Monitoramento",
"appMenus.promptEng": "Orquestrar",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Agora você pode criar variáveis de tipo de arquivo no formulário inicial. Não daremos mais suporte ao recurso de upload de imagens no futuro.",
"common.accessAPIReference": "Acessar referência da API",
"common.accessPointDescription": "Configurar aplicativo web, API, MCP e gatilhos",
"common.addBlock": "Adicionar Nó",
"common.addDescription": "Adicionar descrição...",
"common.addFailureBranch": "Adicionar ramificação com falha",
@@ -168,6 +169,7 @@
"common.currentView": "Visualização atual",
"common.currentWorkflow": "Fluxo de trabalho atual",
"common.debugAndPreview": "Visualizar",
"common.deployDescription": "Implantar versões nos ambientes",
"common.disconnect": "Desligar",
"common.draftSaveFailed": "Falha ao salvar o rascunho",
"common.duplicate": "Duplicar",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Este passo não está conectado a nada",
"common.needOutputNode": "O nó de Saída deve ser adicionado",
"common.needStartNode": "Pelo menos um nó inicial deve ser adicionado",
"common.noChanges": "Nenhuma alteração",
"common.noHistory": "Sem História",
"common.noVar": "Sem variável",
"common.notPublishedYet": "Ainda não publicado",
"common.notRunning": "Ainda não está em execução",
"common.onFailure": "Em caso de falha",
"common.openInExplore": "Abrir no Explore",
"common.openWebApp": "Abrir aplicativo web",
"common.openWebAppDescription": "Abrir o aplicativo publicado em uma nova guia",
"common.output": "Saída",
"common.overwriteAndImport": "Substituir e importar",
"common.parallel": "PARALELO",
@@ -237,10 +243,12 @@
"common.processData": "Processar dados",
"common.publish": "Publicar",
"common.publishToMarketplace": "Publicar no Marketplace",
"common.publishToMarketplaceDescription": "Compartilhar este aplicativo com a comunidade",
"common.publishToMarketplaceFailed": "Falha ao publicar no Marketplace",
"common.publishUpdate": "Publicar Atualização",
"common.published": "Publicado",
"common.publishedAt": "Publicado em",
"common.publishedBy": "Publicado {{time}} por {{author}}",
"common.publishingToMarketplace": "Publicando...",
"common.redo": "Refazer",
"common.restart": "Reiniciar",
@@ -250,6 +258,7 @@
"common.runApp": "Executar aplicativo",
"common.runHistory": "Histórico de execução",
"common.running": "Executando",
"common.savedAt": "Salvo {{time}}",
"common.scheduleTriggerRunFailed": "Falha na execução do gatilho agendado",
"common.searchVar": "Buscar variável",
"common.setVarValuePlaceholder": "Definir valor da variável",
@@ -264,6 +273,7 @@
"common.tagBound": "Número de aplicativos usando esta tag",
"common.undo": "Desfazer",
"common.unpublished": "Não publicado",
"common.unpublishedChanges": "Alterações não publicadas",
"common.update": "Atualizar",
"common.variableNamePlaceholder": "Nome da variável",
"common.versionHistory": "Histórico de Versão",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Falha na depuração do Webhook",
"common.webhookDebugRequestFailed": "Falha na solicitação de depuração do Webhook",
"common.workflowAsTool": "Fluxo de trabalho como ferramenta",
"common.workflowAsToolDescription": "Usar como ferramenta em outros aplicativos",
"common.workflowAsToolDisabledHint": "Publique o fluxo de trabalho mais recente e garanta que haja um nó de Entrada do Usuário conectado antes de configurá-lo como uma ferramenta.",
"common.workflowAsToolReady": "Pronto",
"common.workflowAsToolReconfigure": "Reconfigurar",
"common.workflowAsToolTip": "É necessária a reconfiguração da ferramenta após a atualização do fluxo de trabalho.",
"common.workflowAsToolUpdateNeeded": "Atualização necessária",
"common.workflowProcess": "Processo de fluxo de trabalho",
"common.workflowProcessFailed": "Processo de fluxo de trabalho falhou",
"common.workflowProcessPaused": "Processo de fluxo de trabalho pausado",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Somente seu",
"versionHistory.filter.reset": "Redefinir Filtro",
"versionHistory.latest": "Último",
"versionHistory.nameIt": "Dê um nome",
"versionHistory.nameThisVersion": "Nomeie esta versão",
"versionHistory.releaseNotesPlaceholder": "Descreva o que mudou",
"versionHistory.restorationTip": "Após a restauração da versão, o rascunho atual será substituído.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Extensiile bazate pe API oferă o gestionare centralizată a API-urilor, simplificând configurația pentru o utilizare ușoară în aplicațiile Dify.",
"apiBasedExtension.type": "Tip",
"apiBasedExtensionPage.description": "Extinde capabilitățile modulelor Dify prin API — adaugă în aplicații moderare de conținut personalizată sau instrumente externe de date.",
"appMenus.accessPoint": "Punct de acces",
"appMenus.annotations": "Anotări",
"appMenus.apiAccess": "Acces API",
"appMenus.apiAccessTip": "Această bază de cunoștințe este accesibilă prin API-ul serviciului",
"appMenus.deploy": "Implementează",
"appMenus.logs": "Jurnale",
"appMenus.overview": "Monitorizare",
"appMenus.promptEng": "Orchestrare",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Acum puteți crea variabile de tip de fișier în formularul de pornire. Nu vom mai accepta funcția de încărcare a imaginilor în viitor.",
"common.accessAPIReference": "Accesează referința API",
"common.accessPointDescription": "Configurează aplicația web, API, MCP și declanșatoarele",
"common.addBlock": "Adaugă nod",
"common.addDescription": "Adaugă descriere...",
"common.addFailureBranch": "Adăugare ramură Fail",
@@ -168,6 +169,7 @@
"common.currentView": "Vizualizare curentă",
"common.currentWorkflow": "Flux de lucru curent",
"common.debugAndPreview": "Previzualizare",
"common.deployDescription": "Publică versiunile în medii",
"common.disconnect": "Deconecta",
"common.draftSaveFailed": "Salvarea schiței a eșuat",
"common.duplicate": "Duplică",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Acest pas nu este conectat la nimic",
"common.needOutputNode": "Nodul de ieșire trebuie adăugat",
"common.needStartNode": "Trebuie adăugat cel puțin un nod de start",
"common.noChanges": "Nicio modificare",
"common.noHistory": "Fără istorie",
"common.noVar": "Fără variabilă",
"common.notPublishedYet": "Nu a fost publicat încă",
"common.notRunning": "Încă nu rulează",
"common.onFailure": "În caz de eșec",
"common.openInExplore": "Deschide în Explorează",
"common.openWebApp": "Deschide aplicația web",
"common.openWebAppDescription": "Deschide aplicația publicată într-o filă nouă",
"common.output": "Ieșire",
"common.overwriteAndImport": "Suprascriere și import",
"common.parallel": "PARALEL",
@@ -237,10 +243,12 @@
"common.processData": "Procesează date",
"common.publish": "Publică",
"common.publishToMarketplace": "Publicați pe Marketplace",
"common.publishToMarketplaceDescription": "Distribuie această aplicație comunității",
"common.publishToMarketplaceFailed": "Eroare la publicarea pe Marketplace",
"common.publishUpdate": "Publicați actualizarea",
"common.published": "Publicat",
"common.publishedAt": "Publicat la",
"common.publishedBy": "Publicat {{time}} de {{author}}",
"common.publishingToMarketplace": "Se publică...",
"common.redo": "Refă",
"common.restart": "Repornește",
@@ -250,6 +258,7 @@
"common.runApp": "Rulează aplicația",
"common.runHistory": "Istoric rulări",
"common.running": "Rulând",
"common.savedAt": "Salvat {{time}}",
"common.scheduleTriggerRunFailed": "Eșec la rularea declanșatorului programat",
"common.searchVar": "Caută variabilă",
"common.setVarValuePlaceholder": "Setează valoarea variabilei",
@@ -264,6 +273,7 @@
"common.tagBound": "Numărul de aplicații care folosesc acest tag",
"common.undo": "Anulează",
"common.unpublished": "Nepublicat",
"common.unpublishedChanges": "Modificări nepublicate",
"common.update": "Actualizează",
"common.variableNamePlaceholder": "Nume variabilă",
"common.versionHistory": "Istoricul versiunilor",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Eșec la depanarea Webhook",
"common.webhookDebugRequestFailed": "Eșec la cererea de depanare Webhook",
"common.workflowAsTool": "Flux de lucru ca instrument",
"common.workflowAsToolDescription": "Folosește-l ca instrument în alte aplicații",
"common.workflowAsToolDisabledHint": "Publicați fluxul de lucru cel mai recent și asigurați-vă că există un nod de Intrare Utilizator conectat înainte de a-l configura ca instrument.",
"common.workflowAsToolReady": "Gata",
"common.workflowAsToolReconfigure": "Reconfigurează",
"common.workflowAsToolTip": "Reconfigurarea instrumentului este necesară după actualizarea fluxului de lucru.",
"common.workflowAsToolUpdateNeeded": "Actualizare necesară",
"common.workflowProcess": "Proces de flux de lucru",
"common.workflowProcessFailed": "Procesul de flux de lucru a eșuat",
"common.workflowProcessPaused": "Procesul de flux de lucru este întrerupt",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Numai al tău",
"versionHistory.filter.reset": "Resetare filtrare",
"versionHistory.latest": "Cea mai recentă",
"versionHistory.nameIt": "Denumește",
"versionHistory.nameThisVersion": "Numește această versiune",
"versionHistory.releaseNotesPlaceholder": "Descrie ce s-a schimbat",
"versionHistory.restorationTip": "După restaurarea versiunii, proiectul actual va fi suprascris.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API-расширения обеспечивают централизованное управление API, упрощая настройку для удобного использования в приложениях Dify.",
"apiBasedExtension.type": "Тип",
"apiBasedExtensionPage.description": "Расширяйте возможности модулей Dify через API — добавляйте в приложения собственную модерацию контента или внешние инструменты данных.",
"appMenus.accessPoint": "Точка доступа",
"appMenus.annotations": "Аннотации",
"appMenus.apiAccess": "Доступ к API",
"appMenus.apiAccessTip": "Эта база знаний доступна через API сервиса",
"appMenus.deploy": "Развернуть",
"appMenus.logs": "Журналы",
"appMenus.overview": "Мониторинг",
"appMenus.promptEng": "Оркестрация",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Теперь вы можете создавать переменные типа файла в стартовой форме. В будущем мы больше не будем поддерживать функцию загрузки изображений.",
"common.accessAPIReference": "Доступ к справочнику API",
"common.accessPointDescription": "Настроить веб-приложение, API, MCP и триггеры",
"common.addBlock": "Добавить узел",
"common.addDescription": "Добавить описание...",
"common.addFailureBranch": "Добавить ветвь Fail",
@@ -168,6 +169,7 @@
"common.currentView": "Текущий вид",
"common.currentWorkflow": "Текущий рабочий процесс",
"common.debugAndPreview": "Предпросмотр",
"common.deployDescription": "Развернуть версии в средах",
"common.disconnect": "Разъединять",
"common.draftSaveFailed": "Не удалось сохранить черновик",
"common.duplicate": "Дублировать",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Этот шаг ни к чему не подключен",
"common.needOutputNode": "Необходимо добавить узел вывода",
"common.needStartNode": "Необходимо добавить хотя бы один начальный узел",
"common.noChanges": "Нет изменений",
"common.noHistory": "Без истории",
"common.noVar": "Нет переменной",
"common.notPublishedYet": "Ещё не опубликовано",
"common.notRunning": "Еще не запущено",
"common.onFailure": "О неудаче",
"common.openInExplore": "Открыть в разделе «Обзор»",
"common.openWebApp": "Открыть веб-приложение",
"common.openWebAppDescription": "Открыть опубликованное приложение в новой вкладке",
"common.output": "Выход",
"common.overwriteAndImport": "Перезаписать и импортировать",
"common.parallel": "ПАРАЛЛЕЛЬНЫЙ",
@@ -237,10 +243,12 @@
"common.processData": "Обработка данных",
"common.publish": "Опубликовать",
"common.publishToMarketplace": "Опубликовать в Marketplace",
"common.publishToMarketplaceDescription": "Поделиться приложением с сообществом",
"common.publishToMarketplaceFailed": "Не удалось опубликовать в Marketplace",
"common.publishUpdate": "Опубликовать обновление",
"common.published": "Опубликовано",
"common.publishedAt": "Опубликовано",
"common.publishedBy": "Опубликовано {{time}} пользователем {{author}}",
"common.publishingToMarketplace": "Публикация...",
"common.redo": "Повторить",
"common.restart": "Перезапустить",
@@ -250,6 +258,7 @@
"common.runApp": "Запустить приложение",
"common.runHistory": "История запусков",
"common.running": "Выполняется",
"common.savedAt": "Сохранено {{time}}",
"common.scheduleTriggerRunFailed": "Не удалось выполнить запланированный триггер",
"common.searchVar": "Поиск переменной",
"common.setVarValuePlaceholder": "Установить значение переменной",
@@ -264,6 +273,7 @@
"common.tagBound": "Количество приложений, использующих этот тег",
"common.undo": "Отменить",
"common.unpublished": "Не опубликовано",
"common.unpublishedChanges": "Неопубликованные изменения",
"common.update": "Обновить",
"common.variableNamePlaceholder": "Имя переменной",
"common.versionHistory": "История версий",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Ошибка отладки Webhook",
"common.webhookDebugRequestFailed": "Ошибка запроса отладки Webhook",
"common.workflowAsTool": "Рабочий процесс как инструмент",
"common.workflowAsToolDescription": "Использовать как инструмент в других приложениях",
"common.workflowAsToolDisabledHint": "Опубликуйте последний рабочий процесс и убедитесь, что подключен узел ввода пользователя, прежде чем настраивать его как инструмент.",
"common.workflowAsToolReady": "Готово",
"common.workflowAsToolReconfigure": "Перенастроить",
"common.workflowAsToolTip": "После обновления рабочего процесса требуется перенастройка инструмента.",
"common.workflowAsToolUpdateNeeded": "Требуется обновление",
"common.workflowProcess": "Процесс рабочего процесса",
"common.workflowProcessFailed": "Процесс рабочего процесса завершился с ошибкой",
"common.workflowProcessPaused": "Процесс рабочего процесса приостановлен",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Только твой",
"versionHistory.filter.reset": "Сбросить фильтр",
"versionHistory.latest": "Последний",
"versionHistory.nameIt": "Назвать",
"versionHistory.nameThisVersion": "Назовите эту версию",
"versionHistory.releaseNotesPlaceholder": "Опишите, что изменилось",
"versionHistory.restorationTip": "После восстановления версии текущий черновик будет перезаписан.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Razširitve API zagotavljajo centralizirano upravljanje API, kar poenostavlja konfiguracijo za enostavno uporabo v aplikacijah Dify.",
"apiBasedExtension.type": "Vrsta",
"apiBasedExtensionPage.description": "Razširite zmožnosti modulov Dify prek API-ja — v aplikacije dodajte prilagojeno moderiranje vsebine ali zunanja podatkovna orodja.",
"appMenus.accessPoint": "Dostopna točka",
"appMenus.annotations": "Opombe",
"appMenus.apiAccess": "Dostop do API-ja",
"appMenus.apiAccessTip": "Ta baza znanja je dostopna prek API storitve",
"appMenus.deploy": "Uvedi",
"appMenus.logs": "Dnevniki",
"appMenus.overview": "Spremljanje",
"appMenus.promptEng": "Orkester",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Zdaj lahko v začetni obliki ustvarite spremenljivke datotečnega tipa. V prihodnje ne bomo več podpirali funkcije nalaganja slik.",
"common.accessAPIReference": "Dostop do referenčnega API-ja",
"common.accessPointDescription": "Nastavi spletno aplikacijo, API, MCP in sprožilce",
"common.addBlock": "Dodaj vozlišče",
"common.addDescription": "Dodajte opis...",
"common.addFailureBranch": "Dodaj neuspešno vejo",
@@ -168,6 +169,7 @@
"common.currentView": "Trenutni pogled",
"common.currentWorkflow": "Trenutni potek dela",
"common.debugAndPreview": "Predogled",
"common.deployDescription": "Razmesti različice v okolja",
"common.disconnect": "Odklop",
"common.draftSaveFailed": "Shranjevanje osnutka ni uspelo",
"common.duplicate": "Podvojiti",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Ta korak ni povezan z ničemer.",
"common.needOutputNode": "Dodati je treba izhodiščno vozlišče",
"common.needStartNode": "Dodati je treba vsaj eno začetno vozlišče",
"common.noChanges": "Brez sprememb",
"common.noHistory": "Brez zgodovine",
"common.noVar": "Brez spremenljivke",
"common.notPublishedYet": "Še ni objavljeno",
"common.notRunning": "Še ne teče",
"common.onFailure": "O neuspehu",
"common.openInExplore": "Odpri v Raziskovanju",
"common.openWebApp": "Odpri spletno aplikacijo",
"common.openWebAppDescription": "Odpri objavljeno aplikacijo v novem zavihku",
"common.output": "Izhod",
"common.overwriteAndImport": "Prepiši in uvozi",
"common.parallel": "PARALELNO",
@@ -237,10 +243,12 @@
"common.processData": "Obdelava podatkov",
"common.publish": "Objavi",
"common.publishToMarketplace": "Objavi na Marketplace",
"common.publishToMarketplaceDescription": "Deli to aplikacijo s skupnostjo",
"common.publishToMarketplaceFailed": "Objava na Marketplace ni uspela",
"common.publishUpdate": "Objavi posodobitev",
"common.published": "Objavljeno",
"common.publishedAt": "Objavljeno",
"common.publishedBy": "Objavljeno {{time}}, objavil/-a {{author}}",
"common.publishingToMarketplace": "Objavljanje...",
"common.redo": "Ponovno naredi",
"common.restart": "Znova zaženi",
@@ -250,6 +258,7 @@
"common.runApp": "Zaženi aplikacijo",
"common.runHistory": "Zgodovina izvajanja",
"common.running": "Tek",
"common.savedAt": "Shranjeno {{time}}",
"common.scheduleTriggerRunFailed": "Zagon načrtovanega sprožilnika ni uspel",
"common.searchVar": "Iskalna spremenljivka",
"common.setVarValuePlaceholder": "Nastavi spremenljivko",
@@ -264,6 +273,7 @@
"common.tagBound": "Število aplikacij, ki uporabljajo to oznako",
"common.undo": "Razveljavi",
"common.unpublished": "Nepublikirano",
"common.unpublishedChanges": "Neobjavljene spremembe",
"common.update": "Posodobitev",
"common.variableNamePlaceholder": "Ime spremenljivke",
"common.versionHistory": "Zgodovina različic",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Razhroščevanje Webhook ni uspelo",
"common.webhookDebugRequestFailed": "Zahteva za razhroščevanje Webhook ni uspela",
"common.workflowAsTool": "Delovni potek kot orodje",
"common.workflowAsToolDescription": "Uporabi kot orodje v drugih aplikacijah",
"common.workflowAsToolDisabledHint": "Objavite najnovejše delovne tokove in zagotovite, da je pred konfiguracijo kot orodje povezan vozel za vnos uporabnika.",
"common.workflowAsToolReady": "Pripravljeno",
"common.workflowAsToolReconfigure": "Ponovno konfiguriraj",
"common.workflowAsToolTip": "Zaradi posodobitve delovnega poteka je potrebna ponovna konfiguracija orodja.",
"common.workflowAsToolUpdateNeeded": "Potrebna je posodobitev",
"common.workflowProcess": "Delovni postopek",
"common.workflowProcessFailed": "Delovni postopek ni uspel",
"common.workflowProcessPaused": "Delovni postopek je začasno ustavljen",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Samo tvoje",
"versionHistory.filter.reset": "Ponastavi filter",
"versionHistory.latest": "Najnovejši",
"versionHistory.nameIt": "Poimenuj",
"versionHistory.nameThisVersion": "Poimenujte to različico",
"versionHistory.releaseNotesPlaceholder": "Opisujte, kaj se je spremenilo",
"versionHistory.restorationTip": "Po obnovitvi različice bo trenutni osnutek prepisan.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "ส่วนขยาย API ให้การจัดการ API แบบรวมศูนย์ ทําให้การกําหนดค่าง่ายขึ้นเพื่อให้ใช้งานได้ง่ายในแอปพลิเคชันของ Dify",
"apiBasedExtension.type": "ประเภท",
"apiBasedExtensionPage.description": "ขยายความสามารถของโมดูล Dify ผ่าน API — เพิ่มการตรวจสอบเนื้อหาแบบกำหนดเองหรือเครื่องมือข้อมูลภายนอกให้แอปของคุณ",
"appMenus.accessPoint": "จุดเข้าถึง",
"appMenus.annotations": "คำ อธิบาย",
"appMenus.apiAccess": "การเข้าถึง API",
"appMenus.apiAccessTip": "ฐานความรู้นี้สามารถเข้าถึงได้ผ่าน API บริการ",
"appMenus.deploy": "ปรับใช้",
"appMenus.logs": "บันทึก",
"appMenus.overview": "ตรวจ สอบ",
"appMenus.promptEng": "ออเคสตร้า",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "ตอนนี้คุณสามารถสร้างตัวแปรชนิดไฟล์ในฟอร์มเริ่มต้นได้แล้ว เราจะไม่รองรับฟีเจอร์การอัปโหลดรูปภาพอีกต่อไปในอนาคต",
"common.accessAPIReference": "การอ้างอิง API การเข้าถึง",
"common.accessPointDescription": "กำหนดค่าเว็บแอป, API, MCP และทริกเกอร์",
"common.addBlock": "เพิ่มโนด",
"common.addDescription": "เพิ่มคําอธิบาย...",
"common.addFailureBranch": "เพิ่มสาขา Fail",
@@ -168,6 +169,7 @@
"common.currentView": "ปัจจุบัน View",
"common.currentWorkflow": "เวิร์กโฟลว์ปัจจุบัน",
"common.debugAndPreview": "ดูตัวอย่าง",
"common.deployDescription": "นำเวอร์ชันไปใช้กับสภาพแวดล้อม",
"common.disconnect": "ยก เลิก",
"common.draftSaveFailed": "บันทึกฉบับร่างไม่สำเร็จ",
"common.duplicate": "สำเนา",
@@ -216,11 +218,15 @@
"common.needConnectTip": "ขั้นตอนนี้ไม่ได้เชื่อมต่อกับสิ่งใด",
"common.needOutputNode": "ต้องเพิ่มโหนดเอาต์พุต",
"common.needStartNode": "ต้องเพิ่มโหนดเริ่มต้นอย่างน้อยหนึ่งโหนด",
"common.noChanges": "ไม่มีการเปลี่ยนแปลง",
"common.noHistory": "ไม่มีประวัติ",
"common.noVar": "ไม่มีตัวแปร",
"common.notPublishedYet": "ยังไม่ได้เผยแพร่",
"common.notRunning": "ยังไม่ได้ทํางาน",
"common.onFailure": "เมื่อล้มเหลว",
"common.openInExplore": "เปิดใน Explore",
"common.openWebApp": "เปิดเว็บแอป",
"common.openWebAppDescription": "เปิดแอปที่เผยแพร่ในแท็บใหม่",
"common.output": "ผลิตภัณฑ์",
"common.overwriteAndImport": "เขียนทับและนําเข้า",
"common.parallel": "ขนาน",
@@ -237,10 +243,12 @@
"common.processData": "ประมวลผลข้อมูล",
"common.publish": "ตีพิมพ์",
"common.publishToMarketplace": "เผยแพร่ไปยัง Marketplace",
"common.publishToMarketplaceDescription": "แชร์แอปนี้กับชุมชน",
"common.publishToMarketplaceFailed": "เผยแพร่ไปยัง Marketplace ล้มเหลว",
"common.publishUpdate": "เผยแพร่การอัปเดต",
"common.published": "เผย แพร่",
"common.publishedAt": "เผย แพร่",
"common.publishedBy": "เผยแพร่เมื่อ {{time}} โดย {{author}}",
"common.publishingToMarketplace": "กำลังเผยแพร่...",
"common.redo": "พร้อม",
"common.restart": "เริ่มใหม่",
@@ -250,6 +258,7 @@
"common.runApp": "เรียกใช้แอพ",
"common.runHistory": "ประวัติการวิ่ง",
"common.running": "กำลัง เรียก ใช้",
"common.savedAt": "บันทึกเมื่อ {{time}}",
"common.scheduleTriggerRunFailed": "การทำงานของตัวทริกเกอร์ที่กำหนดเวลาล้มเหลว",
"common.searchVar": "ตัวแปรการค้นหา",
"common.setVarValuePlaceholder": "ตั้งค่าตัวแปร",
@@ -264,6 +273,7 @@
"common.tagBound": "จำนวนแอปพลิเคชันที่ใช้แท็กนี้",
"common.undo": "แก้",
"common.unpublished": "ไม่ได้เผยแพร่",
"common.unpublishedChanges": "การเปลี่ยนแปลงที่ยังไม่ได้เผยแพร่",
"common.update": "อัพเดต",
"common.variableNamePlaceholder": "ชื่อตัวแปร",
"common.versionHistory": "ประวัติรุ่น",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "การดีบัก Webhook ล้มเหลว",
"common.webhookDebugRequestFailed": "คำขอดีบัก Webhook ล้มเหลว",
"common.workflowAsTool": "เวิร์กโฟลว์เป็นเครื่องมือ",
"common.workflowAsToolDescription": "ใช้เป็นเครื่องมือในแอปอื่น",
"common.workflowAsToolDisabledHint": "เผยแพร่เวิร์กโฟลว์ล่าสุดและตรวจสอบให้แน่ใจว่ามีโหนดป้อนข้อมูลผู้ใช้เชื่อมต่อก่อนที่จะกำหนดค่าเป็นเครื่องมือ",
"common.workflowAsToolReady": "พร้อม",
"common.workflowAsToolReconfigure": "กำหนดค่าใหม่",
"common.workflowAsToolTip": "จําเป็นต้องมีการกําหนดค่าเครื่องมือใหม่หลังจากการอัปเดตเวิร์กโฟลว์",
"common.workflowAsToolUpdateNeeded": "ต้องอัปเดต",
"common.workflowProcess": "กระบวนการเวิร์กโฟลว์",
"common.workflowProcessFailed": "กระบวนการเวิร์กโฟลว์ล้มเหลว",
"common.workflowProcessPaused": "กระบวนการเวิร์กโฟลว์หยุดชั่วคราว",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "เพียงของคุณเท่านั้น",
"versionHistory.filter.reset": "รีเซ็ตตัวกรอง",
"versionHistory.latest": "ล่าสุด",
"versionHistory.nameIt": "ตั้งชื่อ",
"versionHistory.nameThisVersion": "ชื่อเวอร์ชันนี้",
"versionHistory.releaseNotesPlaceholder": "อธิบายว่าสิ่งที่เปลี่ยนแปลงไปคืออะไร",
"versionHistory.restorationTip": "หลังจากการกู้คืนเวอร์ชันแล้ว ร่างปัจจุบันจะถูกเขียนทับ.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API uzantıları merkezi API yönetimi sağlar, Dify'nin uygulamaları arasında kolay kullanım için yapılandırmayı basitleştirir.",
"apiBasedExtension.type": "Tür",
"apiBasedExtensionPage.description": "API ile Dify modül yeteneklerini genişletin — uygulamalarınıza özel içerik denetimi veya harici veri araçları ekleyin.",
"appMenus.accessPoint": "Erişim Noktası",
"appMenus.annotations": "Ek Açıklamalar",
"appMenus.apiAccess": "API Erişimi",
"appMenus.apiAccessTip": "Bu bilgi tabanına Servis API üzerinden erişilebilir",
"appMenus.deploy": "Dağıt",
"appMenus.logs": "Günlükler",
"appMenus.overview": "İzleme",
"appMenus.promptEng": "Düzenle",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Yanıtla",
"common.ImageUploadLegacyTip": "Artık başlangıç formunda dosya türü değişkenleri oluşturabilirsiniz. Gelecekte resim yükleme özelliğini artık desteklemeyeceğiz.",
"common.accessAPIReference": "API Referansına Eriş",
"common.accessPointDescription": "Web uygulamasını, API'yi, MCP'yi ve tetikleyicileri yapılandır",
"common.addBlock": "Düğüm Ekle",
"common.addDescription": "Açıklama ekle...",
"common.addFailureBranch": "Başarısız dal ekle",
@@ -168,6 +169,7 @@
"common.currentView": "Geçerli Görünüm",
"common.currentWorkflow": "Mevcut İş Akışı",
"common.debugAndPreview": "Önizleme",
"common.deployDescription": "Sürümleri ortamlara dağıt",
"common.disconnect": "Bağlantıyı Kes",
"common.draftSaveFailed": "Taslak kaydedilemedi",
"common.duplicate": "Çoğalt",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Bu adım hiçbir şeye bağlı değil",
"common.needOutputNode": "Çıktı düğümü eklenmelidir",
"common.needStartNode": "En az bir başlangıç düğümü eklenmelidir",
"common.noChanges": "Değişiklik yok",
"common.noHistory": "Tarih Yok",
"common.noVar": "Değişken yok",
"common.notPublishedYet": "Henüz yayınlanmadı",
"common.notRunning": "Henüz çalıştırılmadı",
"common.onFailure": "Başarısızlık Üzerine",
"common.openInExplore": "Keşfet'te Aç",
"common.openWebApp": "Web uygulamasını aç",
"common.openWebAppDescription": "Yayınlanan uygulamayı yeni bir sekmede aç",
"common.output": "Çıktı",
"common.overwriteAndImport": "Üzerine Yaz ve İçe Aktar",
"common.parallel": "PARALEL",
@@ -237,10 +243,12 @@
"common.processData": "Veriyi İşle",
"common.publish": "Yayınla",
"common.publishToMarketplace": "Pazar Yeri'nde Yayınla",
"common.publishToMarketplaceDescription": "Bu uygulamayı toplulukla paylaş",
"common.publishToMarketplaceFailed": "Pazar Yeri'nde yayınlanamadı",
"common.publishUpdate": "Güncellemeyi Yayınla",
"common.published": "Yayınlandı",
"common.publishedAt": "Yayınlandı",
"common.publishedBy": "{{author}} tarafından {{time}} yayınlandı",
"common.publishingToMarketplace": "Yayınlanıyor...",
"common.redo": "Yinele",
"common.restart": "Yeniden Başlat",
@@ -250,6 +258,7 @@
"common.runApp": "Uygulamayı Çalıştır",
"common.runHistory": "Çalıştırma Geçmişi",
"common.running": "Çalışıyor",
"common.savedAt": "{{time}} kaydedildi",
"common.scheduleTriggerRunFailed": "Zamanlanmış tetikleyici çalıştırma başarısız oldu",
"common.searchVar": "Değişkeni ara",
"common.setVarValuePlaceholder": "Değişkeni ayarla",
@@ -264,6 +273,7 @@
"common.tagBound": "Bu etiketi kullanan uygulama sayısı",
"common.undo": "Geri Al",
"common.unpublished": "Yayınlanmamış",
"common.unpublishedChanges": "Yayınlanmamış değişiklikler",
"common.update": "Güncelle",
"common.variableNamePlaceholder": "Değişken adı",
"common.versionHistory": "Sürüm Geçmişi",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook hata ayıklama başarısız oldu",
"common.webhookDebugRequestFailed": "Webhook hata ayıklama isteği başarısız oldu",
"common.workflowAsTool": "Araç Olarak İş Akışı",
"common.workflowAsToolDescription": "Diğer uygulamalarda araç olarak kullan",
"common.workflowAsToolDisabledHint": "En son iş akışını yayınlayın ve bunu bir araç olarak yapılandırmadan önce bağlı bir Kullanıcı Girdisi düğümünün olduğundan emin olun.",
"common.workflowAsToolReady": "Hazır",
"common.workflowAsToolReconfigure": "Yeniden yapılandır",
"common.workflowAsToolTip": "İş Akışı güncellemesinden sonra araç yeniden yapılandırması gereklidir.",
"common.workflowAsToolUpdateNeeded": "Güncelleme gerekli",
"common.workflowProcess": "İş Akışı Süreci",
"common.workflowProcessFailed": "İş Akışı Süreci başarısız oldu",
"common.workflowProcessPaused": "İş Akışı Süreci duraklatıldı",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Sadece senin",
"versionHistory.filter.reset": "Filtreyi Sıfırla",
"versionHistory.latest": "Sonuncu",
"versionHistory.nameIt": "Adlandır",
"versionHistory.nameThisVersion": "Bu versiyona isim ver",
"versionHistory.releaseNotesPlaceholder": "Değişen şeyleri tanımlayın",
"versionHistory.restorationTip": "Sürüm geri yüklemeden sonra, mevcut taslak üzerine yazılacak.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "API-розширення забезпечують централізоване керування API, спрощуючи конфігурацію для зручного використання в різних програмах Dify.",
"apiBasedExtension.type": "Тип",
"apiBasedExtensionPage.description": "Розширюйте можливості модулів Dify через API — додавайте до застосунків власну модерацію контенту або зовнішні інструменти даних.",
"appMenus.accessPoint": "Точка доступу",
"appMenus.annotations": "Анотації",
"appMenus.apiAccess": "Доступ до API",
"appMenus.apiAccessTip": "Ця база знань доступна через API сервісу",
"appMenus.deploy": "Розгорнути",
"appMenus.logs": "Журнали",
"appMenus.overview": "Моніторинг",
"appMenus.promptEng": "Налаштування",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Тепер ви можете створювати змінні типу файлу у початковій формі. У майбутньому ми більше не підтримуватимемо функцію завантаження зображень.",
"common.accessAPIReference": "Доступ до довідника API",
"common.accessPointDescription": "Налаштувати вебзастосунок, API, MCP і тригери",
"common.addBlock": "Додати вузол",
"common.addDescription": "Додати опис...",
"common.addFailureBranch": "Додано гілку помилки",
@@ -168,6 +169,7 @@
"common.currentView": "Поточний вигляд",
"common.currentWorkflow": "Поточний робочий процес",
"common.debugAndPreview": "Попередній перегляд",
"common.deployDescription": "Розгорнути версії в середовищах",
"common.disconnect": "Відключити",
"common.draftSaveFailed": "Не вдалося зберегти чернетку",
"common.duplicate": "Дублювати",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Цей крок ні до чого не підключений",
"common.needOutputNode": "Необхідно додати вихідний вузол",
"common.needStartNode": "Потрібно додати щонайменше один початковий вузол",
"common.noChanges": "Немає змін",
"common.noHistory": "Без історії",
"common.noVar": "Без змінної",
"common.notPublishedYet": "Ще не опубліковано",
"common.notRunning": "Ще не запущено",
"common.onFailure": "Про невдачу",
"common.openInExplore": "Відкрити в Огляді",
"common.openWebApp": "Відкрити вебзастосунок",
"common.openWebAppDescription": "Відкрити опублікований застосунок у новій вкладці",
"common.output": "Вихід",
"common.overwriteAndImport": "Перезапис та імпорт",
"common.parallel": "ПАРАЛЕЛЬНИЙ",
@@ -237,10 +243,12 @@
"common.processData": "Обробити дані",
"common.publish": "Опублікувати",
"common.publishToMarketplace": "Опублікувати на Marketplace",
"common.publishToMarketplaceDescription": "Поділитися цим застосунком зі спільнотою",
"common.publishToMarketplaceFailed": "Не вдалося опублікувати на Marketplace",
"common.publishUpdate": "Опублікувати оновлення",
"common.published": "Опубліковано",
"common.publishedAt": "Опубліковано о",
"common.publishedBy": "Опубліковано {{time}} користувачем {{author}}",
"common.publishingToMarketplace": "Публікація...",
"common.redo": "Повторити",
"common.restart": "Перезапустити",
@@ -250,6 +258,7 @@
"common.runApp": "Запустити додаток",
"common.runHistory": "Історія запусків",
"common.running": "Запущено",
"common.savedAt": "Збережено {{time}}",
"common.scheduleTriggerRunFailed": "Помилка виконання запланованого тригера",
"common.searchVar": "Пошук змінної",
"common.setVarValuePlaceholder": "Встановити значення змінної",
@@ -264,6 +273,7 @@
"common.tagBound": "Кількість додатків, що використовують цей тег",
"common.undo": "Скасувати",
"common.unpublished": "Неопубліковано",
"common.unpublishedChanges": "Неопубліковані зміни",
"common.update": "Оновити",
"common.variableNamePlaceholder": "Назва змінної",
"common.versionHistory": "Історія версій",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Помилка налагодження Webhook",
"common.webhookDebugRequestFailed": "Помилка запиту налагодження Webhook",
"common.workflowAsTool": "Робочий потік як інструмент",
"common.workflowAsToolDescription": "Використовувати як інструмент в інших застосунках",
"common.workflowAsToolDisabledHint": "Опублікуйте останній робочий процес і переконайтеся, що перед налаштуванням його як інструменту є підключений вузол введення користувача.",
"common.workflowAsToolReady": "Готово",
"common.workflowAsToolReconfigure": "Переналаштувати",
"common.workflowAsToolTip": "Після оновлення робочого потоку необхідна переконфігурація інструменту.",
"common.workflowAsToolUpdateNeeded": "Потрібне оновлення",
"common.workflowProcess": "Процес робочого потоку",
"common.workflowProcessFailed": "Процес робочого потоку завершився помилкою",
"common.workflowProcessPaused": "Процес робочого потоку призупинено",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Тільки твоє",
"versionHistory.filter.reset": "Скинути фільтр",
"versionHistory.latest": "Останні новини",
"versionHistory.nameIt": "Назвати",
"versionHistory.nameThisVersion": "Назвіть цю версію",
"versionHistory.releaseNotesPlaceholder": "Опишіть, що змінилося",
"versionHistory.restorationTip": "Після відновлення версії нинішній проект буде перезаписано.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Các tiện ích API cung cấp quản lý API tập trung, giúp cấu hình dễ dàng sử dụng trên các ứng dụng của Dify.",
"apiBasedExtension.type": "Loại",
"apiBasedExtensionPage.description": "Mở rộng khả năng mô-đun của Dify qua API — thêm kiểm duyệt nội dung tùy chỉnh hoặc công cụ dữ liệu bên ngoài vào ứng dụng của bạn.",
"appMenus.accessPoint": "Điểm truy cập",
"appMenus.annotations": "Chú thích",
"appMenus.apiAccess": "Truy cập API",
"appMenus.apiAccessTip": "Cơ sở kiến thức này có thể truy cập qua API dịch vụ",
"appMenus.deploy": "Triển khai",
"appMenus.logs": "Nhật ký",
"appMenus.overview": "Giám sát",
"appMenus.promptEng": "Orchestrate",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "Reply",
"common.ImageUploadLegacyTip": "Bây giờ bạn có thể tạo các biến loại tệp trong biểu mẫu bắt đầu. Chúng tôi sẽ không còn hỗ trợ tính năng tải lên hình ảnh trong tương lai.",
"common.accessAPIReference": "Truy cập tài liệu API",
"common.accessPointDescription": "Cấu hình ứng dụng web, API, MCP và trình kích hoạt",
"common.addBlock": "Thêm Node",
"common.addDescription": "Thêm mô tả...",
"common.addFailureBranch": "Thêm nhánh Fail",
@@ -168,6 +169,7 @@
"common.currentView": "Hiện tại View",
"common.currentWorkflow": "Quy trình làm việc hiện tại",
"common.debugAndPreview": "Xem trước",
"common.deployDescription": "Triển khai các phiên bản tới môi trường",
"common.disconnect": "Ngắt kết nối",
"common.draftSaveFailed": "Lưu bản nháp thất bại",
"common.duplicate": "Nhân bản",
@@ -216,11 +218,15 @@
"common.needConnectTip": "Bước này không được kết nối với bất kỳ điều gì",
"common.needOutputNode": "Phải thêm nút Đầu ra",
"common.needStartNode": "Phải thêm ít nhất một nút bắt đầu",
"common.noChanges": "Không có thay đổi",
"common.noHistory": "Không có lịch sử",
"common.noVar": "Không có biến",
"common.notPublishedYet": "Chưa xuất bản",
"common.notRunning": "Chưa chạy",
"common.onFailure": "Khi thất bại",
"common.openInExplore": "Mở trong Khám phá",
"common.openWebApp": "Mở ứng dụng web",
"common.openWebAppDescription": "Mở ứng dụng đã xuất bản trong tab mới",
"common.output": "Đầu ra",
"common.overwriteAndImport": "Ghi đè và nhập",
"common.parallel": "SONG SONG",
@@ -237,10 +243,12 @@
"common.processData": "Xử lý dữ liệu",
"common.publish": "Xuất bản",
"common.publishToMarketplace": "Xuất bản lên Marketplace",
"common.publishToMarketplaceDescription": "Chia sẻ ứng dụng này với cộng đồng",
"common.publishToMarketplaceFailed": "Xuất bản lên Marketplace thất bại",
"common.publishUpdate": "Cập nhật xuất bản",
"common.published": "Đã xuất bản",
"common.publishedAt": "Đã xuất bản lúc",
"common.publishedBy": "Đã xuất bản {{time}} bởi {{author}}",
"common.publishingToMarketplace": "Đang xuất bản...",
"common.redo": "Làm lại",
"common.restart": "Khởi động lại",
@@ -250,6 +258,7 @@
"common.runApp": "Chạy ứng dụng",
"common.runHistory": "Lịch sử chạy",
"common.running": "Đang chạy",
"common.savedAt": "Đã lưu {{time}}",
"common.scheduleTriggerRunFailed": "Chạy trigger theo lịch thất bại",
"common.searchVar": "Tìm kiếm biến",
"common.setVarValuePlaceholder": "Đặt giá trị biến",
@@ -264,6 +273,7 @@
"common.tagBound": "Số lượng ứng dụng sử dụng thẻ này",
"common.undo": "Hoàn tác",
"common.unpublished": "Chưa xuất bản",
"common.unpublishedChanges": "Thay đổi chưa xuất bản",
"common.update": "Cập nhật",
"common.variableNamePlaceholder": "Tên biến",
"common.versionHistory": "Lịch sử phiên bản",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Gỡ lỗi Webhook thất bại",
"common.webhookDebugRequestFailed": "Yêu cầu gỡ lỗi Webhook thất bại",
"common.workflowAsTool": "Quy trình làm việc như công cụ",
"common.workflowAsToolDescription": "Sử dụng làm công cụ trong các ứng dụng khác",
"common.workflowAsToolDisabledHint": "Xuất bản quy trình làm việc mới nhất và đảm bảo một nút Nhập liệu Người dùng kết nối trước khi cấu hình nó như một công cụ.",
"common.workflowAsToolReady": "Sẵn sàng",
"common.workflowAsToolReconfigure": "Cấu hình lại",
"common.workflowAsToolTip": "Cần cấu hình lại công cụ sau khi cập nhật quy trình làm việc.",
"common.workflowAsToolUpdateNeeded": "Cần cập nhật",
"common.workflowProcess": "Quy trình làm việc",
"common.workflowProcessFailed": "Quy trình làm việc thất bại",
"common.workflowProcessPaused": "Quy trình làm việc đã tạm dừng",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "Chỉ của bạn",
"versionHistory.filter.reset": "Đặt lại bộ lọc",
"versionHistory.latest": "Mới nhất",
"versionHistory.nameIt": "Đặt tên",
"versionHistory.nameThisVersion": "Đặt tên cho phiên bản này",
"versionHistory.releaseNotesPlaceholder": "Mô tả những gì đã thay đổi",
"versionHistory.restorationTip": "Sau khi phục hồi phiên bản, bản nháp hiện tại sẽ bị ghi đè.",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Custom Endpoints 提供集中式 API 管理,简化配置,方便在 Dify 的应用中使用。",
"apiBasedExtension.type": "类型",
"apiBasedExtensionPage.description": "通过 API 扩展 Dify 的模块能力——为应用接入自定义内容审核或外部数据工具。",
"appMenus.accessPoint": "访问点",
"appMenus.annotations": "标注",
"appMenus.apiAccess": "访问 API",
"appMenus.apiAccessTip": "此知识库可通过服务 API 访问",
"appMenus.deploy": "部署",
"appMenus.logs": "日志",
"appMenus.overview": "监测",
"appMenus.promptEng": "编排",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "回复",
"common.ImageUploadLegacyTip": "现在可以在 start 表单中创建文件类型变量。未来我们将不继续支持图片上传功能。",
"common.accessAPIReference": "访问 API",
"common.accessPointDescription": "配置 Web 应用、API、MCP 和触发器",
"common.addBlock": "添加节点",
"common.addDescription": "添加描述...",
"common.addFailureBranch": "添加异常分支",
@@ -168,6 +169,7 @@
"common.currentView": "当前视图",
"common.currentWorkflow": "整个工作流",
"common.debugAndPreview": "预览",
"common.deployDescription": "将版本部署到环境",
"common.disconnect": "断开连接",
"common.draftSaveFailed": "草稿保存失败",
"common.duplicate": "复制",
@@ -216,11 +218,15 @@
"common.needConnectTip": "此节点尚未连接到其他节点",
"common.needOutputNode": "必须添加输出节点",
"common.needStartNode": "必须添加至少一个开始节点",
"common.noChanges": "无更改",
"common.noHistory": "没有历史版本",
"common.noVar": "没有变量",
"common.notPublishedYet": "尚未发布",
"common.notRunning": "尚未运行",
"common.onFailure": "异常时",
"common.openInExplore": "在“探索”中打开",
"common.openWebApp": "打开 Web 应用",
"common.openWebAppDescription": "在新标签页中打开已发布的应用",
"common.output": "输出",
"common.overwriteAndImport": "覆盖并导入",
"common.parallel": "并行",
@@ -237,10 +243,12 @@
"common.processData": "数据处理",
"common.publish": "发布",
"common.publishToMarketplace": "发布到 Marketplace",
"common.publishToMarketplaceDescription": "与社区分享此应用",
"common.publishToMarketplaceFailed": "发布到 Marketplace 失败",
"common.publishUpdate": "发布更新",
"common.published": "已发布",
"common.publishedAt": "发布于",
"common.publishedBy": "{{author}} 发布于 {{time}}",
"common.publishingToMarketplace": "发布中...",
"common.redo": "重做",
"common.restart": "重新开始",
@@ -250,6 +258,7 @@
"common.runApp": "运行",
"common.runHistory": "运行历史",
"common.running": "运行中",
"common.savedAt": "{{time}}已保存",
"common.scheduleTriggerRunFailed": "计划触发运行失败",
"common.searchVar": "搜索变量",
"common.setVarValuePlaceholder": "设置变量值",
@@ -264,6 +273,7 @@
"common.tagBound": "使用此标签的应用数量",
"common.undo": "撤销",
"common.unpublished": "未发布",
"common.unpublishedChanges": "有未发布的更改",
"common.update": "更新",
"common.variableNamePlaceholder": "变量名",
"common.versionHistory": "版本历史",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook 调试失败",
"common.webhookDebugRequestFailed": "Webhook 调试请求失败",
"common.workflowAsTool": "工作流作为工具",
"common.workflowAsToolDescription": "在其他应用中作为工具使用",
"common.workflowAsToolDisabledHint": "请先发布最新的工作流,并确保已连接的 User Input 节点后再配置为工具。",
"common.workflowAsToolReady": "已就绪",
"common.workflowAsToolReconfigure": "重新配置",
"common.workflowAsToolTip": "工作流更新后需要重新配置工具参数",
"common.workflowAsToolUpdateNeeded": "需要更新",
"common.workflowProcess": "工作流",
"common.workflowProcessFailed": "工作流运行失败",
"common.workflowProcessPaused": "工作流运行已暂停",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "仅你的",
"versionHistory.filter.reset": "重置",
"versionHistory.latest": "最新",
"versionHistory.nameIt": "命名",
"versionHistory.nameThisVersion": "命名",
"versionHistory.releaseNotesPlaceholder": "请描述变更",
"versionHistory.restorationTip": "版本回滚后,当前草稿将被覆盖。",
+2
View File
@@ -96,9 +96,11 @@
"apiBasedExtension.title": "Custom Endpoints 提供集中式 API 管理,簡化配置,方便在 Dify 的應用中使用。",
"apiBasedExtension.type": "型別",
"apiBasedExtensionPage.description": "註冊你自己的 API endpoints,讓 Dify 呼叫它們完成內容審核、外部資料等能力。",
"appMenus.accessPoint": "存取點",
"appMenus.annotations": "標註",
"appMenus.apiAccess": "訪問 API",
"appMenus.apiAccessTip": "此知識庫可透過服務 API 訪問",
"appMenus.deploy": "部署",
"appMenus.logs": "日誌",
"appMenus.overview": "監控",
"appMenus.promptEng": "編排",
+15
View File
@@ -146,6 +146,7 @@
"comments.reply": "回覆",
"common.ImageUploadLegacyTip": "現在,您可以在起始表單中創建檔案類型變數。我們將來不再支持圖片上傳功能。",
"common.accessAPIReference": "訪問 API",
"common.accessPointDescription": "設定 Web 應用、API、MCP 和觸發器",
"common.addBlock": "新增節點",
"common.addDescription": "新增描述...",
"common.addFailureBranch": "新增 Fail Branch",
@@ -168,6 +169,7 @@
"common.currentView": "當前檢視",
"common.currentWorkflow": "當前工作流程",
"common.debugAndPreview": "預覽",
"common.deployDescription": "將版本部署至環境",
"common.disconnect": "斷開",
"common.draftSaveFailed": "草稿儲存失敗",
"common.duplicate": "複製",
@@ -216,11 +218,15 @@
"common.needConnectTip": "此節點尚未連接到其他節點",
"common.needOutputNode": "必須新增輸出節點",
"common.needStartNode": "至少必須新增一個起始節點",
"common.noChanges": "沒有變更",
"common.noHistory": "無歷史記錄",
"common.noVar": "沒有變數",
"common.notPublishedYet": "尚未發佈",
"common.notRunning": "尚未運行",
"common.onFailure": "失敗時",
"common.openInExplore": "在“探索”中打開",
"common.openWebApp": "開啟 Web 應用",
"common.openWebAppDescription": "在新分頁中開啟已發佈的應用",
"common.output": "輸出",
"common.overwriteAndImport": "覆蓋和導入",
"common.parallel": "並行",
@@ -237,10 +243,12 @@
"common.processData": "資料處理",
"common.publish": "發佈",
"common.publishToMarketplace": "發佈到 Marketplace",
"common.publishToMarketplaceDescription": "與社群分享此應用",
"common.publishToMarketplaceFailed": "發佈到 Marketplace 失敗",
"common.publishUpdate": "發布更新",
"common.published": "已發佈",
"common.publishedAt": "發佈於",
"common.publishedBy": "{{author}} 發佈於 {{time}}",
"common.publishingToMarketplace": "發佈中...",
"common.redo": "重做",
"common.restart": "重新開始",
@@ -250,6 +258,7 @@
"common.runApp": "運行",
"common.runHistory": "運行歷史",
"common.running": "運行中",
"common.savedAt": "{{time}}已儲存",
"common.scheduleTriggerRunFailed": "排程觸發執行失敗",
"common.searchVar": "搜索變數",
"common.setVarValuePlaceholder": "設置變數值",
@@ -264,6 +273,7 @@
"common.tagBound": "使用此標籤的應用程式數量",
"common.undo": "復原",
"common.unpublished": "未發佈",
"common.unpublishedChanges": "有未發佈的變更",
"common.update": "更新",
"common.variableNamePlaceholder": "變數名",
"common.versionHistory": "版本歷史",
@@ -273,8 +283,12 @@
"common.webhookDebugFailed": "Webhook 除錯失敗",
"common.webhookDebugRequestFailed": "Webhook 除錯請求失敗",
"common.workflowAsTool": "工作流程作為工具",
"common.workflowAsToolDescription": "在其他應用中作為工具使用",
"common.workflowAsToolDisabledHint": "發布最新的工作流程,並確保在將其配置為工具之前有一個已連接的使用者輸入節點。",
"common.workflowAsToolReady": "已就緒",
"common.workflowAsToolReconfigure": "重新設定",
"common.workflowAsToolTip": "工作流更新後需要重新配置工具參數",
"common.workflowAsToolUpdateNeeded": "需要更新",
"common.workflowProcess": "工作流",
"common.workflowProcessFailed": "工作流運行失敗",
"common.workflowProcessPaused": "工作流運行已暫停",
@@ -1312,6 +1326,7 @@
"versionHistory.filter.onlyYours": "只有妳的",
"versionHistory.filter.reset": "重置過濾器",
"versionHistory.latest": "最新",
"versionHistory.nameIt": "命名",
"versionHistory.nameThisVersion": "給這個版本命名",
"versionHistory.releaseNotesPlaceholder": "描述發生了什麼變化",
"versionHistory.restorationTip": "版本恢復後,當前草稿將被覆蓋。",
@@ -0,0 +1,52 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useUpdateWorkflow } from '../use-workflow'
const mockPatch = vi.hoisted(() => vi.fn())
vi.mock('../base', () => ({
del: vi.fn(),
get: vi.fn(),
patch: (...args: unknown[]) => mockPatch(...args),
post: vi.fn(),
put: vi.fn(),
}))
const createWrapper = (queryClient: QueryClient) =>
function Wrapper({ children }: { children: ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
describe('useUpdateWorkflow', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPatch.mockResolvedValue({})
})
it('should invalidate workflow version history after updating version information', async () => {
const queryClient = new QueryClient({
defaultOptions: {
mutations: { retry: false },
queries: { retry: false },
},
})
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
const { result } = renderHook(() => useUpdateWorkflow(), {
wrapper: createWrapper(queryClient),
})
await act(async () => {
await result.current.mutateAsync({
url: '/apps/app-1/workflows/workflow-1',
title: 'Release 1',
releaseNotes: 'Notes',
})
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['workflow', 'versionHistory'],
})
})
})
+18 -3
View File
@@ -11,19 +11,28 @@ import type {
WorkflowConfigResponse,
WorkflowRunHistoryResponse,
} from '@/types/workflow'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
queryOptions,
useInfiniteQuery,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { del, get, patch, post, put } from './base'
import { useInvalid, useReset } from './use-base'
import { getFlowPrefix } from './utils'
const NAME_SPACE = 'workflow'
export const useAppWorkflow = (appID: string) => {
return useQuery<FetchWorkflowDraftResponse | null>({
export const appWorkflowQueryOptions = (appID: string) =>
queryOptions({
enabled: !!appID,
queryKey: [NAME_SPACE, 'publish', appID],
queryFn: () => get<FetchWorkflowDraftResponse | null>(`/apps/${appID}/workflows/publish`),
})
export const useAppWorkflow = (appID: string) => {
return useQuery(appWorkflowQueryOptions(appID))
}
const WorkflowRunHistoryKey = [NAME_SPACE, 'runHistory']
@@ -97,6 +106,7 @@ export const useResetWorkflowVersionHistory = () => {
}
export const useUpdateWorkflow = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (params: UpdateWorkflowParams) =>
@@ -106,6 +116,11 @@ export const useUpdateWorkflow = () => {
marked_comment: params.releaseNotes,
},
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: [...WorkflowVersionHistoryKey],
})
},
})
}
-4
View File
@@ -73,10 +73,6 @@ export const getLoopSingleNodeRunUrl = (
return `${getFlowPrefix(flowType)}/${flowId}/${isChatFlow ? 'advanced-chat/' : ''}workflows/draft/loop/nodes/${nodeId}/run`
}
export const fetchPublishedWorkflow = (url: string) => {
return get<FetchWorkflowDraftResponse | null>(url)
}
export const stopWorkflowRun = (url: string) => {
return post<CommonResponse>(url)
}