mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
fix(app): align package import and export feedback (#42732)
This commit is contained in:
@@ -5,5 +5,5 @@ Feature: Export app package
|
||||
And there is an existing E2E completion app available for testing
|
||||
When I open the apps console
|
||||
And I open the options menu for the last created E2E app
|
||||
And I click "Export DSL" in the app options menu
|
||||
And I click "Export App" in the app options menu
|
||||
Then an app package named after the app should be downloaded
|
||||
|
||||
@@ -2490,21 +2490,11 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/components/rag-pipeline-children.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/hooks/use-DSL.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/rag-pipeline/hooks/use-input-fields.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -2679,12 +2669,7 @@
|
||||
},
|
||||
"web/app/components/workflow-app/components/workflow-children.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow-app/hooks/use-DSL.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts": {
|
||||
@@ -4130,11 +4115,6 @@
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/update-dsl-modal.tsx": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/utils/__tests__/node-navigation.spec.ts": {
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 3
|
||||
|
||||
@@ -72,8 +72,8 @@ vi.mock('@/app/components/workflow/update-dsl-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
DSLExportConfirmContent: ({
|
||||
vi.mock('@/app/components/app/export-confirm-modal', () => ({
|
||||
AppExportConfirmContent: ({
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: {
|
||||
@@ -129,7 +129,7 @@ const defaultProps = {
|
||||
setSecretEnvList: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
onCopy: vi.fn(),
|
||||
onExport: vi.fn(async () => {}),
|
||||
onExport: vi.fn(async () => true),
|
||||
isExporting: false,
|
||||
exportCheck: vi.fn(),
|
||||
handleConfirmExport: vi.fn(async () => {}),
|
||||
@@ -218,7 +218,7 @@ describe('AppInfoModals', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should render DSLExportConfirmModal when secretEnvList is not empty', async () => {
|
||||
it('should render AppExportConfirmModal when secretEnvList is not empty', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<AppInfoModals
|
||||
@@ -241,7 +241,7 @@ describe('AppInfoModals', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render DSLExportConfirmModal when secretEnvList is empty', async () => {
|
||||
it('should not render AppExportConfirmModal when secretEnvList is empty', async () => {
|
||||
await act(async () => {
|
||||
render(<AppInfoModals {...defaultProps} activeModal={null} />)
|
||||
})
|
||||
@@ -337,7 +337,7 @@ describe('AppInfoModals', () => {
|
||||
expect(defaultProps.exportCheck).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call setSecretEnvList with empty array when closing DSLExportConfirmModal', async () => {
|
||||
it('should call setSecretEnvList with empty array when closing AppExportConfirmModal', async () => {
|
||||
const user = userEvent.setup()
|
||||
await act(async () => {
|
||||
render(
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('AppInfoTrigger', () => {
|
||||
expect(screen.getAllByRole('menuitem').map((item) => item.textContent)).toEqual([
|
||||
'app.editApp',
|
||||
'app.duplicate',
|
||||
'app.export',
|
||||
'app.exportApp',
|
||||
'common.operation.delete',
|
||||
'app.switch',
|
||||
])
|
||||
@@ -142,10 +142,10 @@ describe('AppInfoTrigger', () => {
|
||||
|
||||
await user.click(getOperationsTrigger())
|
||||
|
||||
expect(screen.getByRole('menuitem', { name: 'workflow.common.importDSL' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'app.importApp' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: 'app.switch' })).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('menuitem', { name: 'workflow.common.importDSL' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'app.importApp' }))
|
||||
expect(props.openModal).toHaveBeenCalledWith('importDSL')
|
||||
})
|
||||
|
||||
@@ -155,14 +155,14 @@ describe('AppInfoTrigger', () => {
|
||||
const { rerender } = render(<AppInfoTrigger {...props} />)
|
||||
|
||||
await user.click(getOperationsTrigger())
|
||||
expect(screen.getByRole('menuitem', { name: 'app.export' })).toHaveAttribute(
|
||||
expect(screen.getByRole('menuitem', { name: 'app.exportApp' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
|
||||
const readyProps = createProps()
|
||||
rerender(<AppInfoTrigger {...readyProps} />)
|
||||
await user.click(screen.getByRole('menuitem', { name: 'app.export' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'app.exportApp' }))
|
||||
|
||||
expect(readyProps.exportCheck).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -408,6 +408,14 @@ describe('useAppInfoActions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the export failure result for the confirmation dialog', async () => {
|
||||
mockExportAppDsl.mockResolvedValue({ status: 'failed' })
|
||||
const { result } = renderHook(() => useAppInfoActions({}))
|
||||
await act(async () => {
|
||||
await expect(result.current.onExport(true)).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onExport - early return', () => {
|
||||
it('should not export when appDetail is undefined', async () => {
|
||||
mockAppDetail = undefined
|
||||
@@ -473,6 +481,25 @@ describe('useAppInfoActions', () => {
|
||||
})
|
||||
|
||||
describe('handleConfirmExport', () => {
|
||||
it('keeps the warning open after failure so the user can retry', async () => {
|
||||
mockAppDetail = { ...mockAppDetail, mode: AppModeEnum.WORKFLOW }
|
||||
mockExportWorkflowAppDsl
|
||||
.mockResolvedValueOnce({ status: 'failed' })
|
||||
.mockResolvedValueOnce({ status: 'downloaded' })
|
||||
const { result } = renderHook(() => useAppInfoActions({}))
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
})
|
||||
await act(async () => {
|
||||
await result.current.handleConfirmExport()
|
||||
})
|
||||
expect(result.current.activeModal).toBe('exportWarning')
|
||||
await act(async () => {
|
||||
await result.current.handleConfirmExport()
|
||||
})
|
||||
expect(result.current.activeModal).toBeNull()
|
||||
})
|
||||
|
||||
it('should export directly when no secret env variables', async () => {
|
||||
mockAppDetail = { ...mockAppDetail, mode: AppModeEnum.WORKFLOW }
|
||||
const { result } = renderHook(() => useAppInfoActions({}))
|
||||
|
||||
@@ -18,7 +18,7 @@ import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-u
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { DSLExportConfirmContent } from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||
import { AppExportConfirmContent } from '@/app/components/app/export-confirm-modal'
|
||||
import dynamic from '@/next/dynamic'
|
||||
|
||||
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), {
|
||||
@@ -42,7 +42,7 @@ type AppInfoModalsProps = {
|
||||
setSecretEnvList: (list: EnvironmentVariableItemResponse[]) => void
|
||||
onEdit: CreateAppModalProps['onConfirm']
|
||||
onCopy: DuplicateAppModalProps['onConfirm']
|
||||
onExport: (include?: boolean) => Promise<void>
|
||||
onExport: (include?: boolean) => Promise<boolean>
|
||||
isExporting: boolean
|
||||
exportCheck: () => void
|
||||
handleConfirmExport: () => Promise<void>
|
||||
@@ -65,7 +65,6 @@ const AppInfoModals = ({
|
||||
}: AppInfoModalsProps) => {
|
||||
const { t } = useTranslation(['app', 'common', 'workflow'])
|
||||
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
|
||||
const [isSecretExporting, setIsSecretExporting] = useState(false)
|
||||
const exportConfirmLabelId = React.useId()
|
||||
const isDeleteConfirmDisabled = confirmDeleteInput !== appDetail.name
|
||||
const exportDialogMode =
|
||||
@@ -88,11 +87,11 @@ const AppInfoModals = ({
|
||||
|
||||
const handleExportDialogOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open || isExporting || isSecretExporting) return
|
||||
if (open || isExporting) return
|
||||
|
||||
handleExportDialogClose()
|
||||
},
|
||||
[handleExportDialogClose, isExporting, isSecretExporting],
|
||||
[handleExportDialogClose, isExporting],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -200,11 +199,11 @@ const AppInfoModals = ({
|
||||
)}
|
||||
<AlertDialog open={isExportDialogOpen} onOpenChange={handleExportDialogOpenChange}>
|
||||
{exportDialogMode === 'secret' ? (
|
||||
<DSLExportConfirmContent
|
||||
<AppExportConfirmContent
|
||||
envList={secretEnvList}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
onExportingChange={setIsSecretExporting}
|
||||
isExporting={isExporting}
|
||||
/>
|
||||
) : (
|
||||
exportDialogMode === 'warning' && (
|
||||
|
||||
@@ -68,7 +68,7 @@ const AppInfoTrigger = ({
|
||||
? [
|
||||
{
|
||||
id: 'export',
|
||||
title: t(($) => $.export, { ns: 'app' }),
|
||||
title: t(($) => $.exportApp, { ns: 'app' }),
|
||||
icon: 'i-ri-file-download-line',
|
||||
onClick: exportCheck,
|
||||
loading: isExporting,
|
||||
@@ -80,7 +80,7 @@ const AppInfoTrigger = ({
|
||||
? [
|
||||
{
|
||||
id: 'import',
|
||||
title: t(($) => $['common.importDSL'], { ns: 'workflow' }),
|
||||
title: t(($) => $.importApp, { ns: 'app' }),
|
||||
icon: 'i-ri-file-upload-line',
|
||||
onClick: () => openModal('importDSL'),
|
||||
},
|
||||
|
||||
@@ -281,12 +281,13 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
|
||||
const onExport = useCallback(
|
||||
async (include = false) => {
|
||||
if (!appDetail) return
|
||||
await exportAppDsl({
|
||||
if (!appDetail) return false
|
||||
const result = await exportAppDsl({
|
||||
appId: appDetail.id,
|
||||
appName: appDetail.name,
|
||||
includeSecret: include,
|
||||
})
|
||||
return result.status === 'downloaded'
|
||||
},
|
||||
[appDetail, exportAppDsl],
|
||||
)
|
||||
@@ -301,14 +302,15 @@ export function useAppInfoActions({ resetKey }: UseAppInfoActionsParams) {
|
||||
}, [appDetail, isExporting, onExport, setActiveModal])
|
||||
|
||||
const handleConfirmExport = useCallback(async () => {
|
||||
if (!appDetail) return
|
||||
if (!appDetail || isExporting) return
|
||||
const result = await exportWorkflowAppDsl({
|
||||
appId: appDetail.id,
|
||||
appName: appDetail.name,
|
||||
})
|
||||
if (result.status === 'failed') return
|
||||
if (result.status === 'confirmation-required') setSecretEnvList(result.secretEnvList)
|
||||
closeModal()
|
||||
}, [appDetail, closeModal, exportWorkflowAppDsl, setSecretEnvList])
|
||||
}, [appDetail, closeModal, exportWorkflowAppDsl, isExporting, setSecretEnvList])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
if (!appDetail) return
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import AppExportConfirmModal from '@/app/components/app/export-confirm-modal'
|
||||
import { expectLoadingButton } from '@/test/button'
|
||||
|
||||
const envList = [
|
||||
{
|
||||
id: 'env-1',
|
||||
name: 'SECRET_TOKEN',
|
||||
value: 'masked-value',
|
||||
value_type: 'secret' as const,
|
||||
description: 'secret token',
|
||||
},
|
||||
]
|
||||
|
||||
describe('AppExportConfirmModal', () => {
|
||||
it('should render environment rows and close when cancel is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={envList}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('SECRET_TOKEN')).toBeInTheDocument()
|
||||
expect(screen.getByText('masked-value')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should confirm with exportSecrets=false by default', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={envList}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'app.exportApp' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should confirm with exportSecrets=true after toggling the checkbox', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={envList}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'app.exportSecret.title' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should also toggle exportSecrets when the label text is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={envList}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByText('workflow.env.export.checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'app.exportSecret.title' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('blocks confirmation and dismissal while the export request is pending', async () => {
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
const onClose = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
envList={envList}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
isExporting
|
||||
/>,
|
||||
)
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'common.operation.exporting' })
|
||||
expectLoadingButton(confirmButton)
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'true')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeDisabled()
|
||||
await user.click(confirmButton)
|
||||
await user.keyboard('{Escape}')
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('explains that secret values are included in the app package', () => {
|
||||
render(
|
||||
<AppExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={envList}
|
||||
onConfirm={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('alertdialog')).toHaveAccessibleDescription(
|
||||
'app.exportSecret.description',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { act, render, renderHook, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import AppExportConfirmModal from '../export-confirm-modal'
|
||||
import { useExportAppDsl, useExportWorkflowAppDsl } from '../use-export-app-dsl'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -12,11 +14,20 @@ const mocks = vi.hoisted(() => ({
|
||||
toastPromise: vi.fn(
|
||||
async (
|
||||
promise: Promise<unknown>,
|
||||
options: { success: (format: unknown) => { title: string }; error: { title: string } },
|
||||
options: {
|
||||
success: (format: unknown) => { title: string }
|
||||
error: (error: unknown) => { title: string; description?: string }
|
||||
},
|
||||
) => {
|
||||
const format = await promise
|
||||
mocks.toastSuccess(options.success(format).title)
|
||||
return format
|
||||
try {
|
||||
const format = await promise
|
||||
mocks.toastSuccess(options.success(format).title)
|
||||
return format
|
||||
} catch (error) {
|
||||
const { title, description } = options.error(error)
|
||||
mocks.toastError(title, { description })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
),
|
||||
}))
|
||||
@@ -66,9 +77,62 @@ function createWrapper() {
|
||||
describe('useExportAppDsl', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.exportAppDsl.mockReset()
|
||||
mocks.getEnvironmentVariables.mockResolvedValue({ items: [] })
|
||||
})
|
||||
|
||||
it('keeps the secret confirmation open after a failed request and retries the same choice', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
let rejectExport!: (reason: unknown) => void
|
||||
mocks.exportAppDsl
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectExport = reject
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(new File(['package'], 'app.ifpkg'))
|
||||
|
||||
function ExportConfirmation() {
|
||||
const { exportAppDsl, isExporting } = useExportAppDsl()
|
||||
return (
|
||||
<AppExportConfirmModal
|
||||
envList={[{ name: 'TOKEN', value: 'masked' }]}
|
||||
isExporting={isExporting}
|
||||
onClose={onClose}
|
||||
onConfirm={async (includeSecret) => {
|
||||
const result = await exportAppDsl({ appId: 'app-1', appName: 'App', includeSecret })
|
||||
return result.status === 'downloaded'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
render(<ExportConfirmation />, { wrapper: createWrapper() })
|
||||
await user.click(screen.getByRole('checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'app.exportSecret.title' }))
|
||||
const pendingButton = await screen.findByRole('button', { name: 'common.operation.exporting' })
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-disabled', 'true')
|
||||
await user.click(pendingButton)
|
||||
expect(mocks.exportAppDsl).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
rejectExport(new Response(JSON.stringify({ message: 'Skill unavailable' }), { status: 400 }))
|
||||
})
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('checkbox')).toBeChecked()
|
||||
expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith('app.exportAppFailed', {
|
||||
description: 'Skill unavailable',
|
||||
})
|
||||
await user.click(await screen.findByRole('button', { name: 'app.exportSecret.title' }))
|
||||
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1))
|
||||
expect(mocks.exportAppDsl).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ query: expect.objectContaining({ include_secret: true }) }),
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
expect(mocks.downloadBlob).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['download.ifpkg', 'download.ifpkg'],
|
||||
['blob', 'Support Agent.ifpkg'],
|
||||
@@ -174,6 +238,18 @@ describe('useExportAppDsl', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the localized fallback when the export response has no readable message', async () => {
|
||||
mocks.exportAppDsl.mockRejectedValue(new Response('Bad gateway', { status: 502 }))
|
||||
const { result } = renderHook(() => useExportAppDsl(), { wrapper: createWrapper() })
|
||||
await act(async () => {
|
||||
await result.current.exportAppDsl({ appId: 'app-id', appName: 'App' })
|
||||
})
|
||||
expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith('app.exportAppFailed', {
|
||||
description: undefined,
|
||||
})
|
||||
expect(mocks.downloadBlob).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('exports through the generated client and hands the YAML file to the browser', async () => {
|
||||
mocks.exportAppDsl.mockResolvedValue({ data: 'kind: app\nversion: 0.1.5\n' })
|
||||
const { result } = renderHook(() => useExportAppDsl(), { wrapper: createWrapper() })
|
||||
@@ -241,10 +317,15 @@ describe('useExportAppDsl', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([undefined, 'ifpkg'] as const)(
|
||||
it.each([undefined, 'ifpkg', 'yaml'] as const)(
|
||||
'lets the promise toast own %s export errors without triggering a download',
|
||||
async (format) => {
|
||||
mocks.exportAppDsl.mockRejectedValue(new Error('Export failed'))
|
||||
mocks.exportAppDsl.mockRejectedValue(
|
||||
Response.json(
|
||||
{ message: "Package contains unusable Skill 'news-source-verification'" },
|
||||
{ status: 500 },
|
||||
),
|
||||
)
|
||||
const { result } = renderHook(() => useExportAppDsl(), { wrapper: createWrapper() })
|
||||
|
||||
await act(async () => {
|
||||
@@ -260,9 +341,13 @@ describe('useExportAppDsl', () => {
|
||||
expect(mocks.toastPromise).toHaveBeenCalledWith(
|
||||
expect.any(Promise),
|
||||
expect.objectContaining({
|
||||
error: { title: format === 'ifpkg' ? 'app.exportAppFailed' : 'app.exportFailed' },
|
||||
error: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
expect(mocks.toastError).toHaveBeenCalledExactlyOnceWith(
|
||||
format === 'yaml' ? 'app.exportFailed' : 'app.exportAppFailed',
|
||||
{ description: "Package contains unusable Skill 'news-source-verification'" },
|
||||
)
|
||||
expect(mocks.downloadBlob).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
@@ -342,7 +427,9 @@ describe('useExportWorkflowAppDsl', () => {
|
||||
).resolves.toEqual({ status: 'failed' })
|
||||
})
|
||||
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('app.exportFailed')
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('app.exportAppFailed', {
|
||||
description: 'Draft unavailable',
|
||||
})
|
||||
expect(mocks.exportAppDsl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -583,6 +583,40 @@ describe('CreateFromDSLModal', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'navigates after a dependency check fails for an import (confirmed: %s)',
|
||||
async (confirmed) => {
|
||||
const user = userEvent.setup()
|
||||
const completed = {
|
||||
status: DSLImportStatus.COMPLETED,
|
||||
app_id: 'created-app',
|
||||
app_mode: AppModeEnum.WORKFLOW,
|
||||
}
|
||||
mockImportDSL.mockResolvedValue(
|
||||
confirmed ? { id: 'pending-import', status: DSLImportStatus.PENDING } : completed,
|
||||
)
|
||||
mockImportDSLConfirm.mockResolvedValue(completed)
|
||||
// The dependency owner has already reported its request failure.
|
||||
mockHandleCheckPluginDependencies.mockResolvedValue(false)
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<CreateFromDSLModal
|
||||
show
|
||||
onClose={onClose}
|
||||
activeTab={CreateFromDSLModalTab.FROM_URL}
|
||||
dslUrl="https://example.com/app.yml"
|
||||
/>,
|
||||
)
|
||||
await user.click(getCreateButton())
|
||||
if (confirmed)
|
||||
await user.click(await screen.findByRole('button', { name: /newApp\.Confirm/ }))
|
||||
|
||||
await waitFor(() => expect(mockGetRedirection).toHaveBeenCalled())
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(toastMocks.error).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
it('should surface Agent warnings after confirming a pending import', async () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'agent-import-pending',
|
||||
@@ -928,7 +962,9 @@ describe('CreateFromDSLModal', () => {
|
||||
status: DSLImportStatus.FAILED,
|
||||
error: 'Invalid YAML format',
|
||||
})
|
||||
mockImportDSL.mockRejectedValueOnce(new Error('boom'))
|
||||
mockImportDSL.mockRejectedValueOnce(
|
||||
Response.json({ message: 'Package checksum mismatch' }, { status: 400 }),
|
||||
)
|
||||
|
||||
const { rerender } = render(
|
||||
<CreateFromDSLModal
|
||||
@@ -942,7 +978,10 @@ describe('CreateFromDSLModal', () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(getCreateButton())
|
||||
})
|
||||
expect(toastMocks.error).toHaveBeenCalledWith('Invalid YAML format')
|
||||
expect(toastMocks.error).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringMatching(/newApp\.appCreateFailed/),
|
||||
{ description: 'Invalid YAML format' },
|
||||
)
|
||||
|
||||
rerender(
|
||||
<CreateFromDSLModal
|
||||
@@ -959,6 +998,7 @@ describe('CreateFromDSLModal', () => {
|
||||
expect(toastMocks.error).toHaveBeenCalledTimes(2)
|
||||
expect(toastMocks.error).toHaveBeenLastCalledWith(
|
||||
expect.stringMatching(/(?:^|\.)newApp\.appCreateFailed(?=$|:)/),
|
||||
{ description: 'Package checksum mismatch' },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1002,7 +1042,10 @@ describe('CreateFromDSLModal', () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)newApp\.Confirm(?=$|:)/ })[0]!)
|
||||
})
|
||||
expect(toastMocks.error).toHaveBeenCalledWith('Confirm failed')
|
||||
expect(toastMocks.error).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.stringMatching(/newApp\.appCreateFailed/),
|
||||
{ description: 'Confirm failed' },
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)newApp\.Confirm(?=$|:)/ })[0]!)
|
||||
@@ -1010,6 +1053,7 @@ describe('CreateFromDSLModal', () => {
|
||||
expect(toastMocks.error).toHaveBeenCalledTimes(2)
|
||||
expect(toastMocks.error).toHaveBeenLastCalledWith(
|
||||
expect.stringMatching(/(?:^|\.)newApp\.appCreateFailed(?=$|:)/),
|
||||
{ description: 'boom' },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@ import { AppModeEnum as AppMode } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { trackCreateApp } from '@/utils/create-app-tracking'
|
||||
import { resolveImportedAppRedirectionTarget } from '@/utils/imported-app-redirection'
|
||||
import { getAppTransferErrorMessage } from '../transfer-error'
|
||||
import DSLConfirmModal from './dsl-confirm-modal'
|
||||
import DSLImportWarningDescription from './dsl-import-warning-description'
|
||||
import { CreateFromDSLModalTab } from './types'
|
||||
@@ -99,7 +100,7 @@ function CreateFromDSLModal({
|
||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||
const [pendingImport, setPendingImport] = useState<PendingImport | null>(null)
|
||||
const { mutateAsync: requestImport } = useMutation(
|
||||
consoleQuery.apps.imports.post.mutationOptions(),
|
||||
consoleQuery.apps.imports.post.mutationOptions({ context: { silent: true } }),
|
||||
)
|
||||
const importMutation = useMutation({
|
||||
mutationFn: async (source: ImportSource) => {
|
||||
@@ -124,7 +125,9 @@ function CreateFromDSLModal({
|
||||
},
|
||||
})
|
||||
const confirmImportMutation = useMutation(
|
||||
consoleQuery.apps.imports.byImportId.confirm.post.mutationOptions(),
|
||||
consoleQuery.apps.imports.byImportId.confirm.post.mutationOptions({
|
||||
context: { silent: true },
|
||||
}),
|
||||
)
|
||||
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
@@ -181,6 +184,7 @@ function CreateFromDSLModal({
|
||||
)
|
||||
if (!response.app_id || !appMode) return
|
||||
|
||||
// Dependency checks own their error feedback; a created app remains navigable.
|
||||
await handleCheckPluginDependencies(response.app_id)
|
||||
const redirectionTarget = await resolveImportedAppRedirectionTarget({
|
||||
id: response.app_id,
|
||||
@@ -210,12 +214,18 @@ function CreateFromDSLModal({
|
||||
return
|
||||
}
|
||||
|
||||
toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
||||
toast.error(
|
||||
t(($) => $['newApp.appCreateFailed'], { ns: 'app' }),
|
||||
{
|
||||
description: response.error || undefined,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: ImportFormValues) => {
|
||||
if (isAppQuotaUnavailable || isAppsFull || isImporting) return
|
||||
|
||||
let response: Import
|
||||
try {
|
||||
let source: ImportSource
|
||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
|
||||
@@ -227,31 +237,46 @@ function CreateFromDSLModal({
|
||||
source = { type: CreateFromDSLModalTab.FROM_URL, url: yamlUrl }
|
||||
}
|
||||
|
||||
const response = await importMutation.mutateAsync(source)
|
||||
await handleImportResponse(response)
|
||||
} catch {
|
||||
toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
||||
response = await importMutation.mutateAsync(source)
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(($) => $['newApp.appCreateFailed'], { ns: 'app' }),
|
||||
{
|
||||
description: await getAppTransferErrorMessage(error),
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
await handleImportResponse(response)
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!pendingImport || isConfirming) return
|
||||
|
||||
let response: Import
|
||||
try {
|
||||
const response = await confirmImportMutation.mutateAsync({
|
||||
response = await confirmImportMutation.mutateAsync({
|
||||
params: { import_id: pendingImport.id },
|
||||
})
|
||||
if (response.status === 'completed' || response.status === 'completed-with-warnings') {
|
||||
setPendingImport(null)
|
||||
await handleCompletedImport(response)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.status === 'failed')
|
||||
toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
||||
} catch {
|
||||
toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(($) => $['newApp.appCreateFailed'], { ns: 'app' }),
|
||||
{ description: await getAppTransferErrorMessage(error) },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.status === 'completed' || response.status === 'completed-with-warnings') {
|
||||
setPendingImport(null)
|
||||
await handleCompletedImport(response)
|
||||
return
|
||||
}
|
||||
|
||||
if (response.status === 'failed')
|
||||
toast.error(
|
||||
t(($) => $['newApp.appCreateFailed'], { ns: 'app' }),
|
||||
{ description: response.error || undefined },
|
||||
)
|
||||
}
|
||||
|
||||
const handleTabChange = (value: string | number) => {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
'use client'
|
||||
import type { EnvironmentVariableItemResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type AppExportConfirmModalProps = {
|
||||
envList: Pick<EnvironmentVariableItemResponse, 'name' | 'value'>[]
|
||||
onConfirm: (includeSecret: boolean) => Promise<boolean>
|
||||
isExporting: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const AppExportConfirmContent = ({
|
||||
envList,
|
||||
onConfirm,
|
||||
onClose,
|
||||
isExporting,
|
||||
}: AppExportConfirmModalProps) => {
|
||||
const { t } = useTranslation(['app', 'workflow', 'common'])
|
||||
|
||||
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
|
||||
const exportButtonLabelId = React.useId()
|
||||
|
||||
const submit = useCallback(async () => {
|
||||
if (isExporting) return
|
||||
|
||||
if (await onConfirm(exportSecrets)) onClose()
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm])
|
||||
|
||||
return (
|
||||
<AlertDialogContent className="w-120">
|
||||
<div className="px-6 pt-6">
|
||||
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(($) => $['exportSecret.title'], { ns: 'app' })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="mt-2 mb-6 system-sm-regular text-text-secondary">
|
||||
{t(($) => $['exportSecret.description'], { ns: 'app' })}
|
||||
</AlertDialogDescription>
|
||||
<div className="relative">
|
||||
<table className="w-full border-separate border-spacing-0 rounded-lg border border-divider-regular shadow-xs">
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<tr>
|
||||
<th className="h-7 w-55 border-r border-b border-divider-regular pl-3 text-left font-[weight:inherit]">
|
||||
{t(($) => $['env.export.name'], { ns: 'workflow' })}
|
||||
</th>
|
||||
<th className="h-7 border-b border-divider-regular pl-3 text-left font-[weight:inherit]">
|
||||
{t(($) => $['env.export.value'], { ns: 'workflow' })}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{envList.map((env, index) => (
|
||||
<tr key={env.name}>
|
||||
<td
|
||||
className={cn(
|
||||
'h-7 border-r border-divider-regular pl-3 system-xs-medium',
|
||||
index + 1 !== envList.length && 'border-b border-divider-regular',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-50 items-center gap-1">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-vender-line-others-env size-4 shrink-0 text-util-colors-violet-violet-600"
|
||||
/>
|
||||
<div className="truncate text-text-primary">{env.name}</div>
|
||||
<div className="shrink-0 text-text-tertiary">
|
||||
{t(($) => $['env.export.secret'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-ri-lock-2-line size-3 shrink-0 text-text-tertiary"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={cn(
|
||||
'h-7 pl-3',
|
||||
index + 1 !== envList.length && 'border-b border-divider-regular',
|
||||
)}
|
||||
>
|
||||
<div className="truncate system-xs-regular text-text-secondary">
|
||||
{typeof env.value === 'object'
|
||||
? JSON.stringify(env.value)
|
||||
: String(env.value)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<label className={cn('mt-4 flex gap-2', !isExporting && 'cursor-pointer')}>
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={exportSecrets}
|
||||
disabled={isExporting}
|
||||
onCheckedChange={setExportSecrets}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-sm text-left system-sm-medium text-text-primary outline-hidden',
|
||||
isExporting && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
{t(($) => $['env.export.checkbox'], { ns: 'workflow' })}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton disabled={isExporting}>
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={isExporting}
|
||||
aria-labelledby={exportButtonLabelId}
|
||||
onClick={submit}
|
||||
>
|
||||
<span id={exportButtonLabelId}>
|
||||
{isExporting
|
||||
? t(($) => $['operation.exporting'], { ns: 'common' })
|
||||
: exportSecrets
|
||||
? t(($) => $['exportSecret.title'], { ns: 'app' })
|
||||
: t(($) => $.exportApp, { ns: 'app' })}
|
||||
</span>
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
const AppExportConfirmModal = (props: AppExportConfirmModalProps) => {
|
||||
const { envList, onClose, isExporting } = props
|
||||
const isDialogOpen = envList.length > 0
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open || isExporting) return
|
||||
|
||||
onClose()
|
||||
},
|
||||
[isExporting, onClose],
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertDialog open={isDialogOpen} onOpenChange={handleOpenChange}>
|
||||
<AppExportConfirmContent {...props} />
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppExportConfirmModal
|
||||
@@ -0,0 +1,15 @@
|
||||
export async function getAppTransferErrorMessage(error: unknown): Promise<string | undefined> {
|
||||
if (error instanceof Response) {
|
||||
try {
|
||||
const body: unknown = await error.clone().json()
|
||||
if (body && typeof body === 'object') {
|
||||
if ('message' in body && typeof body.message === 'string') return body.message || undefined
|
||||
if ('error' in body && typeof body.error === 'string') return body.error || undefined
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return error instanceof Error ? error.message || undefined : undefined
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { getAppTransferErrorMessage } from './transfer-error'
|
||||
|
||||
type ExportAppDslInput = {
|
||||
appId: string
|
||||
@@ -85,7 +86,10 @@ export async function exportAppDslFile({
|
||||
}
|
||||
|
||||
async function downloadAppDsl(input: ExportAppDslInput, messages: ExportAppDslMessages) {
|
||||
await toast.promise(exportAppDslFile(input), {
|
||||
const exportPromise = exportAppDslFile(input).catch(async (error: unknown) => {
|
||||
throw new Error(await getAppTransferErrorMessage(error), { cause: error })
|
||||
})
|
||||
await toast.promise(exportPromise, {
|
||||
loading: {
|
||||
title: messages.loading,
|
||||
},
|
||||
@@ -93,9 +97,10 @@ async function downloadAppDsl(input: ExportAppDslInput, messages: ExportAppDslMe
|
||||
title: format === 'ifpkg' ? messages.packageSuccess : messages.success,
|
||||
timeout: 3000,
|
||||
}),
|
||||
error: {
|
||||
title: input.format === 'ifpkg' ? messages.packageError : messages.error,
|
||||
},
|
||||
error: (error) => ({
|
||||
title: input.format === 'yaml' ? messages.error : messages.packageError,
|
||||
description: error instanceof Error ? error.message || undefined : undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
return { status: 'downloaded' } as const
|
||||
@@ -148,7 +153,7 @@ export function useExportWorkflowAppDsl() {
|
||||
try {
|
||||
secretEnvList = await getSecretEnvironmentVariables(input.appId)
|
||||
} catch (error) {
|
||||
toast.error(messages.error)
|
||||
toast.error(messages.packageError, { description: await getAppTransferErrorMessage(error) })
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@ vi.mock('@/next/dynamic', () => ({
|
||||
)
|
||||
}
|
||||
}
|
||||
if (fnString.includes('dsl-export-confirm-modal')) {
|
||||
if (fnString.includes('app/export-confirm-modal')) {
|
||||
return function MockDSLExportModal({
|
||||
onClose,
|
||||
onConfirm,
|
||||
@@ -800,7 +800,7 @@ describe('AppCard', () => {
|
||||
|
||||
expect(await screen.findByRole('menuitem', { name: 'app.editApp' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'app.duplicate' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'app.export' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'app.exportApp' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep card actions outside the card context menu trigger', async () => {
|
||||
@@ -838,7 +838,7 @@ describe('AppCard', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('app.duplicate')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('app.export')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.exportApp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show duplicate option on starred cards when user can create apps without app import export permission', async () => {
|
||||
@@ -854,7 +854,7 @@ describe('AppCard', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('app.duplicate')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('app.export')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.exportApp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show export option when dropdown menu is opened', async () => {
|
||||
@@ -863,7 +863,7 @@ describe('AppCard', () => {
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('app.export')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.exportApp')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1219,7 +1219,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
expect(mockAppDslExport.exportAppDsl).toHaveBeenCalledWith({
|
||||
@@ -1322,7 +1322,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1345,7 +1345,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1359,7 +1359,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1378,7 +1378,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1400,7 +1400,7 @@ describe('AppCard', () => {
|
||||
|
||||
fireEvent.click(getOperationsTrigger())
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('app.export'))
|
||||
fireEvent.click(screen.getByText('app.exportApp'))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -882,7 +882,7 @@ describe('List', () => {
|
||||
'data-step-by-step-tour-target',
|
||||
STEP_BY_STEP_TOUR_TARGETS.studioEmptyBlank,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: /app\.importDSL/ })).toHaveAttribute(
|
||||
expect(screen.getByRole('button', { name: /app\.importApp/ })).toHaveAttribute(
|
||||
'data-step-by-step-tour-target',
|
||||
STEP_BY_STEP_TOUR_TARGETS.studioEmptyDSL,
|
||||
)
|
||||
@@ -996,7 +996,7 @@ describe('List', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /app\.newApp\.startFromTemplate/ }))
|
||||
expect(screen.getByTestId('template-dialog'))!.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /app\.importDSL/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /app\.importApp/ }))
|
||||
expect(screen.getByTestId('create-dsl-modal'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -72,12 +72,9 @@ const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-m
|
||||
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const DSLExportConfirmModal = dynamic(
|
||||
() => import('@/app/components/workflow/dsl-export-confirm-modal'),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
)
|
||||
const AppExportConfirmModal = dynamic(() => import('@/app/components/app/export-confirm-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const OPERATIONS_MENU_POPUP_CLASS_NAME = 'min-w-[216px]'
|
||||
const APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE = new Set<AppPartial['mode']>([
|
||||
@@ -208,7 +205,7 @@ function AppCardOperationsMenuItems({
|
||||
onClick={(event) => handleMenuAction(event, onExport)}
|
||||
>
|
||||
<span className="system-sm-regular text-text-secondary">
|
||||
{t(($) => $.export, { ns: 'app' })}
|
||||
{t(($) => $.exportApp, { ns: 'app' })}
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -475,7 +472,8 @@ export function AppCardInteractions({
|
||||
}
|
||||
|
||||
const onExport = async (include = false) => {
|
||||
await exportAppDsl({ appId: app.id, appName: app.name, includeSecret: include })
|
||||
const result = await exportAppDsl({ appId: app.id, appName: app.name, includeSecret: include })
|
||||
return result.status === 'downloaded'
|
||||
}
|
||||
|
||||
const exportCheck = async () => {
|
||||
@@ -743,8 +741,9 @@ export function AppCardInteractions({
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
<AppExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
isExporting={isExporting}
|
||||
onConfirm={onExport}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
|
||||
@@ -62,7 +62,7 @@ function FirstEmptyState({
|
||||
{
|
||||
id: 'dsl',
|
||||
icon: <span aria-hidden className="i-ri-file-upload-line size-4" />,
|
||||
title: t(($) => $.importDSL, { ns: 'app' }),
|
||||
title: t(($) => $.importApp, { ns: 'app' }),
|
||||
description: t(($) => $['firstEmpty.importDescription'], { ns: 'app' }),
|
||||
onClick: onImportDSL,
|
||||
target: STEP_BY_STEP_TOUR_TARGETS.studioEmptyDSL,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PipelineExportConfirmModal from '../export-confirm-modal'
|
||||
|
||||
it('keeps pipeline DSL copy and the explicit secret opt-in after separating app exports', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn().mockResolvedValue(true)
|
||||
render(
|
||||
<PipelineExportConfirmModal
|
||||
isExporting={false}
|
||||
envList={[{ name: 'TOKEN', value: 'masked' }]}
|
||||
onConfirm={onConfirm}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('alertdialog')).toHaveAccessibleName('workflow.env.export.title')
|
||||
expect(screen.getByRole('button', { name: 'workflow.env.export.ignore' })).toBeEnabled()
|
||||
await user.click(screen.getByRole('checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.env.export.export' }))
|
||||
expect(onConfirm).toHaveBeenCalledExactlyOnceWith(true)
|
||||
})
|
||||
@@ -101,7 +101,7 @@ vi.mock('../update-dsl-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
vi.mock('../export-confirm-modal', () => ({
|
||||
default: ({
|
||||
envList,
|
||||
onConfirm,
|
||||
|
||||
+15
-27
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import type { EnvironmentVariableItemResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@@ -13,41 +14,29 @@ import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type DSLExportConfirmModalProps = {
|
||||
envList: Array<{ name: string; value: unknown }>
|
||||
onConfirm: (state: boolean) => void | Promise<void>
|
||||
type PipelineExportConfirmModalProps = {
|
||||
envList: Pick<EnvironmentVariableItemResponse, 'name' | 'value'>[]
|
||||
onConfirm: (includeSecret: boolean) => Promise<boolean>
|
||||
isExporting: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type DSLExportConfirmContentProps = DSLExportConfirmModalProps & {
|
||||
onExportingChange?: (isExporting: boolean) => void
|
||||
}
|
||||
|
||||
export const DSLExportConfirmContent = ({
|
||||
envList = [],
|
||||
const PipelineExportConfirmContent = ({
|
||||
envList,
|
||||
onConfirm,
|
||||
onClose,
|
||||
onExportingChange,
|
||||
}: DSLExportConfirmContentProps) => {
|
||||
isExporting,
|
||||
}: PipelineExportConfirmModalProps) => {
|
||||
const { t } = useTranslation(['common', 'workflow'])
|
||||
|
||||
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const exportButtonLabelId = React.useId()
|
||||
|
||||
const submit = useCallback(async () => {
|
||||
if (isExporting) return
|
||||
|
||||
setIsExporting(true)
|
||||
onExportingChange?.(true)
|
||||
try {
|
||||
await onConfirm(exportSecrets)
|
||||
onClose()
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
onExportingChange?.(false)
|
||||
}
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm, onExportingChange])
|
||||
if (await onConfirm(exportSecrets)) onClose()
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm])
|
||||
|
||||
return (
|
||||
<AlertDialogContent className="w-120 max-w-120">
|
||||
@@ -148,9 +137,8 @@ export const DSLExportConfirmContent = ({
|
||||
)
|
||||
}
|
||||
|
||||
const DSLExportConfirmModal = (props: DSLExportConfirmModalProps) => {
|
||||
const { envList, onClose } = props
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
const PipelineExportConfirmModal = (props: PipelineExportConfirmModalProps) => {
|
||||
const { envList, onClose, isExporting } = props
|
||||
const isDialogOpen = envList.length > 0
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
@@ -164,9 +152,9 @@ const DSLExportConfirmModal = (props: DSLExportConfirmModalProps) => {
|
||||
|
||||
return (
|
||||
<AlertDialog open={isDialogOpen} onOpenChange={handleOpenChange}>
|
||||
<DSLExportConfirmContent {...props} onExportingChange={setIsExporting} />
|
||||
<PipelineExportConfirmContent {...props} />
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSLExportConfirmModal
|
||||
export default PipelineExportConfirmModal
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import type { ExportSecretEnvironmentVariable } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { memo, useState } from 'react'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||
import { isExportSecretEnvironmentEvent } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useDSL } from '@/app/components/workflow/hooks/use-DSL'
|
||||
import { usePanelInteractions } from '@/app/components/workflow/hooks/use-panel-interactions'
|
||||
@@ -9,6 +8,7 @@ import PluginDependency from '@/app/components/workflow/plugin-dependency'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search'
|
||||
import PipelineExportConfirmModal from './export-confirm-modal'
|
||||
import RagPipelinePanel from './panel'
|
||||
import PublishToast from './publish-toast'
|
||||
import RagPipelineHeader from './rag-pipeline-header'
|
||||
@@ -16,18 +16,18 @@ import UpdateDSLModal from './update-dsl-modal'
|
||||
|
||||
const RagPipelineChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const [secretEnvList, setSecretEnvList] = useState<ExportSecretEnvironmentVariable[]>([])
|
||||
const showImportDSLModal = useStore((s) => s.showImportDSLModal)
|
||||
const setShowImportDSLModal = useStore((s) => s.setShowImportDSLModal)
|
||||
const canImportExportDSL = useHooksStore((s) => s.accessControl.canImportExportDSL)
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const { exportCheck, handleExportDSL } = useDSL()
|
||||
const { exportCheck, handleExportDSL, isExporting } = useDSL()
|
||||
|
||||
// Initialize RAG pipeline search functionality
|
||||
useRagPipelineSearch()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === DSL_EXPORT_CHECK) setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
eventEmitter?.useSubscription((event) => {
|
||||
if (isExportSecretEnvironmentEvent(event)) setSecretEnvList(event.payload.data)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -41,9 +41,10 @@ const RagPipelineChildren = () => {
|
||||
/>
|
||||
)}
|
||||
{canImportExportDSL && secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
<PipelineExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={handleExportDSL!}
|
||||
isExporting={isExporting}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -80,7 +80,9 @@ const RagPipelineMain = ({ nodes, edges, viewport }: RagPipelineMainProps) => {
|
||||
)
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl()
|
||||
const { exportCheck, handleExportDSL } = useDSLByCanEdit(datasetACLCapabilities.canEdit)
|
||||
const { exportCheck, handleExportDSL, isExporting } = useDSLByCanEdit(
|
||||
datasetACLCapabilities.canEdit,
|
||||
)
|
||||
|
||||
const configsMap = useConfigsMap()
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
|
||||
@@ -119,6 +121,7 @@ const RagPipelineMain = ({ nodes, edges, viewport }: RagPipelineMainProps) => {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
@@ -157,6 +160,7 @@ const RagPipelineMain = ({ nodes, edges, viewport }: RagPipelineMainProps) => {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { createElement } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
|
||||
import { useDSLByCanEdit } from '../use-DSL'
|
||||
|
||||
@@ -44,8 +47,16 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}))
|
||||
|
||||
const mockExportPipelineConfig = vi.fn()
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useExportPipelineDSL: () => ({ mutateAsync: mockExportPipelineConfig }),
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleClient: {
|
||||
rag: {
|
||||
pipelines: {
|
||||
byPipelineId: {
|
||||
exports: { get: (...args: unknown[]) => mockExportPipelineConfig(...args) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const mockFetchWorkflowDraft = vi.fn()
|
||||
@@ -61,6 +72,10 @@ vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
}))
|
||||
|
||||
let queryClient: QueryClient
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children)
|
||||
|
||||
describe('useDSLByCanEdit', () => {
|
||||
let mockLink: {
|
||||
href: string
|
||||
@@ -76,6 +91,7 @@ describe('useDSLByCanEdit', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
|
||||
|
||||
mockLink = {
|
||||
href: '',
|
||||
@@ -119,11 +135,35 @@ describe('useDSLByCanEdit', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does not export a stale draft after synchronization reports a failure', async () => {
|
||||
mockDoSyncWorkflowDraft.mockImplementation(async (_options, callback) => {
|
||||
callback.onError()
|
||||
return null
|
||||
})
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
await act(async () => {
|
||||
expect(await result.current.handleExportDSL()).toBe(false)
|
||||
})
|
||||
expect(mockExportPipelineConfig).not.toHaveBeenCalled()
|
||||
expect(mockDownloadBlob).not.toHaveBeenCalled()
|
||||
expect(toastMocks.call).toHaveBeenCalledTimes(1)
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.exportFailed' })
|
||||
})
|
||||
|
||||
it('allows export when draft synchronization is skipped for read-only access', async () => {
|
||||
mockDoSyncWorkflowDraft.mockResolvedValue(null)
|
||||
const { result } = renderHook(() => useDSLByCanEdit(false), { wrapper })
|
||||
await act(async () => {
|
||||
expect(await result.current.handleExportDSL()).toBe(true)
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('handleExportDSL', () => {
|
||||
it('should return early when pipelineId is not set', async () => {
|
||||
mockGetState.mockReturnValue({ pipelineId: null, knowledgeName: 'test' })
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
@@ -133,7 +173,7 @@ describe('useDSLByCanEdit', () => {
|
||||
})
|
||||
|
||||
it('should create and download file', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
@@ -143,7 +183,7 @@ describe('useDSLByCanEdit', () => {
|
||||
})
|
||||
|
||||
it('should set correct download filename', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
@@ -157,7 +197,7 @@ describe('useDSLByCanEdit', () => {
|
||||
})
|
||||
|
||||
it('should pass blob data to downloadBlob', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
@@ -173,10 +213,10 @@ describe('useDSLByCanEdit', () => {
|
||||
it('should handle export error', async () => {
|
||||
mockExportPipelineConfig.mockRejectedValue(new Error('Export failed'))
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
expect(await result.current.handleExportDSL()).toBe(false)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -188,17 +228,17 @@ describe('useDSLByCanEdit', () => {
|
||||
})
|
||||
|
||||
it('should pass include parameter', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL(true)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockExportPipelineConfig).toHaveBeenCalledWith({
|
||||
pipelineId: 'test-pipeline-id',
|
||||
include: true,
|
||||
})
|
||||
expect(mockExportPipelineConfig).toHaveBeenCalledWith(
|
||||
{ params: { pipeline_id: 'test-pipeline-id' }, query: { include_secret: 'true' } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -207,7 +247,7 @@ describe('useDSLByCanEdit', () => {
|
||||
it('should return early when pipelineId is not set', async () => {
|
||||
mockGetState.mockReturnValue({ pipelineId: null })
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
@@ -219,7 +259,7 @@ describe('useDSLByCanEdit', () => {
|
||||
it('should call handleExportDSL directly when no secret variables', async () => {
|
||||
mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] })
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
@@ -237,7 +277,7 @@ describe('useDSLByCanEdit', () => {
|
||||
const secretVars = [{ value_type: 'secret', name: 'API_KEY' }]
|
||||
mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: secretVars })
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
@@ -256,7 +296,7 @@ describe('useDSLByCanEdit', () => {
|
||||
it('should handle export check error', async () => {
|
||||
mockFetchWorkflowDraft.mockRejectedValue(new Error('Fetch failed'))
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { ExportSecretEnvironmentEvent } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { useNodesSyncDraftByCanEdit } from './use-nodes-sync-draft'
|
||||
@@ -15,30 +17,44 @@ type DoSyncWorkflowDraft = ReturnType<typeof useNodesSyncDraft>['doSyncWorkflowD
|
||||
const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
const handleExportDSL = useCallback(
|
||||
async (include = false) => {
|
||||
const { mutateAsync: exportPipeline, isPending: isExporting } = useMutation({
|
||||
mutationFn: async (include: boolean) => {
|
||||
const { pipelineId, knowledgeName } = workflowStore.getState()
|
||||
if (!pipelineId) return
|
||||
if (exporting) return
|
||||
if (!pipelineId) return false
|
||||
|
||||
let syncFailed = false
|
||||
await doSyncWorkflowDraft(undefined, {
|
||||
onError: () => {
|
||||
syncFailed = true
|
||||
},
|
||||
})
|
||||
if (syncFailed) {
|
||||
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
|
||||
return false
|
||||
}
|
||||
|
||||
const { data } = await consoleClient.rag.pipelines.byPipelineId.exports.get(
|
||||
{ params: { pipeline_id: pipelineId }, query: { include_secret: String(include) } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
downloadBlob({ data: file, fileName: `${knowledgeName}.pipeline` })
|
||||
return true
|
||||
},
|
||||
})
|
||||
const handleExportDSL = useCallback(
|
||||
async (include = false): Promise<boolean> => {
|
||||
if (isExporting) return false
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
await doSyncWorkflowDraft()
|
||||
const { data } = await exportPipelineConfig({
|
||||
pipelineId,
|
||||
include,
|
||||
})
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
downloadBlob({ data: file, fileName: `${knowledgeName}.pipeline` })
|
||||
return await exportPipeline(include)
|
||||
} catch {
|
||||
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
return false
|
||||
}
|
||||
},
|
||||
[t, doSyncWorkflowDraft, exporting, exportPipelineConfig, workflowStore],
|
||||
[t, isExporting, exportPipeline],
|
||||
)
|
||||
const exportCheck = useCallback(async () => {
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
@@ -49,7 +65,7 @@ const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
(env) => env.value_type === 'secret',
|
||||
)
|
||||
if (list.length === 0) {
|
||||
handleExportDSL()
|
||||
await handleExportDSL()
|
||||
return
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
@@ -57,7 +73,7 @@ const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
payload: {
|
||||
data: list,
|
||||
},
|
||||
} as any)
|
||||
} satisfies ExportSecretEnvironmentEvent)
|
||||
} catch {
|
||||
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
|
||||
}
|
||||
@@ -65,6 +81,7 @@ const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ vi.mock('@/app/components/workflow/update-dsl-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
vi.mock('@/app/components/app/export-confirm-modal', () => ({
|
||||
default: ({
|
||||
envList,
|
||||
onConfirm,
|
||||
|
||||
@@ -2,10 +2,11 @@ import type {
|
||||
BlockDefaultValue,
|
||||
TriggerDefaultValue,
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import type { ExportSecretEnvironmentVariable } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { DSL_EXPORT_CHECK, START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import { START_INITIAL_POSITION } from '@/app/components/workflow/constants'
|
||||
import { isExportSecretEnvironmentEvent } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useAutoGenerateWebhookUrl } from '@/app/components/workflow/hooks/use-auto-generate-webhook-url'
|
||||
import { useDSL } from '@/app/components/workflow/hooks/use-DSL'
|
||||
@@ -28,12 +29,9 @@ const Features = dynamic(() => import('@/app/components/workflow/features'), {
|
||||
const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const DSLExportConfirmModal = dynamic(
|
||||
() => import('@/app/components/workflow/dsl-export-confirm-modal'),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
)
|
||||
const AppExportConfirmModal = dynamic(() => import('@/app/components/app/export-confirm-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const WorkflowOnboardingModal = dynamic(() => import('./workflow-onboarding-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
@@ -65,7 +63,7 @@ const getTriggerPluginNodeData = (
|
||||
|
||||
const WorkflowChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const [secretEnvList, setSecretEnvList] = useState<ExportSecretEnvironmentVariable[]>([])
|
||||
const showFeaturesPanel = useStore((s) => s.showFeaturesPanel)
|
||||
const showImportDSLModal = useStore((s) => s.showImportDSLModal)
|
||||
const setShowImportDSLModal = useStore((s) => s.setShowImportDSLModal)
|
||||
@@ -80,10 +78,10 @@ const WorkflowChildren = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleOnboardingClose } = useAutoOnboarding()
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const { exportCheck, handleExportDSL } = useDSL()
|
||||
const { exportCheck, handleExportDSL, isExporting } = useDSL()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === DSL_EXPORT_CHECK) setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
eventEmitter?.useSubscription((event) => {
|
||||
if (isExportSecretEnvironmentEvent(event)) setSecretEnvList(event.payload.data)
|
||||
})
|
||||
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
@@ -180,9 +178,10 @@ const WorkflowChildren = () => {
|
||||
/>
|
||||
)}
|
||||
{canImportExportDSL && secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
<AppExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={handleExportDSL!}
|
||||
isExporting={isExporting}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -414,7 +414,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
|
||||
} = useWorkflowStartRunByCanEdit(appACLCapabilities.canEdit)
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl()
|
||||
const { exportCheck, handleExportDSL } = useDSLByCanEdit(appACLCapabilities.canEdit)
|
||||
const { exportCheck, handleExportDSL, isExporting } = useDSLByCanEdit(appACLCapabilities.canEdit)
|
||||
|
||||
const configsMap = useConfigsMap()
|
||||
|
||||
@@ -459,6 +459,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
@@ -502,6 +503,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { createElement } from 'react'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import { useDSLByCanEdit } from '../use-DSL'
|
||||
|
||||
@@ -31,7 +34,7 @@ vi.mock('@/app/notifications', () => ({
|
||||
const mockEmit = vi.fn()
|
||||
const mockDoSyncWorkflowDraft = vi.fn()
|
||||
const mockExportAppConfig = vi.fn()
|
||||
const mockFetchWorkflowDraft = vi.fn()
|
||||
const mockFetchEnvironmentVariables = vi.fn()
|
||||
const mockDownloadBlob = vi.fn()
|
||||
|
||||
let appStoreState: {
|
||||
@@ -61,14 +64,21 @@ vi.mock('../use-nodes-sync-draft', () => ({
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleClient: {
|
||||
apps: { byAppId: { export: { get: (...args: unknown[]) => mockExportAppConfig(...args) } } },
|
||||
apps: {
|
||||
byAppId: {
|
||||
export: { get: (...args: unknown[]) => mockExportAppConfig(...args) },
|
||||
workflows: {
|
||||
draft: {
|
||||
environmentVariables: {
|
||||
get: (...args: unknown[]) => mockFetchEnvironmentVariables(...args),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
|
||||
}))
|
||||
@@ -81,9 +91,14 @@ const createDeferred = <T>() => {
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
let queryClient: QueryClient
|
||||
const wrapper = ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children)
|
||||
|
||||
describe('useDSLByCanEdit', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
|
||||
appStoreState = {
|
||||
appDetail: {
|
||||
id: 'app-1',
|
||||
@@ -92,15 +107,39 @@ describe('useDSLByCanEdit', () => {
|
||||
}
|
||||
mockDoSyncWorkflowDraft.mockResolvedValue(undefined)
|
||||
mockExportAppConfig.mockResolvedValue({ data: 'yaml-content' })
|
||||
mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: [] })
|
||||
mockFetchEnvironmentVariables.mockResolvedValue({ items: [] })
|
||||
})
|
||||
|
||||
it('does not export a stale draft after synchronization reports a failure', async () => {
|
||||
mockDoSyncWorkflowDraft.mockImplementation(async (_options, callback) => {
|
||||
callback.onError()
|
||||
return null
|
||||
})
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
await act(async () => {
|
||||
expect(await result.current.handleExportDSL()).toBe(false)
|
||||
})
|
||||
expect(mockExportAppConfig).not.toHaveBeenCalled()
|
||||
expect(mockDownloadBlob).not.toHaveBeenCalled()
|
||||
expect(toastMocks.call).toHaveBeenCalledTimes(1)
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({ type: 'error', message: 'app.exportAppFailed' })
|
||||
})
|
||||
|
||||
it('allows export when draft synchronization is skipped for read-only access', async () => {
|
||||
mockDoSyncWorkflowDraft.mockResolvedValue(null)
|
||||
const { result } = renderHook(() => useDSLByCanEdit(false), { wrapper })
|
||||
await act(async () => {
|
||||
expect(await result.current.handleExportDSL()).toBe(true)
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('downloads the default package bytes and forwards the selected workflow version', async () => {
|
||||
const file = new File([new Uint8Array([0x50, 0x4b, 0xff])], 'workflow.ifpkg')
|
||||
mockExportAppConfig.mockResolvedValue(file)
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL(true, 'revision-1')
|
||||
expect(await result.current.handleExportDSL(true, 'revision-1')).toBe(true)
|
||||
})
|
||||
expect(mockExportAppConfig).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -109,17 +148,21 @@ describe('useDSLByCanEdit', () => {
|
||||
},
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith({ data: file, fileName: 'workflow.ifpkg' })
|
||||
})
|
||||
|
||||
it('should export workflow dsl and download the yaml blob when no secret env is present', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
})
|
||||
|
||||
expect(mockFetchWorkflowDraft).toHaveBeenCalledWith('/apps/app-1/workflows/draft')
|
||||
expect(mockFetchEnvironmentVariables).toHaveBeenCalledWith(
|
||||
{ params: { app_id: 'app-1' } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
|
||||
expect(mockExportAppConfig).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -152,7 +195,7 @@ workflow:
|
||||
package_ref: agent_1
|
||||
`
|
||||
mockExportAppConfig.mockResolvedValue({ data: agentDSL })
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
@@ -166,7 +209,7 @@ workflow:
|
||||
})
|
||||
|
||||
it('should forward include and workflow id arguments when exporting dsl directly', async () => {
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL(true, 'workflow-1')
|
||||
@@ -183,9 +226,9 @@ workflow:
|
||||
|
||||
it('should emit DSL_EXPORT_CHECK when secret environment variables exist', async () => {
|
||||
const secretVars = [{ id: 'env-1', value_type: 'secret', value: 'secret-token' }]
|
||||
mockFetchWorkflowDraft.mockResolvedValue({ environment_variables: secretVars })
|
||||
mockFetchEnvironmentVariables.mockResolvedValue({ items: secretVars })
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
@@ -203,40 +246,43 @@ workflow:
|
||||
it('should return early when app detail is unavailable', async () => {
|
||||
appStoreState = {}
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
expect(mockFetchWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockFetchEnvironmentVariables).not.toHaveBeenCalled()
|
||||
expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockExportAppConfig).not.toHaveBeenCalled()
|
||||
expect(mockEmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should notify when export fails', async () => {
|
||||
mockExportAppConfig.mockRejectedValue(new Error('export failed'))
|
||||
mockExportAppConfig.mockRejectedValue(
|
||||
new Response(JSON.stringify({ message: 'Package contains unusable Skill' }), { status: 500 }),
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
expect(await result.current.handleExportDSL()).toBe(false)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'app.exportFailed',
|
||||
message: 'app.exportAppFailed',
|
||||
description: 'Package contains unusable Skill',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify when exportCheck cannot load the workflow draft', async () => {
|
||||
mockFetchWorkflowDraft.mockRejectedValue(new Error('draft fetch failed'))
|
||||
mockFetchEnvironmentVariables.mockRejectedValue(new Error('draft fetch failed'))
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
@@ -245,27 +291,35 @@ workflow:
|
||||
await waitFor(() => {
|
||||
expect(toastMocks.call).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'app.exportFailed',
|
||||
message: 'app.exportAppFailed',
|
||||
description: 'draft fetch failed',
|
||||
})
|
||||
})
|
||||
expect(mockExportAppConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should ignore repeated export attempts while an export is already in progress', async () => {
|
||||
it('keeps export pending through draft synchronization and download and ignores repeated attempts', async () => {
|
||||
const sync = createDeferred<void>()
|
||||
const deferred = createDeferred<{ data: string }>()
|
||||
mockDoSyncWorkflowDraft.mockReturnValue(sync.promise)
|
||||
mockExportAppConfig.mockReturnValue(deferred.promise)
|
||||
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true))
|
||||
let firstExportPromise!: Promise<void>
|
||||
const { result } = renderHook(() => useDSLByCanEdit(true), { wrapper })
|
||||
let firstExportPromise!: Promise<boolean>
|
||||
|
||||
act(() => {
|
||||
firstExportPromise = result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
expect(mockExportAppConfig).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => expect(result.current.isExporting).toBe(true))
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
expect(mockExportAppConfig).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
sync.resolve()
|
||||
})
|
||||
expect(mockExportAppConfig).toHaveBeenCalledTimes(1)
|
||||
expect(result.current.isExporting).toBe(true)
|
||||
|
||||
act(() => {
|
||||
void result.current.handleExportDSL()
|
||||
@@ -275,7 +329,8 @@ workflow:
|
||||
|
||||
await act(async () => {
|
||||
deferred.resolve({ data: 'yaml-content' })
|
||||
await firstExportPromise
|
||||
expect(await firstExportPromise).toBe(true)
|
||||
})
|
||||
await waitFor(() => expect(result.current.isExporting).toBe(false))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { ExportSecretEnvironmentEvent } from '@/app/components/workflow/export-secret-env-event'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { getAppTransferErrorMessage } from '@/app/components/app/transfer-error'
|
||||
import { exportAppDslFile } from '@/app/components/app/use-export-app-dsl'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { useNodesSyncDraftByCanEdit } from './use-nodes-sync-draft'
|
||||
|
||||
type DoSyncWorkflowDraft = ReturnType<typeof useNodesSyncDraft>['doSyncWorkflowDraft']
|
||||
@@ -14,43 +17,63 @@ type DoSyncWorkflowDraft = ReturnType<typeof useNodesSyncDraft>['doSyncWorkflowD
|
||||
const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
const { t } = useTranslation(['app'])
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [exporting, setExporting] = useState(false)
|
||||
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
|
||||
const handleExportDSL = useCallback(
|
||||
async (include = false, workflowId?: string) => {
|
||||
if (!appDetail) return
|
||||
const { mutateAsync: exportWorkflow, isPending: isExporting } = useMutation({
|
||||
mutationFn: async ({ include, workflowId }: { include: boolean; workflowId?: string }) => {
|
||||
if (!appDetail) return false
|
||||
|
||||
if (exporting) return
|
||||
if (!workflowId) {
|
||||
let syncFailed = false
|
||||
await doSyncWorkflowDraft(undefined, {
|
||||
onError: () => {
|
||||
syncFailed = true
|
||||
},
|
||||
})
|
||||
if (syncFailed) {
|
||||
toast.error(t(($) => $.exportAppFailed, { ns: 'app' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
await exportAppDslFile({
|
||||
appId: appDetail.id,
|
||||
appName: appDetail.name,
|
||||
includeSecret: include,
|
||||
workflowId,
|
||||
})
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
const handleExportDSL = useCallback(
|
||||
async (include = false, workflowId?: string): Promise<boolean> => {
|
||||
if (isExporting) return false
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
await doSyncWorkflowDraft()
|
||||
await exportAppDslFile({
|
||||
appId: appDetail.id,
|
||||
appName: appDetail.name,
|
||||
includeSecret: include,
|
||||
workflowId,
|
||||
})
|
||||
} catch {
|
||||
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
|
||||
} finally {
|
||||
setExporting(false)
|
||||
return await exportWorkflow({ include, workflowId })
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(($) => $.exportAppFailed, { ns: 'app' }),
|
||||
{ description: await getAppTransferErrorMessage(error) },
|
||||
)
|
||||
return false
|
||||
}
|
||||
},
|
||||
[appDetail, t, doSyncWorkflowDraft, exporting],
|
||||
[exportWorkflow, isExporting, t],
|
||||
)
|
||||
|
||||
const exportCheck = useCallback(async () => {
|
||||
if (!appDetail) return
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(
|
||||
(env) => env.value_type === 'secret',
|
||||
const { items } = await consoleClient.apps.byAppId.workflows.draft.environmentVariables.get(
|
||||
{ params: { app_id: appDetail.id } },
|
||||
{ context: { silent: true } },
|
||||
)
|
||||
const list = items.filter((env) => env.value_type === 'secret')
|
||||
if (list.length === 0) {
|
||||
handleExportDSL()
|
||||
await handleExportDSL()
|
||||
return
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
@@ -58,15 +81,21 @@ const useDSLBase = (doSyncWorkflowDraft: DoSyncWorkflowDraft) => {
|
||||
payload: {
|
||||
data: list,
|
||||
},
|
||||
} as any)
|
||||
} catch {
|
||||
toast.error(t(($) => $.exportFailed, { ns: 'app' }))
|
||||
} satisfies ExportSecretEnvironmentEvent)
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(($) => $.exportAppFailed, { ns: 'app' }),
|
||||
{
|
||||
description: await getAppTransferErrorMessage(error),
|
||||
},
|
||||
)
|
||||
}
|
||||
}, [appDetail, eventEmitter, handleExportDSL, t])
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { expectLoadingButton } from '@/test/button'
|
||||
import DSLExportConfirmModal from '../dsl-export-confirm-modal'
|
||||
|
||||
const envList = [
|
||||
{
|
||||
id: 'env-1',
|
||||
name: 'SECRET_TOKEN',
|
||||
value: 'masked-value',
|
||||
value_type: 'secret' as const,
|
||||
description: 'secret token',
|
||||
},
|
||||
]
|
||||
|
||||
const multiEnvList = [
|
||||
...envList,
|
||||
{
|
||||
id: 'env-2',
|
||||
name: 'SERVICE_KEY',
|
||||
value: 'another-secret',
|
||||
value_type: 'secret' as const,
|
||||
description: 'service key',
|
||||
},
|
||||
]
|
||||
|
||||
describe('DSLExportConfirmModal', () => {
|
||||
it('should render environment rows and close when cancel is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<DSLExportConfirmModal envList={envList} onConfirm={onConfirm} onClose={onClose} />)
|
||||
|
||||
expect(screen.getByText('SECRET_TOKEN')).toBeInTheDocument()
|
||||
expect(screen.getByText('masked-value')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should confirm with exportSecrets=false by default', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<DSLExportConfirmModal envList={envList} onConfirm={onConfirm} onClose={onClose} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.env.export.ignore' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should confirm with exportSecrets=true after toggling the checkbox', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<DSLExportConfirmModal envList={envList} onConfirm={onConfirm} onClose={onClose} />)
|
||||
|
||||
await user.click(screen.getByRole('checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.env.export.export' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should also toggle exportSecrets when the label text is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onConfirm = vi.fn()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<DSLExportConfirmModal envList={envList} onConfirm={onConfirm} onClose={onClose} />)
|
||||
|
||||
await user.click(screen.getByText('workflow.env.export.checkbox'))
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.env.export.export' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show exporting state and prevent duplicate submits while exporting', async () => {
|
||||
let resolveConfirm: () => void
|
||||
const onConfirm = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveConfirm = resolve
|
||||
}),
|
||||
)
|
||||
const onClose = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<DSLExportConfirmModal envList={envList} onConfirm={onConfirm} onClose={onClose} />)
|
||||
|
||||
const confirmButton = screen.getByRole('button', { name: 'workflow.env.export.ignore' })
|
||||
|
||||
const firstClick = user.click(confirmButton)
|
||||
await waitFor(() => {
|
||||
expectLoadingButton(confirmButton)
|
||||
expect(confirmButton).toHaveTextContent('common.operation.exporting')
|
||||
expect(confirmButton).toHaveAccessibleName('common.operation.exporting')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.cancel' })).toBeDisabled()
|
||||
})
|
||||
|
||||
await user.click(confirmButton)
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
|
||||
resolveConfirm!()
|
||||
await firstClick
|
||||
|
||||
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('should render border separators for all rows except the last one', () => {
|
||||
render(<DSLExportConfirmModal envList={multiEnvList} onConfirm={vi.fn()} onClose={vi.fn()} />)
|
||||
|
||||
const firstNameCell = screen.getByText('SECRET_TOKEN').closest('td')
|
||||
const lastNameCell = screen.getByText('SERVICE_KEY').closest('td')
|
||||
const firstValueCell = screen.getByText('masked-value').closest('td')
|
||||
const lastValueCell = screen.getByText('another-secret').closest('td')
|
||||
|
||||
expect(firstNameCell).toHaveClass('border-b')
|
||||
expect(firstValueCell).toHaveClass('border-b')
|
||||
expect(lastNameCell).not.toHaveClass('border-b')
|
||||
expect(lastValueCell).not.toHaveClass('border-b')
|
||||
})
|
||||
})
|
||||
@@ -198,7 +198,7 @@ describe('PanelContextmenu', () => {
|
||||
fireEvent.click(screen.getByText('nodes.note.addNote'))
|
||||
fireEvent.click(screen.getByText('common.run'))
|
||||
fireEvent.click(screen.getByText('common.pasteHere'))
|
||||
fireEvent.click(screen.getByText('export'))
|
||||
fireEvent.click(screen.getByText('exportApp'))
|
||||
fireEvent.click(screen.getByText('importApp'))
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -224,7 +224,7 @@ describe('PanelContextmenu', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(await screen.findByText('export')).toBeInTheDocument()
|
||||
expect(await screen.findByText('exportApp')).toBeInTheDocument()
|
||||
expect(screen.queryByText('importApp')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { EventEmitterValue } from '@/context/event-emitter'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, render as rtlRender, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { EventEmitterContext } from '@/context/event-emitter'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import UpdateDSLModal from '../update-dsl-modal'
|
||||
|
||||
class MockFileReader {
|
||||
onload: ((this: FileReader, event: ProgressEvent<FileReader>) => void) | null = null
|
||||
|
||||
readAsText(_file: Blob) {
|
||||
const event = {
|
||||
target: { result: 'workflow:\n graph:\n nodes:\n - data:\n type: tool\n' },
|
||||
} as unknown as ProgressEvent<FileReader>
|
||||
this.onload?.call(this as unknown as FileReader, event)
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader)
|
||||
const mockEmit = vi.fn()
|
||||
const mockEmitWorkflowUpdate = vi.hoisted(() => vi.fn())
|
||||
|
||||
@@ -33,12 +24,24 @@ vi.mock('@/app/notifications', () => ({
|
||||
const mockImportDSL = vi.fn()
|
||||
const mockImportDSLConfirm = vi.fn()
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleClient: {
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
imports: {
|
||||
post: ({ body }: { body: unknown }) => mockImportDSL(body),
|
||||
post: {
|
||||
mutationOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
mutationFn: ({ body }: { body: unknown }) => mockImportDSL(body),
|
||||
}),
|
||||
},
|
||||
byImportId: {
|
||||
confirm: { post: ({ params }: { params: unknown }) => mockImportDSLConfirm(params) },
|
||||
confirm: {
|
||||
post: {
|
||||
mutationOptions: (options: Record<string, unknown>) => ({
|
||||
...options,
|
||||
mutationFn: ({ params }: { params: unknown }) => mockImportDSLConfirm(params),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -83,6 +86,11 @@ vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
function render(children: ReactNode) {
|
||||
const client = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
|
||||
return rtlRender(<QueryClientProvider client={client}>{children}</QueryClientProvider>)
|
||||
}
|
||||
|
||||
describe('UpdateDSLModal', () => {
|
||||
const mockToastError = vi.mocked(toast.error)
|
||||
const defaultProps = {
|
||||
@@ -94,6 +102,14 @@ describe('UpdateDSLModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useRealTimers()
|
||||
Object.defineProperty(File.prototype, 'text', {
|
||||
configurable: true,
|
||||
value: vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
'workflow:\n graph:\n nodes:\n - data:\n type: tool\n',
|
||||
),
|
||||
})
|
||||
mockFetchWorkflowDraft.mockResolvedValue({
|
||||
graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
|
||||
features: {},
|
||||
@@ -234,6 +250,7 @@ describe('UpdateDSLModal', () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'import-1',
|
||||
status: DSLImportStatus.FAILED,
|
||||
error: 'Invalid workflow package',
|
||||
app_id: 'app-1',
|
||||
})
|
||||
|
||||
@@ -246,7 +263,9 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
expect(mockToastError).toHaveBeenCalledExactlyOnceWith('workflow.common.importFailure', {
|
||||
description: 'Invalid workflow package',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -315,7 +334,7 @@ describe('UpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should open the pending modal after the timeout and allow dismissing it', async () => {
|
||||
it('should close the owner when cancelling a pending import', async () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'import-5',
|
||||
status: DSLImportStatus.PENDING,
|
||||
@@ -344,7 +363,7 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.newApp.Cancel' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'app.newApp.Confirm' })).not.toBeInTheDocument()
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -370,23 +389,114 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'app.newApp.Confirm' })).not.toBeInTheDocument()
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show an error when the selected file content is invalid for the current app mode', async () => {
|
||||
class InvalidDSLFileReader extends MockFileReader {
|
||||
override readAsText(_file: Blob) {
|
||||
const event = {
|
||||
target: {
|
||||
result: 'workflow:\n graph:\n nodes:\n - data:\n type: answer\n',
|
||||
},
|
||||
} as unknown as ProgressEvent<FileReader>
|
||||
this.onload?.call(this as unknown as FileReader, event)
|
||||
}
|
||||
}
|
||||
it('keeps a successful import successful when refreshing the draft fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
const reload = vi.spyOn(window.location, 'reload').mockImplementation(() => {})
|
||||
mockFetchWorkflowDraft.mockRejectedValue(new Error('Draft refresh unavailable'))
|
||||
renderModal()
|
||||
await user.upload(screen.getByTestId('dsl-file-input'), new File(['workflow'], 'workflow.yml'))
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' }))
|
||||
|
||||
vi.stubGlobal('FileReader', InvalidDSLFileReader as unknown as typeof FileReader)
|
||||
await waitFor(() =>
|
||||
expect(mockToastError).toHaveBeenCalledExactlyOnceWith('common.error', {
|
||||
description: 'Draft refresh unavailable',
|
||||
}),
|
||||
)
|
||||
expect(toast.success).toHaveBeenCalledWith('workflow.common.importSuccess', undefined)
|
||||
expect(defaultProps.onCancel).not.toHaveBeenCalled()
|
||||
expect(mockEmitWorkflowUpdate).toHaveBeenCalledWith('app-1')
|
||||
expect(mockHandleCheckPluginDependencies).not.toHaveBeenCalled()
|
||||
expect(reload).toHaveBeenCalledTimes(1)
|
||||
reload.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps the import pending until the committed graph has replaced the canvas', async () => {
|
||||
const user = userEvent.setup()
|
||||
let resolveDraft!: (value: unknown) => void
|
||||
mockFetchWorkflowDraft.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveDraft = resolve
|
||||
}),
|
||||
)
|
||||
renderModal()
|
||||
await user.upload(screen.getByTestId('dsl-file-input'), new File(['workflow'], 'workflow.yml'))
|
||||
const submit = screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' })
|
||||
await user.click(submit)
|
||||
await waitFor(() => expect(mockFetchWorkflowDraft).toHaveBeenCalledTimes(1))
|
||||
expect(submit).toHaveAttribute('aria-disabled', 'true')
|
||||
await user.click(submit)
|
||||
await user.keyboard('{Escape}')
|
||||
expect(mockImportDSL).toHaveBeenCalledTimes(1)
|
||||
expect(defaultProps.onCancel).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () =>
|
||||
resolveDraft({ graph: { nodes: [], edges: [], viewport: {} }, features: {} }),
|
||||
)
|
||||
await waitFor(() => expect(defaultProps.onCancel).toHaveBeenCalledTimes(1))
|
||||
expect(mockEmit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prevents closing and duplicate confirmation while a pending import is submitted, and allows retry after failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockImportDSL.mockResolvedValue({ id: 'pending-1', status: DSLImportStatus.PENDING })
|
||||
let resolveRequest!: (value: { status: DSLImportStatus; error?: string }) => void
|
||||
mockImportDSLConfirm.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveRequest = resolve
|
||||
}),
|
||||
)
|
||||
renderModal()
|
||||
await user.upload(screen.getByTestId('dsl-file-input'), new File(['workflow'], 'workflow.yml'))
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' }))
|
||||
const confirm = await screen.findByRole('button', { name: 'app.newApp.Confirm' })
|
||||
await user.click(confirm)
|
||||
await waitFor(() => expect(confirm).toHaveAttribute('aria-disabled', 'true'))
|
||||
await user.click(confirm)
|
||||
await user.keyboard('{Escape}')
|
||||
expect(mockImportDSLConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(defaultProps.onCancel).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => resolveRequest({ status: DSLImportStatus.FAILED, error: 'Retry import' }))
|
||||
await waitFor(() => expect(confirm).not.toHaveAttribute('aria-disabled', 'true'))
|
||||
await user.click(confirm)
|
||||
await waitFor(() => expect(mockImportDSLConfirm).toHaveBeenCalledTimes(2))
|
||||
expect(mockImportDSLConfirm).toHaveBeenLastCalledWith({ import_id: 'pending-1' })
|
||||
await waitFor(() => expect(defaultProps.onCancel).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('imports the submitted file snapshot even when selection changes while reading', async () => {
|
||||
const user = userEvent.setup()
|
||||
let resolveContent!: (value: string) => void
|
||||
const file = new File(['first'], 'first.yml')
|
||||
vi.spyOn(file, 'text').mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveContent = resolve
|
||||
}),
|
||||
)
|
||||
renderModal()
|
||||
const input = screen.getByTestId('dsl-file-input')
|
||||
await user.upload(input, file)
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' }))
|
||||
expect(mockImportDSL).not.toHaveBeenCalled()
|
||||
fireEvent.change(input, { target: { files: [new File(['second'], 'second.yml')] } })
|
||||
await act(async () => resolveContent('workflow: { graph: { nodes: [] } }'))
|
||||
await waitFor(() =>
|
||||
expect(mockImportDSL).toHaveBeenCalledExactlyOnceWith({
|
||||
app_id: 'app-1',
|
||||
mode: 'yaml-content',
|
||||
yaml_content: 'workflow: { graph: { nodes: [] } }',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should show an error when the selected file content is invalid for the current app mode', async () => {
|
||||
vi.mocked(File.prototype.text).mockResolvedValueOnce(
|
||||
'workflow:\n graph:\n nodes:\n - data:\n type: answer\n',
|
||||
)
|
||||
renderModal()
|
||||
|
||||
fireEvent.change(screen.getByTestId('dsl-file-input'), {
|
||||
@@ -399,12 +509,12 @@ describe('UpdateDSLModal', () => {
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockImportDSL).not.toHaveBeenCalled()
|
||||
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader)
|
||||
})
|
||||
|
||||
it('should show an error notification when import throws', async () => {
|
||||
mockImportDSL.mockRejectedValue(new Error('boom'))
|
||||
mockImportDSL.mockRejectedValue(
|
||||
Response.json({ message: 'Invalid app package' }, { status: 400 }),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
|
||||
@@ -415,7 +525,9 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
expect(mockToastError).toHaveBeenCalledExactlyOnceWith('workflow.common.importFailure', {
|
||||
description: 'Invalid app package',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -446,6 +558,7 @@ describe('UpdateDSLModal', () => {
|
||||
})
|
||||
mockImportDSLConfirm.mockResolvedValue({
|
||||
status: DSLImportStatus.FAILED,
|
||||
error: 'Import session expired',
|
||||
})
|
||||
|
||||
renderModal()
|
||||
@@ -462,7 +575,9 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.newApp.Confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
expect(mockToastError).toHaveBeenCalledExactlyOnceWith('workflow.common.importFailure', {
|
||||
description: 'Import session expired',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -473,7 +588,9 @@ describe('UpdateDSLModal', () => {
|
||||
imported_dsl_version: '1.0.0',
|
||||
current_dsl_version: '2.0.0',
|
||||
})
|
||||
mockImportDSLConfirm.mockRejectedValue(new Error('boom'))
|
||||
mockImportDSLConfirm.mockRejectedValue(
|
||||
Response.json({ message: 'Invalid app package' }, { status: 400 }),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
|
||||
@@ -489,7 +606,9 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'app.newApp.Confirm' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
expect(mockToastError).toHaveBeenCalledExactlyOnceWith('workflow.common.importFailure', {
|
||||
description: 'Invalid app package',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { EnvironmentVariableItemResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { EventEmitterValue } from '@/context/event-emitter'
|
||||
import { DSL_EXPORT_CHECK } from './constants'
|
||||
|
||||
export type ExportSecretEnvironmentVariable = Pick<
|
||||
EnvironmentVariableItemResponse,
|
||||
'name' | 'value'
|
||||
>
|
||||
|
||||
export type ExportSecretEnvironmentEvent = {
|
||||
type: typeof DSL_EXPORT_CHECK
|
||||
payload: { data: ExportSecretEnvironmentVariable[] }
|
||||
}
|
||||
|
||||
export function isExportSecretEnvironmentEvent(
|
||||
event: EventEmitterValue,
|
||||
): event is ExportSecretEnvironmentEvent {
|
||||
return typeof event !== 'string' && event.type === DSL_EXPORT_CHECK
|
||||
}
|
||||
@@ -1,74 +1,50 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { use } from 'react'
|
||||
import { HooksStoreContext, HooksStoreContextProvider } from '../provider'
|
||||
|
||||
const mockRefreshAll = vi.fn()
|
||||
const mockStore = {
|
||||
getState: () => ({
|
||||
refreshAll: mockRefreshAll,
|
||||
}),
|
||||
}
|
||||
|
||||
let mockReactflowState = {
|
||||
d3Selection: null as object | null,
|
||||
d3Zoom: null as object | null,
|
||||
}
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { HooksStoreContextProvider } from '../provider'
|
||||
import { useHooksStore } from '../store'
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStore: (selector: (state: typeof mockReactflowState) => unknown) =>
|
||||
selector(mockReactflowState),
|
||||
useStore: (selector: (state: { d3Selection: null; d3Zoom: null }) => unknown) =>
|
||||
selector({ d3Selection: null, d3Zoom: null }),
|
||||
}))
|
||||
|
||||
vi.mock('../store', async () => {
|
||||
const actual = await vi.importActual<typeof import('../store')>('../store')
|
||||
return {
|
||||
...actual,
|
||||
createHooksStore: vi.fn(() => mockStore),
|
||||
}
|
||||
})
|
||||
|
||||
const Consumer = () => {
|
||||
const store = use(HooksStoreContext)
|
||||
return <div>{store ? 'has-hooks-store' : 'missing-hooks-store'}</div>
|
||||
const ExportButton = () => {
|
||||
const isExporting = useHooksStore((state) => state.isExporting)
|
||||
const handleExportDSL = useHooksStore((state) => state.handleExportDSL)
|
||||
return (
|
||||
<button disabled={isExporting} onClick={() => handleExportDSL?.()}>
|
||||
Export
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
describe('hooks-store provider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockReactflowState = {
|
||||
d3Selection: null,
|
||||
d3Zoom: null,
|
||||
}
|
||||
})
|
||||
|
||||
it('should provide the hooks store context without refreshing when the canvas handles are missing', () => {
|
||||
render(
|
||||
<HooksStoreContextProvider>
|
||||
<Consumer />
|
||||
it('propagates export pending state and the latest handler independently of canvas initialization', async () => {
|
||||
const user = userEvent.setup()
|
||||
const firstExport = vi.fn().mockResolvedValue(true)
|
||||
const retryExport = vi.fn().mockResolvedValue(true)
|
||||
const { rerender } = render(
|
||||
<HooksStoreContextProvider handleExportDSL={firstExport} isExporting={false}>
|
||||
<ExportButton />
|
||||
</HooksStoreContextProvider>,
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'Export' }))
|
||||
expect(firstExport).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(screen.getByText('has-hooks-store')).toBeInTheDocument()
|
||||
expect(mockRefreshAll).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should refresh the hooks store when both d3Selection and d3Zoom are available', async () => {
|
||||
const handleRun = vi.fn()
|
||||
mockReactflowState = {
|
||||
d3Selection: {},
|
||||
d3Zoom: {},
|
||||
}
|
||||
|
||||
render(
|
||||
<HooksStoreContextProvider handleRun={handleRun}>
|
||||
<Consumer />
|
||||
rerender(
|
||||
<HooksStoreContextProvider handleExportDSL={firstExport} isExporting>
|
||||
<ExportButton />
|
||||
</HooksStoreContextProvider>,
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'Export' })).toBeDisabled()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefreshAll).toHaveBeenCalledWith({
|
||||
handleRun,
|
||||
})
|
||||
})
|
||||
rerender(
|
||||
<HooksStoreContextProvider handleExportDSL={retryExport} isExporting={false}>
|
||||
<ExportButton />
|
||||
</HooksStoreContextProvider>,
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'Export' }))
|
||||
expect(retryExport).toHaveBeenCalledTimes(1)
|
||||
expect(firstExport).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,15 +15,20 @@ export const HooksStoreContextProvider = ({
|
||||
const [store] = useState(() => createHooksStore(restProps))
|
||||
const d3Selection = useStore((s) => s.d3Selection)
|
||||
const d3Zoom = useStore((s) => s.d3Zoom)
|
||||
const { accessControl } = restProps
|
||||
const { accessControl, exportCheck, handleExportDSL, isExporting = false } = restProps
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Selection && d3Zoom) store.getState().refreshAll(restProps)
|
||||
}, [d3Selection, d3Zoom, store])
|
||||
|
||||
useEffect(() => {
|
||||
if (accessControl) store.getState().refreshAll({ accessControl })
|
||||
}, [accessControl, store])
|
||||
store.getState().refreshAll({
|
||||
...(accessControl && { accessControl }),
|
||||
...(exportCheck && { exportCheck }),
|
||||
...(handleExportDSL && { handleExportDSL }),
|
||||
isExporting,
|
||||
})
|
||||
}, [accessControl, exportCheck, handleExportDSL, isExporting, store])
|
||||
|
||||
return <HooksStoreContext.Provider value={store}>{children}</HooksStoreContext.Provider>
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ type CommonHooksFnMap = {
|
||||
availableNodesMetaData?: AvailableNodesMetaData
|
||||
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string; traceUrl: string }
|
||||
exportCheck?: () => Promise<void>
|
||||
handleExportDSL?: (include?: boolean, flowId?: string) => Promise<void>
|
||||
handleExportDSL?: (include?: boolean, flowId?: string) => Promise<boolean>
|
||||
isExporting: boolean
|
||||
fetchInspectVars: (params: {
|
||||
passInVars?: boolean
|
||||
vars?: VarInInspect[]
|
||||
@@ -139,7 +140,8 @@ export const createHooksStore = ({
|
||||
traceUrl: '',
|
||||
}),
|
||||
exportCheck = async () => noop(),
|
||||
handleExportDSL = async () => noop(),
|
||||
handleExportDSL = async () => false,
|
||||
isExporting = false,
|
||||
fetchInspectVars = async () => noop(),
|
||||
hasNodeInspectVars = () => false,
|
||||
hasSetInspectVar = () => false,
|
||||
@@ -179,6 +181,7 @@ export const createHooksStore = ({
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
|
||||
@@ -4,8 +4,11 @@ export const useDSL = () => {
|
||||
const exportCheck = useHooksStore((s) => s.exportCheck)
|
||||
const handleExportDSL = useHooksStore((s) => s.handleExportDSL)
|
||||
|
||||
const isExporting = useHooksStore((s) => s.isExporting)
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
isExporting,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function PanelContextmenu({ onClose }: { onClose: () => void }) {
|
||||
className="justify-between gap-4 px-3 text-text-secondary"
|
||||
onClick={() => exportCheck?.()}
|
||||
>
|
||||
{t(($) => $.export, { ns: 'app' })}
|
||||
{t(($) => $.exportApp, { ns: 'app' })}
|
||||
</ContextMenuItem>
|
||||
{!shouldHideImportApp && (
|
||||
<ContextMenuItem
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@ describe('VersionHistoryItem', () => {
|
||||
expect(screen.getByText(/Alice$/)).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.restore')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.versionHistory.editVersionInfo')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.export')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.exportApp')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.versionHistory.copyId')).toBeInTheDocument()
|
||||
expect(screen.getByText('version-1')).toBeInTheDocument()
|
||||
expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument()
|
||||
@@ -190,7 +190,7 @@ describe('VersionHistoryItem', () => {
|
||||
const triggerButton = await screen.findByRole('button', { name: 'common.operation.more' })
|
||||
await user.click(triggerButton)
|
||||
|
||||
expect(screen.queryByText('app.export')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.exportApp')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.versionHistory.copyId')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ const useActionMenu = (props: ActionMenuProps) => {
|
||||
? [
|
||||
{
|
||||
key: VersionHistoryContextMenuOptions.exportDSL,
|
||||
name: t(($) => $.export, { ns: 'app' }),
|
||||
name: t(($) => $.exportApp, { ns: 'app' }),
|
||||
disabled: deploymentEdition === 'CLOUD' && plan === undefined,
|
||||
...(shouldShowUpgrade ? { showUpgrade: true } : {}),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { usePluginDependencies } from '../hooks'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
request: vi.fn(),
|
||||
error: vi.fn(),
|
||||
pipeline: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
apps: {
|
||||
imports: {
|
||||
byAppId: {
|
||||
checkDependencies: {
|
||||
get: {
|
||||
mutationOptions: ({ context }: { context: { silent: boolean } }) => ({
|
||||
mutationFn: (input: unknown) => mocks.request(input, context),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useCheckPipelineDependencies: () => ({ mutateAsync: mocks.pipeline }),
|
||||
}))
|
||||
vi.mock('@/app/notifications', () => ({ toast: { error: mocks.error } }))
|
||||
|
||||
function renderDependencies() {
|
||||
const client = new QueryClient({ defaultOptions: { mutations: { retry: false } } })
|
||||
return renderHook(usePluginDependencies, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={client}>{children}</QueryClientProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
describe('usePluginDependencies', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('stores generated dependencies with nullable versions and clears an omitted dependency list', async () => {
|
||||
const dependency = {
|
||||
type: 'marketplace',
|
||||
value: { marketplace_plugin_unique_identifier: 'plugin-1', version: null },
|
||||
}
|
||||
mocks.request
|
||||
.mockResolvedValueOnce({ leaked_dependencies: [dependency] })
|
||||
.mockResolvedValueOnce({})
|
||||
const { result } = renderDependencies()
|
||||
await act(async () =>
|
||||
expect(await result.current.handleCheckPluginDependencies('app-1')).toBe(true),
|
||||
)
|
||||
expect(useStore.getState().dependencies).toEqual([
|
||||
{ ...dependency, value: { ...dependency.value, version: undefined } },
|
||||
])
|
||||
expect(mocks.request).toHaveBeenCalledWith({ params: { app_id: 'app-1' } }, { silent: true })
|
||||
await act(async () => result.current.handleCheckPluginDependencies('app-1'))
|
||||
expect(useStore.getState().dependencies).toEqual([])
|
||||
})
|
||||
|
||||
it.each([
|
||||
[new TypeError('Network unavailable'), 'Network unavailable'],
|
||||
[
|
||||
Response.json({ message: 'Dependency check unavailable' }, { status: 503 }),
|
||||
'Dependency check unavailable',
|
||||
],
|
||||
])(
|
||||
'reports a request failure once without rejecting the completed import',
|
||||
async (error, description) => {
|
||||
useStore.getState().setDependencies([
|
||||
{
|
||||
type: 'marketplace',
|
||||
value: { marketplace_plugin_unique_identifier: 'previous-app-plugin' },
|
||||
},
|
||||
])
|
||||
mocks.request.mockRejectedValue(error)
|
||||
const { result } = renderDependencies()
|
||||
await act(async () =>
|
||||
expect(await result.current.handleCheckPluginDependencies('app-1')).toBe(false),
|
||||
)
|
||||
expect(mocks.error).toHaveBeenCalledExactlyOnceWith('common.error', { description })
|
||||
expect(useStore.getState().dependencies).toEqual([])
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -1,20 +1,52 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getAppTransferErrorMessage } from '@/app/components/app/transfer-error'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useCheckPipelineDependencies } from '@/service/use-pipeline'
|
||||
import { useMutationCheckDependencies } from '@/service/use-plugins'
|
||||
import { useStore as usePluginDependenciesStore } from './store'
|
||||
|
||||
export const usePluginDependencies = () => {
|
||||
const { mutateAsync: checkWorkflowDependencies } = useMutationCheckDependencies()
|
||||
const { t } = useTranslation(['common'])
|
||||
const { mutateAsync: checkWorkflowDependencies } = useMutation(
|
||||
consoleQuery.apps.imports.byAppId.checkDependencies.get.mutationOptions({
|
||||
context: { silent: true },
|
||||
}),
|
||||
)
|
||||
const { mutateAsync: checkPipelineDependencies } = useCheckPipelineDependencies()
|
||||
|
||||
const handleCheckPluginDependencies = useCallback(
|
||||
async (id: string, isPipeline = false) => {
|
||||
const checkDependencies = isPipeline ? checkPipelineDependencies : checkWorkflowDependencies
|
||||
const { leaked_dependencies } = await checkDependencies(id)
|
||||
const { setDependencies } = usePluginDependenciesStore.getState()
|
||||
setDependencies(leaked_dependencies)
|
||||
if (isPipeline) {
|
||||
const { leaked_dependencies } = await checkPipelineDependencies(id)
|
||||
setDependencies(leaked_dependencies)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const { leaked_dependencies } = await checkWorkflowDependencies({ params: { app_id: id } })
|
||||
setDependencies(
|
||||
(leaked_dependencies ?? []).map((dependency) => ({
|
||||
...dependency,
|
||||
// The legacy dependency store represents an absent version with undefined.
|
||||
value: { ...dependency.value, version: dependency.value.version ?? undefined },
|
||||
})),
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
setDependencies([])
|
||||
toast.error(
|
||||
t(($) => $.error, { ns: 'common' }),
|
||||
{
|
||||
description: await getAppTransferErrorMessage(error),
|
||||
},
|
||||
)
|
||||
return false
|
||||
}
|
||||
},
|
||||
[checkWorkflowDependencies, checkPipelineDependencies],
|
||||
[checkWorkflowDependencies, checkPipelineDependencies, t],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,16 +5,18 @@ import type { MouseEventHandler } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { RiAlertFill, RiCloseLine, RiFileDownloadLine } from '@remixicon/react'
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DSLImportWarningDescription from '@/app/components/app/create-from-dsl-modal/dsl-import-warning-description'
|
||||
import { Uploader } from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { getAppTransferErrorMessage } from '@/app/components/app/transfer-error'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { DSLImportMode, DSLImportStatus } from '@/models/app'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { collaborationManager } from './collaboration/core/collaboration-manager'
|
||||
import { WORKFLOW_DATA_UPDATE } from './constants'
|
||||
@@ -35,31 +37,10 @@ type UpdateDSLModalProps = {
|
||||
const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) => {
|
||||
const { t } = useTranslation(['workflow', 'app', 'common'])
|
||||
const appDetail = useAppStore((s) => s.appDetail)
|
||||
const [currentFile, setDSLFile] = useState<File>()
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentFile, setCurrentFile] = useState<File>()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [show, setShow] = useState(true)
|
||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||
const [importId, setImportId] = useState<string>()
|
||||
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||
|
||||
const readFile = (file: File) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
const content = event.target?.result
|
||||
setFileContent(content as string)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const handleFile = (file?: File) => {
|
||||
setDSLFile(file)
|
||||
setFileContent('')
|
||||
if (file && !file.name.toLowerCase().endsWith('.ifpkg')) readFile(file)
|
||||
}
|
||||
|
||||
const handleWorkflowUpdate = useCallback(
|
||||
async (app_id: string) => {
|
||||
const { graph, features, hash, conversation_variables, environment_variables } =
|
||||
@@ -77,12 +58,11 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
conversation_variables: conversation_variables || [],
|
||||
environment_variables: environment_variables || [],
|
||||
},
|
||||
} as any)
|
||||
})
|
||||
},
|
||||
[eventEmitter],
|
||||
)
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const handleCompletedImport = useCallback(
|
||||
async (status: Import['status'], appId?: string | null, warnings: Import['warnings'] = []) => {
|
||||
if (!appId) {
|
||||
@@ -90,9 +70,6 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
return
|
||||
}
|
||||
|
||||
await handleWorkflowUpdate(appId)
|
||||
collaborationManager.emitWorkflowUpdate(appId)
|
||||
onImport?.()
|
||||
const payload = getImportNotificationPayload(status, t)
|
||||
toast[payload.type](
|
||||
payload.message,
|
||||
@@ -104,94 +81,108 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
collaborationManager.emitWorkflowUpdate(appId)
|
||||
try {
|
||||
await handleWorkflowUpdate(appId)
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
t(($) => $.error, { ns: 'common' }),
|
||||
{
|
||||
description: await getAppTransferErrorMessage(error),
|
||||
},
|
||||
)
|
||||
// Reload the committed graph before the old canvas can resume autosaving.
|
||||
window.location.reload()
|
||||
return
|
||||
}
|
||||
onImport?.()
|
||||
// Dependency checks own their feedback after the import has already succeeded.
|
||||
await handleCheckPluginDependencies(appId)
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
},
|
||||
[handleCheckPluginDependencies, handleWorkflowUpdate, onCancel, onImport, t],
|
||||
)
|
||||
|
||||
const handlePendingImport = useCallback(
|
||||
(id: string, importedVersion?: string | null, currentVersion?: string | null) => {
|
||||
setShow(false)
|
||||
setTimeout(() => {
|
||||
setShowErrorModal(true)
|
||||
}, 300)
|
||||
setVersions({
|
||||
importedVersion: importedVersion ?? '',
|
||||
systemVersion: currentVersion ?? '',
|
||||
})
|
||||
setImportId(id)
|
||||
},
|
||||
[],
|
||||
const handleImportResponse = async (response: Import) => {
|
||||
if (isImportCompleted(response.status)) {
|
||||
await handleCompletedImport(response.status, response.app_id, response.warnings)
|
||||
} else if (response.status === DSLImportStatus.FAILED) {
|
||||
toast.error(
|
||||
t(($) => $['common.importFailure'], { ns: 'workflow' }),
|
||||
{
|
||||
description: response.error || undefined,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const notifyImportError = async (error: unknown) => {
|
||||
toast.error(
|
||||
t(($) => $['common.importFailure'], { ns: 'workflow' }),
|
||||
{
|
||||
description: await getAppTransferErrorMessage(error),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const { mutateAsync: requestImport } = useMutation(
|
||||
consoleQuery.apps.imports.post.mutationOptions({ context: { silent: true } }),
|
||||
)
|
||||
const importMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
if (!appDetail) return
|
||||
if (file.name.toLowerCase().endsWith('.ifpkg'))
|
||||
return requestImport({ body: { file, app_id: appDetail.id } })
|
||||
|
||||
const handleImport: MouseEventHandler = useCallback(async () => {
|
||||
if (isCreatingRef.current) return
|
||||
isCreatingRef.current = true
|
||||
if (!currentFile) {
|
||||
isCreatingRef.current = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const isPackage = currentFile.name.toLowerCase().endsWith('.ifpkg')
|
||||
if (
|
||||
appDetail &&
|
||||
(isPackage || (fileContent && validateDSLContent(fileContent, appDetail.mode)))
|
||||
) {
|
||||
setLoading(true)
|
||||
const response = await consoleClient.apps.imports.post({
|
||||
body: isPackage
|
||||
? { file: currentFile, app_id: appDetail.id }
|
||||
: { mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, app_id: appDetail.id },
|
||||
})
|
||||
const { id, status, app_id, imported_dsl_version, current_dsl_version, warnings } = response
|
||||
|
||||
if (isImportCompleted(status)) {
|
||||
await handleCompletedImport(status, app_id, warnings)
|
||||
} else if (status === DSLImportStatus.PENDING) {
|
||||
handlePendingImport(id, imported_dsl_version, current_dsl_version)
|
||||
} else {
|
||||
setLoading(false)
|
||||
toast.error(t(($) => $['common.importFailure'], { ns: 'workflow' }))
|
||||
}
|
||||
} else if (fileContent) {
|
||||
const content = await file.text()
|
||||
if (!content || !validateDSLContent(content, appDetail.mode)) {
|
||||
toast.error(t(($) => $['common.importFailure'], { ns: 'workflow' }))
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setLoading(false)
|
||||
toast.error(t(($) => $['common.importFailure'], { ns: 'workflow' }))
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [currentFile, fileContent, t, appDetail, handleCompletedImport, handlePendingImport])
|
||||
|
||||
const onUpdateDSLConfirm: MouseEventHandler = async () => {
|
||||
try {
|
||||
if (!importId) return
|
||||
const response = await consoleClient.apps.imports.byImportId.confirm.post({
|
||||
params: { import_id: importId },
|
||||
return requestImport({
|
||||
body: {
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: content,
|
||||
app_id: appDetail.id,
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: notifyImportError,
|
||||
onSettled: async (response) => {
|
||||
if (response) await handleImportResponse(response)
|
||||
},
|
||||
})
|
||||
const confirmImportMutation = useMutation(
|
||||
consoleQuery.apps.imports.byImportId.confirm.post.mutationOptions({
|
||||
context: { silent: true },
|
||||
onError: (error) => notifyImportError(error),
|
||||
onSettled: async (response) => {
|
||||
if (response) await handleImportResponse(response)
|
||||
},
|
||||
}),
|
||||
)
|
||||
const pendingImport = importMutation.data?.status === 'pending' ? importMutation.data : undefined
|
||||
const isImporting = importMutation.isPending || confirmImportMutation.isPending
|
||||
|
||||
const { status, app_id, warnings } = response
|
||||
const handleImport: MouseEventHandler = () => {
|
||||
if (isImporting || !currentFile || !appDetail) return
|
||||
importMutation.mutate(currentFile)
|
||||
}
|
||||
|
||||
if (isImportCompleted(status)) {
|
||||
await handleCompletedImport(status, app_id, warnings)
|
||||
} else if (status === DSLImportStatus.FAILED) {
|
||||
setLoading(false)
|
||||
toast.error(t(($) => $['common.importFailure'], { ns: 'workflow' }))
|
||||
}
|
||||
} catch {
|
||||
setLoading(false)
|
||||
toast.error(t(($) => $['common.importFailure'], { ns: 'workflow' }))
|
||||
}
|
||||
const onUpdateDSLConfirm: MouseEventHandler = () => {
|
||||
if (!pendingImport || isImporting) return
|
||||
|
||||
confirmImportMutation.mutate({
|
||||
params: { import_id: pendingImport.id },
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={show}
|
||||
open={!pendingImport}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCancel()
|
||||
if (!open && !isImporting) onCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-full max-w-120! overflow-hidden! rounded-2xl border-none p-6 text-left align-middle">
|
||||
@@ -203,6 +194,7 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
type="button"
|
||||
className="flex h-5.5 w-5.5 cursor-pointer items-center justify-center border-none bg-transparent p-0 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
disabled={isImporting}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className="h-4.5 w-4.5 text-text-tertiary" aria-hidden="true" />
|
||||
@@ -218,7 +210,13 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
{t(($) => $['common.importDSLTip'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex items-start gap-1 self-stretch pt-1 pb-0.5">
|
||||
<Button size="small" variant="secondary" className="relative" onClick={onBackup}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
className="relative"
|
||||
disabled={isImporting}
|
||||
onClick={onBackup}
|
||||
>
|
||||
<RiFileDownloadLine className="size-3.5 text-components-button-secondary-text" />
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{t(($) => $['common.backupCurrentDraft'], { ns: 'workflow' })}
|
||||
@@ -229,25 +227,28 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
</div>
|
||||
<div>
|
||||
<div className="pt-2 system-md-semibold text-text-primary">
|
||||
{t(($) => $['common.chooseDSL'], { ns: 'workflow' })}
|
||||
{t(($) => $.chooseAppFile, { ns: 'app' })}
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4">
|
||||
<Uploader
|
||||
importType="app"
|
||||
disabled={isImporting}
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
updateFile={setCurrentFile}
|
||||
className="mt-0! w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
|
||||
<Button onClick={onCancel}>{t(($) => $['newApp.Cancel'], { ns: 'app' })}</Button>
|
||||
<Button disabled={isImporting} onClick={onCancel}>
|
||||
{t(($) => $['newApp.Cancel'], { ns: 'app' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!currentFile}
|
||||
disabled={isImporting || !currentFile || !appDetail}
|
||||
variant="primary"
|
||||
tone="destructive"
|
||||
onClick={handleImport}
|
||||
loading={loading}
|
||||
loading={importMutation.isPending}
|
||||
>
|
||||
{t(($) => $['common.overwriteAndImport'], { ns: 'workflow' })}
|
||||
</Button>
|
||||
@@ -255,9 +256,9 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={showErrorModal}
|
||||
open={!!pendingImport}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setShowErrorModal(false)
|
||||
if (!open && !isImporting) onCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent className="w-full max-w-120! overflow-hidden! border-none text-left align-middle">
|
||||
@@ -271,19 +272,25 @@ const UpdateDSLModal = ({ onCancel, onBackup, onImport }: UpdateDSLModalProps) =
|
||||
<br />
|
||||
<div>
|
||||
{t(($) => $['newApp.appCreateDSLErrorPart3'], { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions?.importedVersion}</span>
|
||||
<span className="system-md-medium">{pendingImport?.imported_dsl_version}</span>
|
||||
</div>
|
||||
<div>
|
||||
{t(($) => $['newApp.appCreateDSLErrorPart4'], { ns: 'app' })}
|
||||
<span className="system-md-medium">{versions?.systemVersion}</span>
|
||||
<span className="system-md-medium">{pendingImport?.current_dsl_version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
|
||||
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>
|
||||
<Button variant="secondary" disabled={isImporting} onClick={onCancel}>
|
||||
{t(($) => $['newApp.Cancel'], { ns: 'app' })}
|
||||
</Button>
|
||||
<Button variant="primary" tone="destructive" onClick={onUpdateDSLConfirm}>
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="destructive"
|
||||
disabled={isImporting}
|
||||
loading={confirmImportMutation.isPending}
|
||||
onClick={onUpdateDSLConfirm}
|
||||
>
|
||||
{t(($) => $['newApp.Confirm'], { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ function AgentSkillPackageUploader({
|
||||
{file && (
|
||||
<div className="group flex items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover">
|
||||
<div className="flex items-center justify-center p-3">
|
||||
<span aria-hidden className="i-custom-public-files-yaml size-6 shrink-0" />
|
||||
<span aria-hidden className="i-ri-file-zip-line size-6 shrink-0 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="flex min-w-0 grow flex-col items-start gap-0.5 py-1 pr-2">
|
||||
<span className="max-w-full min-w-0 truncate text-[12px] leading-4 font-medium text-text-secondary">
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "لا توجد معلمات مطلوبة",
|
||||
"appSelector.params": "معلمات التطبيق",
|
||||
"appSelector.placeholder": "اختر تطبيقًا...",
|
||||
"chooseAppFile": "اختر حزمة تطبيق أو ملف DSL",
|
||||
"deleteAppConfirmContent": "حذف التطبيق لا رجعة فيه. لن يتمكن المستخدمون من الوصول إلى تطبيقك بعد الآن، وسيتم حذف جميع تكوينات المطالبة والسجلات بشكل دائم.",
|
||||
"deleteAppConfirmInputLabel": "للتأكيد، اكتب <appName>{{appName}}</appName> في المربع أدناه:",
|
||||
"deleteAppConfirmInputPlaceholder": "أدخل اسم التطبيق…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "تعديل معلومات التطبيق",
|
||||
"editDone": "تم تحديث معلومات التطبيق",
|
||||
"editFailed": "فشل تحديث معلومات التطبيق",
|
||||
"export": "تصدير DSL",
|
||||
"exportApp": "تصدير التطبيق",
|
||||
"exportAppFailed": "تعذّر تصدير التطبيق",
|
||||
"exportAppSuccess": "تم تنزيل حزمة التطبيق (.ifpkg)",
|
||||
"exportFailed": "فشل تصدير DSL.",
|
||||
"exportSecret.description": "عند التحديد، ستُضمّن القيم السرية في حزمة التطبيق المُصدَّرة.",
|
||||
"exportSecret.title": "تصدير التطبيق مع القيم السرية",
|
||||
"filterEmpty.noApps": "لا توجد تطبيقات هنا",
|
||||
"firstEmpty.blankDescription": "ابدأ بلوحة فارغة عندما تعرف ما تريد بناءه.",
|
||||
"firstEmpty.importDescription": "استعد تطبيقًا من ملف تعريف Dify DSL.",
|
||||
"firstEmpty.importDescription": "استورد حزمة تطبيق أو ملف DSL لاستعادة تطبيق تمت مشاركته أو نسخه احتياطيًا.",
|
||||
"firstEmpty.learnDifyTitle": "تعلّم Dify",
|
||||
"firstEmpty.or": "أو",
|
||||
"firstEmpty.templateDescription": "ابدأ من سير عمل مُعد مسبقًا. مناسب لتعلم Dify والأنماط الشائعة.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "حزمة تطبيق أو ملف DSL",
|
||||
"importAppFormats": "يدعم .ifpkg و.yaml و.yml",
|
||||
"importAppUrlPlaceholder": "الصق هنا رابط حزمة تطبيق (.ifpkg) أو ملف DSL",
|
||||
"importDSL": "استيراد ملف DSL",
|
||||
"importFromDSL": "استيراد من DSL",
|
||||
"importFromDSLFile": "من ملف DSL",
|
||||
"importFromDSLUrl": "من رابط",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "مولد نصوص",
|
||||
"newApp.completionShortDescription": "مساعد AI لمهام توليد النصوص",
|
||||
"newApp.completionUserDescription": "قم ببناء مساعد AI لمهام توليد النصوص بسرعة مع تكوين بسيط.",
|
||||
"newApp.dropDSLToCreateApp": "أفلت ملف DSL هنا لإنشاء تطبيق",
|
||||
"newApp.dropDSLToCreateApp": "أفلت حزمة تطبيق أو ملف DSL هنا لإنشاء تطبيق",
|
||||
"newApp.forBeginners": "أنواع تطبيقات أبسط",
|
||||
"newApp.foundResult": "{{count}} نتيجة",
|
||||
"newApp.foundResults": "{{count}} نتائج",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "فهمت",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "ابدأ بلوحة فارغة عندما تعرف مسبقًا ما تريد بناءه.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "الإنشاء من الصفر",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "عندك ملف Dify DSL؟ استورده هنا لاستعادة تطبيق شاركته أو أخذت منه نسخة احتياطية.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "استيراد ملف DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "استورد حزمة تطبيق أو ملف DSL لاستعادة تطبيق تمت مشاركته أو نسخه احتياطيًا.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "استيراد التطبيق",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "جديد على Dify؟ ابدأ بدرس موجّه أولًا — سترى Workflows وAgents وغيرها أثناء العمل.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "أو ابدأ بـ Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "تصفّح قوالب Dify واختر ما يناسب ما تريد بناءه. طريقة ممتازة للبدء دون كتابة كل شيء من الصفر.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "لا توجد تطبيقات لعرضها بعد",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "يمكنك تصفح التطبيقات وفتحها في مساحة العمل هذه، لكن إنشاء التطبيقات أو تعديلها يتطلب صلاحية. بدّل إلى مساحة عمل لديك فيها وصول، أو تواصل مع مالك مساحة العمل أو المسؤول.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio متاح للعرض فقط لك",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "استخدم Create لإضافة تطبيق جديد — اختر قالبًا، أو ابدأ من الصفر، أو استورد ملف DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "استخدم «إنشاء» لإضافة تطبيق من قالب أو البدء من الصفر أو استيراد حزمة تطبيق أو ملف DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "إنشاء تطبيق جديد",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "اضغط على أي تطبيق لفتح صفحة التنسيق الخاصة به — عدّل من هناك المطالبات والنماذج والمنطق أو أدر إعداداته.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "فتح وإدارة كل تطبيق",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "عرض نقاط الوصول",
|
||||
"agent.acl.delete": "حذف الوكيل",
|
||||
"agent.acl.edit": "تعديل معلومات الوكيل وإعداداته",
|
||||
"agent.acl.import_export_dsl": "استيراد DSL الوكيل وتصديره",
|
||||
"agent.acl.import_export_dsl": "استيراد الوكلاء وتصديرهم",
|
||||
"agent.acl.log_manage": "عرض السجلات وإدارتها",
|
||||
"agent.acl.monitor": "عرض مراقبة الوكيل",
|
||||
"agent.acl.preview": "عرض المعلومات الأساسية للوكيل",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "حذف التطبيق",
|
||||
"app.acl.deploy": "نشر التطبيق",
|
||||
"app.acl.edit": "تعديل معلومات التطبيق وتنسيقه",
|
||||
"app.acl.import_export_dsl": "استيراد DSL التطبيق وتصديره",
|
||||
"app.acl.import_export_dsl": "استيراد التطبيقات وتصديرها",
|
||||
"app.acl.log_and_annotation": "عرض السجلات والتعليقات التوضيحية وإدارتها",
|
||||
"app.acl.monitor": "عرض مراقبة التطبيق",
|
||||
"app.acl.preview": "معاينة التطبيق",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Parametr tələb olunmur",
|
||||
"appSelector.params": "Tətbiqin parametrləri",
|
||||
"appSelector.placeholder": "Tətbiq seçin...",
|
||||
"chooseAppFile": "Tətbiq paketi və ya DSL faylı seçin",
|
||||
"deleteAppConfirmContent": "Tətbiqin silinməsi geri qaytarıla bilməz. İstifadəçilər artıq tətbiqinizə daxil ola bilməyəcək, bütün təlimat konfiqurasiyaları və jurnallar həmişəlik silinəcək.",
|
||||
"deleteAppConfirmInputLabel": "Təsdiqləmək üçün aşağıdakı xanaya <appName>{{appName}}</appName> yazın:",
|
||||
"deleteAppConfirmInputPlaceholder": "Tətbiqin adını daxil edin…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Tətbiq məlumatlarının redaktəsi",
|
||||
"editDone": "Tətbiq məlumatı yeniləndi",
|
||||
"editFailed": "Tətbiq məlumatını yeniləmək mümkün olmadı",
|
||||
"export": "DSL-i ixrac et",
|
||||
"exportApp": "Tətbiqi ixrac et",
|
||||
"exportAppFailed": "Tətbiqi ixrac etmək mümkün olmadı",
|
||||
"exportAppSuccess": "Tətbiq paketi (.ifpkg) endirildi",
|
||||
"exportFailed": "DSL-i ixrac etmək mümkün olmadı.",
|
||||
"exportSecret.description": "Seçildikdə məxfi dəyərlər ixrac edilən tətbiq paketinə daxil ediləcək.",
|
||||
"exportSecret.title": "Tətbiqi məxfi dəyərlərlə ixrac et",
|
||||
"filterEmpty.noApps": "Burada tətbiq yoxdur",
|
||||
"firstEmpty.blankDescription": "Boş iş sahəsindən başlayın və tətbiqinizi addım-addım qurun.",
|
||||
"firstEmpty.importDescription": "Artıq DSL kimi ixrac edilmiş Dify tətbiqiniz var? Qaldığınız yerdən davam etmək üçün onu idxal edin.",
|
||||
"firstEmpty.importDescription": "Paylaşılmış və ya ehtiyat nüsxəsi yaradılmış tətbiqi bərpa etmək üçün tətbiq paketi və ya DSL faylı idxal edin.",
|
||||
"firstEmpty.learnDifyTitle": "Şablon vasitəsilə Dify ilə tanışlıq",
|
||||
"firstEmpty.or": "və ya",
|
||||
"firstEmpty.templateDescription": "Hazır tətbiq seçin və onu fərdiləşdirin. Bu, Dify tətbiqinin necə işlədiyini görməyin ən sürətli yoludur.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Tətbiq paketi və ya DSL faylı",
|
||||
"importAppFormats": ".ifpkg, .yaml və .yml dəstəklənir",
|
||||
"importAppUrlPlaceholder": "Tətbiq paketi (.ifpkg) və ya DSL faylının keçidini buraya yapışdırın",
|
||||
"importDSL": "DSL faylı idxal et",
|
||||
"importFromDSL": "DSL-dən idxal et",
|
||||
"importFromDSLFile": "DSL faylından",
|
||||
"importFromDSLUrl": "URL-dən",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Mətn generatoru",
|
||||
"newApp.completionShortDescription": "Mətn yaratma tapşırıqları üçün süni intellekt köməkçisi",
|
||||
"newApp.completionUserDescription": "Sadə konfiqurasiya ilə mətn yaratma tapşırıqları üçün süni intellekt köməkçisini tez yaradın.",
|
||||
"newApp.dropDSLToCreateApp": "Tətbiq yaratmaq üçün DSL faylını buraya buraxın",
|
||||
"newApp.dropDSLToCreateApp": "Tətbiq yaratmaq üçün tətbiq paketini və ya DSL faylını buraya buraxın",
|
||||
"newApp.forBeginners": "Daha sadə tətbiq növləri",
|
||||
"newApp.foundResult": "{{count}} nəticə",
|
||||
"newApp.foundResults": "{{count}} nəticə",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Aydındır",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Nə quracağınızı artıq bilirsinizsə, boş iş lövhəsindən başlayın.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Sıfırdan yaratma",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Dify DSL faylınız var? Paylaşdığınız və ya ehtiyat nüsxəsini saxladığınız tətbiqi bərpa etmək üçün onu buraya idxal edin.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "DSL faylının idxalı",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Paylaşılmış və ya ehtiyat nüsxəsi yaradılmış tətbiqi bərpa etmək üçün tətbiq paketi və ya DSL faylı idxal edin.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Tətbiq idxal et",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Dify ilə yeni tanış olursunuz? Əvvəlcə bələdçili dərs keçin, iş axınlarının, agentlərin və digər imkanların necə işlədiyini görəcəksiniz.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Və ya «Dify-ı öyrən» bölməsi ilə başlanğıc",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Dify şablonlarına baxın və qurmaq istədiyinizə uyğun olanı seçin. Hər şeyi sıfırdan yazmadan başlamaq üçün əla yoldur.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Baxmaq üçün hələ tətbiq yoxdur",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Bu iş sahəsindəki tətbiqlərə baxa bilərsiniz. Tətbiq yaratmaq və ya redaktə etmək üçün icazəniz olan iş sahəsinə keçin və ya iş sahəsinin sahibi və ya inzibatçısı ilə əlaqə saxlayın.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studiyada yalnız baxış icazəniz var",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Yeni tətbiq əlavə etmək üçün «Yarat» düyməsindən istifadə edin: şablon seçin, sıfırdan başlayın və ya DSL faylını idxal edin.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Şablondan tətbiq əlavə etmək, sıfırdan başlamaq və ya tətbiq paketi yaxud DSL faylı idxal etmək üçün “Yarat” seçimini istifadə edin.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Yeni tətbiqin yaradılması",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "İdarəetmə səhifəsini açmaq üçün istənilən tətbiqə toxunun, təlimatları, modelləri və məntiqi oradan redaktə edin və ya parametrlərini idarə edin.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Tətbiqlərin açılması və idarə edilməsi",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Giriş nöqtələrinə bax",
|
||||
"agent.acl.delete": "Agenti sil",
|
||||
"agent.acl.edit": "Agentin məlumatlarını və konfiqurasiyasını redaktə et",
|
||||
"agent.acl.import_export_dsl": "Agentin DSL faylını idxal və ixrac et",
|
||||
"agent.acl.import_export_dsl": "Agentləri idxal və ixrac et",
|
||||
"agent.acl.log_manage": "Jurnallara bax və onları idarə et",
|
||||
"agent.acl.monitor": "Agentin monitorinqinə bax",
|
||||
"agent.acl.preview": "Agentin əsas məlumatlarına bax",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Tətbiqi sil",
|
||||
"app.acl.deploy": "Tətbiqi yerləşdir",
|
||||
"app.acl.edit": "Tətbiqin məlumatlarını redaktə et və iş prosesini qur",
|
||||
"app.acl.import_export_dsl": "Tətbiqin DSL faylını idxal və ixrac et",
|
||||
"app.acl.import_export_dsl": "Tətbiqləri idxal və ixrac et",
|
||||
"app.acl.log_and_annotation": "Jurnallara və annotasiyalara bax və onları idarə et",
|
||||
"app.acl.monitor": "Tətbiqin monitorinqinə bax",
|
||||
"app.acl.preview": "Tətbiqin önbaxışına bax",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Keine Parameter erforderlich",
|
||||
"appSelector.params": "APP-PARAMETER",
|
||||
"appSelector.placeholder": "Wählen Sie eine App aus...",
|
||||
"chooseAppFile": "App-Paket oder DSL-Datei auswählen",
|
||||
"deleteAppConfirmContent": "Das Löschen der App ist unwiderruflich. Nutzer werden keinen Zugang mehr zu Ihrer App haben, und alle Prompt-Konfigurationen und Logs werden dauerhaft gelöscht.",
|
||||
"deleteAppConfirmInputLabel": "Geben Sie zur Bestätigung <appName>{{appName}}</appName> in das Feld unten ein:",
|
||||
"deleteAppConfirmInputPlaceholder": "App-Namen eingeben…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "App-Informationen bearbeiten",
|
||||
"editDone": "App-Informationen wurden aktualisiert",
|
||||
"editFailed": "Aktualisierung der App-Informationen fehlgeschlagen",
|
||||
"export": "DSL exportieren",
|
||||
"exportApp": "App exportieren",
|
||||
"exportAppFailed": "App konnte nicht exportiert werden",
|
||||
"exportAppSuccess": "App-Paket (.ifpkg) heruntergeladen",
|
||||
"exportFailed": "Fehler beim Exportieren von DSL.",
|
||||
"exportSecret.description": "Wenn ausgewählt, werden geheime Werte in das exportierte App-Paket aufgenommen.",
|
||||
"exportSecret.title": "App mit geheimen Werten exportieren",
|
||||
"filterEmpty.noApps": "Hier gibt es keine Apps",
|
||||
"firstEmpty.blankDescription": "Beginne mit einer leeren Arbeitsfläche, wenn du weißt, was du bauen möchtest.",
|
||||
"firstEmpty.importDescription": "Stelle eine App aus einer Dify-DSL-Definitionsdatei wieder her.",
|
||||
"firstEmpty.importDescription": "Importiere ein App-Paket oder eine DSL-Datei, um eine geteilte oder gesicherte App wiederherzustellen.",
|
||||
"firstEmpty.learnDifyTitle": "Dify lernen",
|
||||
"firstEmpty.or": "Oder",
|
||||
"firstEmpty.templateDescription": "Starte mit einem vorgefertigten Workflow. Ideal, um Dify und gängige Muster zu lernen.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "App-Paket oder DSL-Datei",
|
||||
"importAppFormats": "Unterstützt .ifpkg, .yaml und .yml",
|
||||
"importAppUrlPlaceholder": "Link zu einem App-Paket (.ifpkg) oder einer DSL-Datei hier einfügen",
|
||||
"importDSL": "DSL-Datei importieren",
|
||||
"importFromDSL": "Import von DSL",
|
||||
"importFromDSLFile": "Aus DSL-Datei",
|
||||
"importFromDSLUrl": "Von URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Textgenerator",
|
||||
"newApp.completionShortDescription": "KI-Assistent für Textgenerierungsaufgaben",
|
||||
"newApp.completionUserDescription": "Erstellen Sie schnell einen KI-Assistenten für Textgenerierungsaufgaben mit einfacher Konfiguration.",
|
||||
"newApp.dropDSLToCreateApp": "Ziehen Sie die DSL-Datei hierher, um die App zu erstellen",
|
||||
"newApp.dropDSLToCreateApp": "App-Paket oder DSL-Datei hier ablegen, um eine App zu erstellen",
|
||||
"newApp.forBeginners": "Einfachere App-Typen",
|
||||
"newApp.foundResult": "{{count}} Ergebnis",
|
||||
"newApp.foundResults": "{{count}} Befund",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Verstanden",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Beginne mit einem leeren Canvas, wenn du bereits weißt, was du erstellen möchtest.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Von Grund auf erstellen",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Hast du eine Dify-DSL-Datei? Importiere sie hier, um eine App wiederherzustellen, die du geteilt oder gesichert hast.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Eine DSL-Datei importieren",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importiere ein App-Paket oder eine DSL-Datei, um eine geteilte oder gesicherte App wiederherzustellen.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "App importieren",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Neu bei Dify? Arbeite dich zuerst durch eine geführte Lektion — du siehst Workflows, Agents und mehr in Aktion.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Oder starte mit Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Durchsuche Difys Vorlagen und wähle eine, die zu dem passt, was du erstellen möchtest. Eine gute Möglichkeit, ohne etwas von Grund auf zu schreiben, zu starten.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Noch keine Apps zum Anzeigen",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Du kannst Apps in diesem Arbeitsbereich durchsuchen und oeffnen, aber zum Erstellen oder Bearbeiten brauchst du eine Berechtigung. Wechsle zu einem Arbeitsbereich mit Zugriff oder kontaktiere den Workspace Owner oder Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio ist fuer dich schreibgeschuetzt",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Nutze „Erstellen“, um eine neue App hinzuzufügen — wähle eine Vorlage, beginne von Grund auf oder importiere eine DSL-Datei.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Füge über „Erstellen“ eine App hinzu: aus einer Vorlage, als leere App oder durch Import eines App-Pakets oder einer DSL-Datei.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Eine neue App erstellen",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Tippe auf eine beliebige App, um ihre Orchestrierungsseite zu öffnen — bearbeite dort Prompts, Modelle und Logik oder verwalte ihre Einstellungen.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Jede App öffnen und verwalten",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Zugriffspunkte anzeigen",
|
||||
"agent.acl.delete": "Agent löschen",
|
||||
"agent.acl.edit": "Agent-Informationen und -Konfiguration bearbeiten",
|
||||
"agent.acl.import_export_dsl": "Agent-DSL importieren und exportieren",
|
||||
"agent.acl.import_export_dsl": "Agenten importieren und exportieren",
|
||||
"agent.acl.log_manage": "Logs anzeigen und verwalten",
|
||||
"agent.acl.monitor": "Agent-Monitoring anzeigen",
|
||||
"agent.acl.preview": "Grundlegende Agent-Informationen anzeigen",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "App löschen",
|
||||
"app.acl.deploy": "App bereitstellen",
|
||||
"app.acl.edit": "App-Informationen bearbeiten und App orchestrieren",
|
||||
"app.acl.import_export_dsl": "App-DSL importieren und exportieren",
|
||||
"app.acl.import_export_dsl": "Apps importieren und exportieren",
|
||||
"app.acl.log_and_annotation": "Logs und Annotationen anzeigen und verwalten",
|
||||
"app.acl.monitor": "App-Monitoring anzeigen",
|
||||
"app.acl.preview": "App-Vorschau",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "No parameters needed",
|
||||
"appSelector.params": "APP PARAMETERS",
|
||||
"appSelector.placeholder": "Select an app...",
|
||||
"chooseAppFile": "Choose an app package or DSL file",
|
||||
"deleteAppConfirmContent": "Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.",
|
||||
"deleteAppConfirmInputLabel": "To confirm, type <appName>{{appName}}</appName> in the box below:",
|
||||
"deleteAppConfirmInputPlaceholder": "Enter app name…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Edit App Info",
|
||||
"editDone": "App info updated",
|
||||
"editFailed": "Failed to update app info",
|
||||
"export": "Export DSL",
|
||||
"exportApp": "Export App",
|
||||
"exportAppFailed": "Failed to export app",
|
||||
"exportAppSuccess": "App package (.ifpkg) downloaded",
|
||||
"exportFailed": "Export DSL failed.",
|
||||
"exportSecret.description": "If selected, secret values will be included in the exported app package.",
|
||||
"exportSecret.title": "Export app with secret values",
|
||||
"filterEmpty.noApps": "No apps here",
|
||||
"firstEmpty.blankDescription": "Start with an empty canvas and build your app step by step.",
|
||||
"firstEmpty.importDescription": "Already have a Dify app exported as DSL? Bring it in to continue where you left off.",
|
||||
"firstEmpty.importDescription": "Import an app package or DSL file to restore a shared or backed-up app.",
|
||||
"firstEmpty.learnDifyTitle": "Learn Dify from Template",
|
||||
"firstEmpty.or": "Or",
|
||||
"firstEmpty.templateDescription": "Pick a ready-made app and customize it. The fastest way to see Dify in action.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "App package or DSL file",
|
||||
"importAppFormats": "Supports .ifpkg, .yaml, and .yml",
|
||||
"importAppUrlPlaceholder": "Paste an app package (.ifpkg) or DSL link here",
|
||||
"importDSL": "Import DSL file",
|
||||
"importFromDSL": "Import from DSL",
|
||||
"importFromDSLFile": "From DSL file",
|
||||
"importFromDSLUrl": "From URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Text Generator",
|
||||
"newApp.completionShortDescription": "AI assistant for text generation tasks",
|
||||
"newApp.completionUserDescription": "Quickly build an AI assistant for text generation tasks with simple configuration.",
|
||||
"newApp.dropDSLToCreateApp": "Drop DSL file here to create app",
|
||||
"newApp.dropDSLToCreateApp": "Drop an app package or DSL file here to create an app",
|
||||
"newApp.forBeginners": "More basic app types",
|
||||
"newApp.foundResult": "{{count}} Result",
|
||||
"newApp.foundResults": "{{count}} Results",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Got it",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Start from an empty canvas when you already know what to build.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Create from blank",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Got a Dify DSL file? Import it here to restore an app you've shared or backed up.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Import a DSL file",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Import an app package or DSL file to restore a shared or backed-up app.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Import App",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "New to Dify? Walk through a guided lesson first — you'll see Workflows, Agents, and more in action.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Or start with Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Browse Dify's templates and pick one that matches what you want to build. Great way to start without writing anything from scratch.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "No apps to view yet",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "You can browse apps in this workspace. To create or edit apps, switch to a workspace where you have permission, or contact your Workspace Owner or Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio is view-only for you",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Use Create to add a new app — pick from a template, start from blank, or import a DSL file.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Use Create to add a new app — pick from a template, start from blank, or import an app package or DSL file.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Create a new app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Tap any app to open its orchestration page — edit prompts, models, and logic, or manage its settings from there.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Open and manage each app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "View access points",
|
||||
"agent.acl.delete": "Delete agent",
|
||||
"agent.acl.edit": "Edit agent information and configuration",
|
||||
"agent.acl.import_export_dsl": "Import and export agent DSL",
|
||||
"agent.acl.import_export_dsl": "Import and export agents",
|
||||
"agent.acl.log_manage": "View and manage logs",
|
||||
"agent.acl.monitor": "View agent monitoring",
|
||||
"agent.acl.preview": "View agent basic information",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Delete app",
|
||||
"app.acl.deploy": "Deploy app",
|
||||
"app.acl.edit": "Edit app information and orchestrate app",
|
||||
"app.acl.import_export_dsl": "Import and export app DSL",
|
||||
"app.acl.import_export_dsl": "Import and export apps",
|
||||
"app.acl.log_and_annotation": "View and manage logs and annotations",
|
||||
"app.acl.monitor": "View app monitoring",
|
||||
"app.acl.preview": "Preview app",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "No se necesitan parámetros",
|
||||
"appSelector.params": "PARÁMETROS DE LA APLICACIÓN",
|
||||
"appSelector.placeholder": "Selecciona una aplicación...",
|
||||
"chooseAppFile": "Seleccionar un paquete de aplicación o archivo DSL",
|
||||
"deleteAppConfirmContent": "Eliminar la app es irreversible. Los usuarios ya no podrán acceder a tu app y todas las configuraciones y registros de prompts se eliminarán permanentemente.",
|
||||
"deleteAppConfirmInputLabel": "Para confirmar, escriba <appName>{{appName}}</appName> en el cuadro a continuación:",
|
||||
"deleteAppConfirmInputPlaceholder": "Ingrese el nombre de la app…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Editar información de la app",
|
||||
"editDone": "Información de la app actualizada",
|
||||
"editFailed": "Error al actualizar información de la app",
|
||||
"export": "Exportar DSL",
|
||||
"exportApp": "Exportar aplicación",
|
||||
"exportAppFailed": "No se pudo exportar la aplicación",
|
||||
"exportAppSuccess": "Paquete de aplicación (.ifpkg) descargado",
|
||||
"exportFailed": "Error al exportar DSL.",
|
||||
"exportSecret.description": "Si se selecciona, los valores secretos se incluirán en el paquete de aplicación exportado.",
|
||||
"exportSecret.title": "Exportar aplicación con valores secretos",
|
||||
"filterEmpty.noApps": "No hay aplicaciones aquí",
|
||||
"firstEmpty.blankDescription": "Empieza con un lienzo vacío cuando sepas qué quieres crear.",
|
||||
"firstEmpty.importDescription": "Restaura una aplicación desde un archivo de definición DSL de Dify.",
|
||||
"firstEmpty.importDescription": "Importa un paquete de aplicación o archivo DSL para restaurar una aplicación compartida o respaldada.",
|
||||
"firstEmpty.learnDifyTitle": "Aprende Dify",
|
||||
"firstEmpty.or": "O",
|
||||
"firstEmpty.templateDescription": "Empieza con un flujo de trabajo prediseñado. Ideal para aprender Dify y patrones comunes.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Paquete de aplicación o archivo DSL",
|
||||
"importAppFormats": "Admite .ifpkg, .yaml y .yml",
|
||||
"importAppUrlPlaceholder": "Pega aquí un enlace a un paquete de aplicación (.ifpkg) o archivo DSL",
|
||||
"importDSL": "Importar archivo DSL",
|
||||
"importFromDSL": "Importar desde DSL",
|
||||
"importFromDSLFile": "Desde el archivo DSL",
|
||||
"importFromDSLUrl": "URL de origen",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Generador de Texto",
|
||||
"newApp.completionShortDescription": "Asistente de IA para tareas de generación de texto",
|
||||
"newApp.completionUserDescription": "Cree rápidamente un asistente de IA para tareas de generación de texto con una configuración sencilla.",
|
||||
"newApp.dropDSLToCreateApp": "Suelta el archivo DSL aquí para crear la aplicación",
|
||||
"newApp.dropDSLToCreateApp": "Suelta aquí un paquete de aplicación o archivo DSL para crear una aplicación",
|
||||
"newApp.forBeginners": "Tipos de aplicación más básicos",
|
||||
"newApp.foundResult": "{{count}} Resultado",
|
||||
"newApp.foundResults": "{{count}} Resultados",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Entendido",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Empieza desde un lienzo en blanco cuando ya sabes qué quieres crear.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Crear desde cero",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "¿Tienes un archivo DSL de Dify? Impórtalo aquí para restaurar una app que hayas compartido o respaldado.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importar un archivo DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importa un paquete de aplicación o archivo DSL para restaurar una aplicación compartida o respaldada.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importar App",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "¿Eres nuevo en Dify? Primero sigue una lección guiada — verás Workflows, Agents y más en acción.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "O empieza con Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Explora las plantillas de Dify y elige una que se ajuste a lo que quieres crear. Una buena forma de empezar sin escribir nada desde cero.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Aun no hay apps para ver",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Puedes explorar y abrir apps en este espacio de trabajo, pero crear o editar apps requiere permiso. Cambia a un espacio donde tengas acceso o contacta al propietario o administrador.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio es de solo lectura para ti",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Usa Crear para añadir una nueva app — elige una plantilla, empieza desde cero o importa un archivo DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Usa «Crear» para añadir una aplicación desde una plantilla, empezar desde cero o importar un paquete de aplicación o archivo DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Crear una nueva app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Toca cualquier app para abrir su página de orquestación — edita prompts, modelos y lógica, o gestiona sus ajustes desde ahí.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Abrir y gestionar cada app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Ver puntos de acceso",
|
||||
"agent.acl.delete": "Eliminar agente",
|
||||
"agent.acl.edit": "Editar la información y la configuración del agente",
|
||||
"agent.acl.import_export_dsl": "Importar y exportar el DSL del agente",
|
||||
"agent.acl.import_export_dsl": "Importar y exportar agentes",
|
||||
"agent.acl.log_manage": "Ver y gestionar logs",
|
||||
"agent.acl.monitor": "Ver la supervisión del agente",
|
||||
"agent.acl.preview": "Ver la información básica del agente",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Eliminar app",
|
||||
"app.acl.deploy": "Desplegar la app",
|
||||
"app.acl.edit": "Editar la información y orquestar la app",
|
||||
"app.acl.import_export_dsl": "Importar y exportar el DSL de la app",
|
||||
"app.acl.import_export_dsl": "Importar y exportar aplicaciones",
|
||||
"app.acl.log_and_annotation": "Ver y gestionar logs y anotaciones",
|
||||
"app.acl.monitor": "Ver la supervisión de la app",
|
||||
"app.acl.preview": "Previsualizar app",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "بدون پارامتر مورد نیاز است",
|
||||
"appSelector.params": "پارامترهای برنامه",
|
||||
"appSelector.placeholder": "برنامه ای را انتخاب کنید...",
|
||||
"chooseAppFile": "بستهٔ برنامه یا فایل DSL را انتخاب کنید",
|
||||
"deleteAppConfirmContent": "حذف برنامه غیرقابل برگشت است. کاربران دیگر قادر به دسترسی به برنامه شما نخواهند بود و تمام تنظیمات و گزارشات درخواستها به صورت دائم حذف خواهند شد.",
|
||||
"deleteAppConfirmInputLabel": "برای تأیید، <appName>{{appName}}</appName> را در کادر زیر تایپ کنید:",
|
||||
"deleteAppConfirmInputPlaceholder": "نام برنامه را وارد کنید…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "ویرایش اطلاعات برنامه",
|
||||
"editDone": "اطلاعات برنامه بهروزرسانی شد",
|
||||
"editFailed": "بهروزرسانی اطلاعات برنامه ناموفق بود",
|
||||
"export": "صادر کردن DSL",
|
||||
"exportApp": "خروجی گرفتن از برنامه",
|
||||
"exportAppFailed": "خروجی گرفتن از برنامه ناموفق بود",
|
||||
"exportAppSuccess": "بستهٔ برنامه (.ifpkg) دانلود شد",
|
||||
"exportFailed": "صادر کردن DSL ناموفق بود.",
|
||||
"exportSecret.description": "در صورت انتخاب، مقادیر محرمانه در بستهٔ خروجی برنامه قرار میگیرند.",
|
||||
"exportSecret.title": "خروجی گرفتن از برنامه با مقادیر محرمانه",
|
||||
"filterEmpty.noApps": "هیچ برنامهای اینجا نیست",
|
||||
"firstEmpty.blankDescription": "وقتی میدانید چه میخواهید بسازید، با یک بوم خالی شروع کنید.",
|
||||
"firstEmpty.importDescription": "یک برنامه را از فایل تعریف Dify DSL بازیابی کنید.",
|
||||
"firstEmpty.importDescription": "برای بازیابی برنامهٔ بهاشتراکگذاشتهشده یا پشتیبانگیریشده، بستهٔ برنامه یا فایل DSL را وارد کنید.",
|
||||
"firstEmpty.learnDifyTitle": "یادگیری Dify",
|
||||
"firstEmpty.or": "یا",
|
||||
"firstEmpty.templateDescription": "از یک گردشکار از پیش ساخته شروع کنید. برای یادگیری Dify و الگوهای رایج مناسب است.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "بستهٔ برنامه یا فایل DSL",
|
||||
"importAppFormats": "از .ifpkg، .yaml و .yml پشتیبانی میکند",
|
||||
"importAppUrlPlaceholder": "پیوند بستهٔ برنامه (.ifpkg) یا فایل DSL را اینجا جایگذاری کنید",
|
||||
"importDSL": "وارد کردن فایل DSL",
|
||||
"importFromDSL": "وارد کردن از DSL",
|
||||
"importFromDSLFile": "از فایل DSL",
|
||||
"importFromDSLUrl": "از URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "تولید کننده متن",
|
||||
"newApp.completionShortDescription": "دستیار هوش مصنوعی برای تسک های تولید متن",
|
||||
"newApp.completionUserDescription": "به سرعت یک دستیار هوش مصنوعی برای وظایف تولید متن با پیکربندی ساده بسازید.",
|
||||
"newApp.dropDSLToCreateApp": "فایل DSL را اینجا رها کنید تا برنامه ساخته شود",
|
||||
"newApp.dropDSLToCreateApp": "برای ایجاد برنامه، بستهٔ برنامه یا فایل DSL را اینجا رها کنید",
|
||||
"newApp.forBeginners": "انواع برنامههای پایهتر",
|
||||
"newApp.foundResult": "{{count}} نتیجه",
|
||||
"newApp.foundResults": "{{count}} نتیجه",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "متوجه شدم",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "وقتی میدانید چه میخواهید بسازید، از یک بوم خالی شروع کنید.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "ساخت از ابتدا",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "فایل DSL دیفای دارید؟ آن را اینجا وارد کنید تا برنامهای را که به اشتراک گذاشتهاید یا از آن نسخه پشتیبان گرفتهاید بازیابی کنید.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "وارد کردن فایل DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "برای بازیابی برنامهٔ بهاشتراکگذاشتهشده یا پشتیبانگیریشده، بستهٔ برنامه یا فایل DSL را وارد کنید.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "وارد کردن برنامه",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "تازه با Dify آشنا شدهاید؟ ابتدا یک درس راهنما را طی کنید — Workflowها، Agentها و موارد دیگر را در عمل میبینید.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "یا با Learn Dify شروع کنید",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "قالبهای Dify را مرور کنید و قالبی را انتخاب کنید که با آنچه میخواهید بسازید هماهنگ است. راهی عالی برای شروع بدون نوشتن همه چیز از صفر.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "هنوز برنامهای برای مشاهده وجود ندارد",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "میتوانید برنامههای این فضای کاری را مرور و باز کنید، اما ساخت یا ویرایش برنامهها به مجوز نیاز دارد. به فضای کاری دارای دسترسی بروید یا با مالک یا مدیر فضای کاری تماس بگیرید.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio برای شما فقط قابل مشاهده است",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "برای افزودن برنامه جدید از Create استفاده کنید — یک قالب انتخاب کنید، از ابتدا شروع کنید یا یک فایل DSL وارد کنید.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "از «ایجاد» برای افزودن برنامه از الگو، شروع از ابتدا یا وارد کردن بستهٔ برنامه یا فایل DSL استفاده کنید.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "ساخت برنامه جدید",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "روی هر برنامه بزنید تا صفحه orchestration آن باز شود — از همانجا پرامپتها، مدلها و منطق را ویرایش کنید یا تنظیمات آن را مدیریت کنید.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "باز کردن و مدیریت هر برنامه",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "مشاهده نقاط دسترسی",
|
||||
"agent.acl.delete": "حذف عامل",
|
||||
"agent.acl.edit": "ویرایش اطلاعات و پیکربندی عامل",
|
||||
"agent.acl.import_export_dsl": "وارد کردن و صادر کردن DSL عامل",
|
||||
"agent.acl.import_export_dsl": "ورود و خروج عاملها",
|
||||
"agent.acl.log_manage": "مشاهده و مدیریت گزارشها",
|
||||
"agent.acl.monitor": "مشاهده نظارت عامل",
|
||||
"agent.acl.preview": "مشاهده اطلاعات پایه عامل",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "حذف برنامه",
|
||||
"app.acl.deploy": "استقرار برنامه",
|
||||
"app.acl.edit": "ویرایش اطلاعات برنامه و هماهنگسازی برنامه",
|
||||
"app.acl.import_export_dsl": "وارد کردن و صادر کردن DSL برنامه",
|
||||
"app.acl.import_export_dsl": "ورود و خروج برنامهها",
|
||||
"app.acl.log_and_annotation": "مشاهده و مدیریت گزارشها و حاشیهنویسیها",
|
||||
"app.acl.monitor": "مشاهده نظارت برنامه",
|
||||
"app.acl.preview": "پیشنمایش برنامه",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Aucun paramètre nécessaire",
|
||||
"appSelector.params": "PARAMÈTRES DE L’APPLICATION",
|
||||
"appSelector.placeholder": "Sélectionnez une application...",
|
||||
"chooseAppFile": "Choisir un paquet d’application ou un fichier DSL",
|
||||
"deleteAppConfirmContent": "La suppression de l'application est irréversible. Les utilisateurs ne pourront plus accéder à votre application et toutes les configurations de prompt et les journaux seront définitivement supprimés.",
|
||||
"deleteAppConfirmInputLabel": "Pour confirmer, tapez <appName>{{appName}}</appName> dans la case ci-dessous :",
|
||||
"deleteAppConfirmInputPlaceholder": "Entrez le nom de l'application…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Modifier les informations de l'application",
|
||||
"editDone": "Informations sur l'application mises à jour",
|
||||
"editFailed": "Échec de la mise à jour des informations de l'application",
|
||||
"export": "Exporter DSL",
|
||||
"exportApp": "Exporter l’application",
|
||||
"exportAppFailed": "Échec de l’exportation de l’application",
|
||||
"exportAppSuccess": "Paquet d’application (.ifpkg) téléchargé",
|
||||
"exportFailed": "Échec de l'exportation du DSL.",
|
||||
"exportSecret.description": "Si cette option est cochée, les valeurs secrètes seront incluses dans le paquet d’application exporté.",
|
||||
"exportSecret.title": "Exporter l’application avec les valeurs secrètes",
|
||||
"filterEmpty.noApps": "Aucune application ici",
|
||||
"firstEmpty.blankDescription": "Commencez avec un canevas vide lorsque vous savez quoi créer.",
|
||||
"firstEmpty.importDescription": "Restaurez une application depuis un fichier de définition DSL Dify.",
|
||||
"firstEmpty.importDescription": "Importez un paquet d’application ou un fichier DSL pour restaurer une application partagée ou sauvegardée.",
|
||||
"firstEmpty.learnDifyTitle": "Apprendre Dify",
|
||||
"firstEmpty.or": "Ou",
|
||||
"firstEmpty.templateDescription": "Commencez avec un workflow prédéfini. Idéal pour apprendre Dify et les modèles courants.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Paquet d’application ou fichier DSL",
|
||||
"importAppFormats": "Formats pris en charge : .ifpkg, .yaml et .yml",
|
||||
"importAppUrlPlaceholder": "Collez ici un lien vers un paquet d’application (.ifpkg) ou un fichier DSL",
|
||||
"importDSL": "Importer le fichier DSL",
|
||||
"importFromDSL": "Importation à partir d'une DSL",
|
||||
"importFromDSLFile": "À partir d’un fichier DSL",
|
||||
"importFromDSLUrl": "À partir de l’URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Générateur de texte",
|
||||
"newApp.completionShortDescription": "Assistant IA pour les tâches de génération de texte",
|
||||
"newApp.completionUserDescription": "Créez rapidement un assistant IA pour les tâches de génération de texte avec une configuration simple.",
|
||||
"newApp.dropDSLToCreateApp": "Déposez le fichier DSL ici pour créer une application",
|
||||
"newApp.dropDSLToCreateApp": "Déposez un paquet d’application ou un fichier DSL ici pour créer une application",
|
||||
"newApp.forBeginners": "Types d’applications plus basiques",
|
||||
"newApp.foundResult": "{{count}} Résultat",
|
||||
"newApp.foundResults": "{{count}} Résultats",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Compris",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Partez d'une toile vierge lorsque vous savez déjà quoi créer.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Créer à partir de zéro",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Vous avez un fichier Dify DSL ? Importez-le ici pour restaurer une app que vous avez partagée ou sauvegardée.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importer un fichier DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importez un paquet d’application ou un fichier DSL pour restaurer une application partagée ou sauvegardée.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importer l'App",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Nouveau sur Dify ? Suivez d'abord une leçon guidée — vous verrez les Workflows, les Agents et plus encore en action.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Ou commencez avec Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Parcourez les modèles de Dify et choisissez celui qui correspond à ce que vous voulez créer. Une excellente façon de démarrer sans partir de zéro.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Aucune app a afficher pour le moment",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Vous pouvez parcourir et ouvrir les apps de cet espace de travail, mais la creation ou la modification necessite une autorisation. Changez d espace de travail ou contactez le proprietaire ou un admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio est en lecture seule pour vous",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Utilisez Créer pour ajouter une nouvelle app — choisissez un modèle, partez de zéro, ou importez un fichier DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Utilisez « Créer » pour ajouter une application à partir d’un modèle, partir de zéro ou importer un paquet d’application ou un fichier DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Créer une nouvelle app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Appuyez sur n'importe quelle app pour ouvrir sa page d'orchestration — modifiez les prompts, les modèles et la logique, ou gérez ses paramètres depuis cet endroit.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Ouvrir et gérer chaque app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Afficher les points d’accès",
|
||||
"agent.acl.delete": "Supprimer l'agent",
|
||||
"agent.acl.edit": "Modifier les informations et la configuration de l'agent",
|
||||
"agent.acl.import_export_dsl": "Importer et exporter le DSL de l'agent",
|
||||
"agent.acl.import_export_dsl": "Importer et exporter des agents",
|
||||
"agent.acl.log_manage": "Afficher et gérer les journaux",
|
||||
"agent.acl.monitor": "Afficher la surveillance de l'agent",
|
||||
"agent.acl.preview": "Afficher les informations de base de l'agent",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Supprimer l'application",
|
||||
"app.acl.deploy": "Déployer l'application",
|
||||
"app.acl.edit": "Modifier les informations et orchestrer l'application",
|
||||
"app.acl.import_export_dsl": "Importer et exporter le DSL de l'application",
|
||||
"app.acl.import_export_dsl": "Importer et exporter des applications",
|
||||
"app.acl.log_and_annotation": "Afficher et gérer les journaux et annotations",
|
||||
"app.acl.monitor": "Afficher la surveillance de l'application",
|
||||
"app.acl.preview": "Prévisualiser l'application",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "कोई पैरामीटर की आवश्यकता नहीं है।",
|
||||
"appSelector.params": "ऐप पैरामीटर",
|
||||
"appSelector.placeholder": "एक ऐप चुनें...",
|
||||
"chooseAppFile": "ऐप पैकेज या DSL फ़ाइल चुनें",
|
||||
"deleteAppConfirmContent": "ऐप को हटाना अपरिवर्तनीय है। उपयोगकर्ता अब आपके ऐप तक पहुँचने में सक्षम नहीं होंगे, और सभी प्रॉम्प्ट कॉन्फ़िगरेशन और लॉग स्थायी रूप से हटा दिए जाएंगे।",
|
||||
"deleteAppConfirmInputLabel": "पुष्टि करने के लिए, नीचे दिए गए बॉक्स में <appName>{{appName}}</appName> टाइप करें:",
|
||||
"deleteAppConfirmInputPlaceholder": "ऐप का नाम दर्ज करें…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "ऐप जानकारी संपादित करें",
|
||||
"editDone": "ऐप जानकारी अपडेट की गई",
|
||||
"editFailed": "ऐप जानकारी अपडेट करने में विफल",
|
||||
"export": "डीएसएल निर्यात करें",
|
||||
"exportApp": "ऐप निर्यात करें",
|
||||
"exportAppFailed": "ऐप निर्यात नहीं हो सका",
|
||||
"exportAppSuccess": "ऐप पैकेज (.ifpkg) डाउनलोड हो गया",
|
||||
"exportFailed": "डीएसएल निर्यात विफल हुआ।",
|
||||
"exportSecret.description": "चयन करने पर गुप्त मान निर्यात किए गए ऐप पैकेज में शामिल होंगे।",
|
||||
"exportSecret.title": "गुप्त मानों सहित ऐप निर्यात करें",
|
||||
"filterEmpty.noApps": "यहाँ कोई ऐप नहीं है",
|
||||
"firstEmpty.blankDescription": "जब आपको पता हो कि क्या बनाना है, तो खाली कैनवास से शुरू करें।",
|
||||
"firstEmpty.importDescription": "Dify DSL परिभाषा फ़ाइल से ऐप पुनर्स्थापित करें।",
|
||||
"firstEmpty.importDescription": "साझा या बैकअप किए गए ऐप को पुनर्स्थापित करने के लिए ऐप पैकेज या DSL फ़ाइल आयात करें।",
|
||||
"firstEmpty.learnDifyTitle": "Dify सीखें",
|
||||
"firstEmpty.or": "या",
|
||||
"firstEmpty.templateDescription": "पहले से बने workflow से शुरू करें। Dify और सामान्य patterns सीखने के लिए बढ़िया।",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "ऐप पैकेज या DSL फ़ाइल",
|
||||
"importAppFormats": ".ifpkg, .yaml और .yml समर्थित हैं",
|
||||
"importAppUrlPlaceholder": "ऐप पैकेज (.ifpkg) या DSL का लिंक यहाँ पेस्ट करें",
|
||||
"importDSL": "डीएसएल फ़ाइल आयात करें",
|
||||
"importFromDSL": "DSL से आयात करें",
|
||||
"importFromDSLFile": "डीएसएल फ़ाइल से",
|
||||
"importFromDSLUrl": "यूआरएल से",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "टेक्स्ट जनरेटर",
|
||||
"newApp.completionShortDescription": "पाठ निर्माण कार्यों के लिए AI सहायक",
|
||||
"newApp.completionUserDescription": "सरल कॉन्फ़िगरेशन के साथ पाठ निर्माण कार्यों के लिए त्वरित रूप से AI सहायक बनाएं।",
|
||||
"newApp.dropDSLToCreateApp": "यहाँ DSL फ़ाइल ड्रॉप करें ताकि ऐप बनाया जा सके",
|
||||
"newApp.dropDSLToCreateApp": "ऐप बनाने के लिए यहाँ ऐप पैकेज या DSL फ़ाइल छोड़ें",
|
||||
"newApp.forBeginners": "नए उपयोगकर्ताओं के लिए बुनियादी ऐप प्रकार",
|
||||
"newApp.foundResult": "{{count}} परिणाम",
|
||||
"newApp.foundResults": "{{count}} परिणाम",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "समझ गए",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "जब आपको पता हो कि क्या बनाना है, तो खाली canvas से शुरू करें।",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "खाली से बनाएं",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "क्या आपके पास Dify DSL फ़ाइल है? किसी साझा किए गए या बैकअप किए गए ऐप को वापस लाने के लिए उसे यहां import करें।",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "DSL फ़ाइल import करें",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "साझा या बैकअप किए गए ऐप को पुनर्स्थापित करने के लिए ऐप पैकेज या DSL फ़ाइल आयात करें।",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "ऐप आयात करें",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Dify में नए हैं? पहले एक guided lesson पूरा करें — आप Workflows, Agents और बहुत कुछ काम करते हुए देखेंगे।",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "या Learn Dify से शुरू करें",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Dify के templates ब्राउज़ करें और वह चुनें जो आपकी ज़रूरत से मेल खाता हो। बिना शून्य से लिखे शुरू करने का यह बढ़िया तरीका है।",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "देखने के लिए अभी कोई ऐप नहीं है",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "आप इस वर्कस्पेस में ऐप ब्राउज़ और खोल सकते हैं, लेकिन ऐप बनाना या संपादित करना अनुमति मांगता है। ऐसे वर्कस्पेस में जाएं जहां आपके पास एक्सेस हो, या Workspace Owner या Admin से संपर्क करें।",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio आपके लिए केवल देखने योग्य है",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "नया ऐप जोड़ने के लिए Create का उपयोग करें — template चुनें, खाली से शुरू करें, या DSL फ़ाइल import करें।",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "टेम्पलेट से ऐप जोड़ने, खाली ऐप से शुरू करने या ऐप पैकेज अथवा DSL फ़ाइल आयात करने के लिए “बनाएँ” का उपयोग करें।",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "नया ऐप बनाएं",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "किसी भी ऐप पर टैप करें ताकि उसका orchestration पेज खुले — वहीं से prompts, models और logic संपादित करें या उसकी settings manage करें।",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "हर ऐप खोलें और manage करें",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "एक्सेस पॉइंट देखें",
|
||||
"agent.acl.delete": "एजेंट हटाएं",
|
||||
"agent.acl.edit": "एजेंट की जानकारी और कॉन्फ़िगरेशन संपादित करें",
|
||||
"agent.acl.import_export_dsl": "एजेंट DSL आयात और निर्यात करें",
|
||||
"agent.acl.import_export_dsl": "एजेंट आयात और निर्यात करें",
|
||||
"agent.acl.log_manage": "लॉग देखें और प्रबंधित करें",
|
||||
"agent.acl.monitor": "एजेंट मॉनिटरिंग देखें",
|
||||
"agent.acl.preview": "एजेंट की बुनियादी जानकारी देखें",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "ऐप हटाएं",
|
||||
"app.acl.deploy": "ऐप डिप्लॉय करें",
|
||||
"app.acl.edit": "ऐप की जानकारी संपादित करें और ऐप को ऑर्केस्ट्रेट करें",
|
||||
"app.acl.import_export_dsl": "ऐप DSL आयात और निर्यात करें",
|
||||
"app.acl.import_export_dsl": "ऐप आयात और निर्यात करें",
|
||||
"app.acl.log_and_annotation": "लॉग और एनोटेशन देखें और प्रबंधित करें",
|
||||
"app.acl.monitor": "ऐप मॉनिटरिंग देखें",
|
||||
"app.acl.preview": "ऐप पूर्वावलोकन करें",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Tidak perlu parameter",
|
||||
"appSelector.params": "PARAMETER APLIKASI",
|
||||
"appSelector.placeholder": "Pilih aplikasi...",
|
||||
"chooseAppFile": "Pilih paket aplikasi atau file DSL",
|
||||
"deleteAppConfirmContent": "Menghapus aplikasi tidak dapat diubah. Pengguna tidak akan dapat lagi mengakses aplikasi Anda, dan semua konfigurasi prompt serta log akan dihapus secara permanen.",
|
||||
"deleteAppConfirmInputLabel": "Untuk konfirmasi, ketik <appName>{{appName}}</appName> di kotak di bawah ini:",
|
||||
"deleteAppConfirmInputPlaceholder": "Masukkan nama aplikasi…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Edit Info Aplikasi",
|
||||
"editDone": "Info aplikasi diperbarui",
|
||||
"editFailed": "Gagal memperbarui info aplikasi",
|
||||
"export": "Ekspor DSL",
|
||||
"exportApp": "Ekspor Aplikasi",
|
||||
"exportAppFailed": "Gagal mengekspor aplikasi",
|
||||
"exportAppSuccess": "Paket aplikasi (.ifpkg) diunduh",
|
||||
"exportFailed": "Ekspor DSL gagal.",
|
||||
"exportSecret.description": "Jika dipilih, nilai rahasia akan disertakan dalam paket aplikasi yang diekspor.",
|
||||
"exportSecret.title": "Ekspor aplikasi dengan nilai rahasia",
|
||||
"filterEmpty.noApps": "Tidak ada aplikasi di sini",
|
||||
"firstEmpty.blankDescription": "Mulai dengan kanvas kosong saat Anda tahu apa yang ingin dibuat.",
|
||||
"firstEmpty.importDescription": "Pulihkan aplikasi dari file definisi DSL Dify.",
|
||||
"firstEmpty.importDescription": "Impor paket aplikasi atau file DSL untuk memulihkan aplikasi yang dibagikan atau dicadangkan.",
|
||||
"firstEmpty.learnDifyTitle": "Pelajari Dify",
|
||||
"firstEmpty.or": "Atau",
|
||||
"firstEmpty.templateDescription": "Mulai dari workflow siap pakai. Cocok untuk mempelajari Dify dan pola umum.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Paket aplikasi atau file DSL",
|
||||
"importAppFormats": "Mendukung .ifpkg, .yaml, dan .yml",
|
||||
"importAppUrlPlaceholder": "Tempel tautan paket aplikasi (.ifpkg) atau DSL di sini",
|
||||
"importDSL": "Impor file DSL",
|
||||
"importFromDSL": "Impor dari DSL",
|
||||
"importFromDSLFile": "Dari file DSL",
|
||||
"importFromDSLUrl": "Dari URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Pembuat Teks",
|
||||
"newApp.completionShortDescription": "Asisten AI untuk tugas pembuatan teks",
|
||||
"newApp.completionUserDescription": "Buat asisten AI dengan cepat untuk tugas pembuatan teks dengan konfigurasi sederhana.",
|
||||
"newApp.dropDSLToCreateApp": "Jatuhkan file DSL di sini untuk membuat aplikasi",
|
||||
"newApp.dropDSLToCreateApp": "Letakkan paket aplikasi atau file DSL di sini untuk membuat aplikasi",
|
||||
"newApp.forBeginners": "Jenis aplikasi yang lebih dasar",
|
||||
"newApp.foundResult": "Hasil {{count}}",
|
||||
"newApp.foundResults": "Hasil {{count}}",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Mengerti",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Mulai dari kanvas kosong jika Anda sudah tahu apa yang ingin dibangun.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Buat dari kosong",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Punya file DSL Dify? Impor di sini untuk memulihkan app yang pernah Anda bagikan atau cadangkan.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Impor file DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Impor paket aplikasi atau file DSL untuk memulihkan aplikasi yang dibagikan atau dicadangkan.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Impor Aplikasi",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Baru di Dify? Ikuti dulu pelajaran berpemandu — Anda akan melihat Workflows, Agents, dan lainnya bekerja.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Atau mulai dengan Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Telusuri template Dify dan pilih yang sesuai dengan yang ingin Anda bangun. Cara bagus untuk memulai tanpa menulis dari nol.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Belum ada app untuk dilihat",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Anda dapat menelusuri dan membuka app di workspace ini, tetapi membuat atau mengedit app memerlukan izin. Pindah ke workspace yang dapat Anda akses, atau hubungi Workspace Owner atau Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio hanya dapat dilihat oleh Anda",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Gunakan Create untuk menambahkan app baru — pilih template, mulai dari kosong, atau impor file DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Gunakan “Buat” untuk menambahkan aplikasi dari templat, mulai dari awal, atau mengimpor paket aplikasi maupun file DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Buat app baru",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Ketuk app mana pun untuk membuka halaman orkestrasinya — edit prompt, model, dan logika, atau kelola pengaturannya dari sana.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Buka dan kelola setiap app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Lihat titik akses",
|
||||
"agent.acl.delete": "Hapus agen",
|
||||
"agent.acl.edit": "Edit informasi dan konfigurasi agen",
|
||||
"agent.acl.import_export_dsl": "Impor dan ekspor DSL agen",
|
||||
"agent.acl.import_export_dsl": "Impor dan ekspor agen",
|
||||
"agent.acl.log_manage": "Lihat dan kelola log",
|
||||
"agent.acl.monitor": "Lihat pemantauan agen",
|
||||
"agent.acl.preview": "Lihat informasi dasar agen",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Hapus aplikasi",
|
||||
"app.acl.deploy": "Deploy aplikasi",
|
||||
"app.acl.edit": "Edit informasi aplikasi dan orkestrasikan aplikasi",
|
||||
"app.acl.import_export_dsl": "Impor dan ekspor DSL aplikasi",
|
||||
"app.acl.import_export_dsl": "Impor dan ekspor aplikasi",
|
||||
"app.acl.log_and_annotation": "Lihat dan kelola log serta anotasi",
|
||||
"app.acl.monitor": "Lihat pemantauan aplikasi",
|
||||
"app.acl.preview": "Pratinjau aplikasi",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Non sono necessari parametri",
|
||||
"appSelector.params": "PARAMETRI DELL'APP",
|
||||
"appSelector.placeholder": "Seleziona un'app...",
|
||||
"chooseAppFile": "Scegli un pacchetto app o un file DSL",
|
||||
"deleteAppConfirmContent": "Eliminare l'app è irreversibile. Gli utenti non potranno più accedere alla tua app e tutte le configurazioni e i log dei prompt verranno eliminati permanentemente.",
|
||||
"deleteAppConfirmInputLabel": "Per confermare, digita <appName>{{appName}}</appName> nel campo sottostante:",
|
||||
"deleteAppConfirmInputPlaceholder": "Inserisci il nome dell'app…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Modifica Info App",
|
||||
"editDone": "Info app aggiornata",
|
||||
"editFailed": "Aggiornamento delle info dell'app fallito",
|
||||
"export": "Esporta DSL",
|
||||
"exportApp": "Esporta app",
|
||||
"exportAppFailed": "Impossibile esportare l’app",
|
||||
"exportAppSuccess": "Pacchetto app (.ifpkg) scaricato",
|
||||
"exportFailed": "Esportazione DSL fallita.",
|
||||
"exportSecret.description": "Se selezionato, i valori segreti saranno inclusi nel pacchetto app esportato.",
|
||||
"exportSecret.title": "Esporta app con valori segreti",
|
||||
"filterEmpty.noApps": "Nessuna app qui",
|
||||
"firstEmpty.blankDescription": "Inizia con una tela vuota quando sai cosa vuoi creare.",
|
||||
"firstEmpty.importDescription": "Ripristina un’app da un file di definizione DSL Dify.",
|
||||
"firstEmpty.importDescription": "Importa un pacchetto app o un file DSL per ripristinare un’app condivisa o di cui è stato eseguito il backup.",
|
||||
"firstEmpty.learnDifyTitle": "Impara Dify",
|
||||
"firstEmpty.or": "Oppure",
|
||||
"firstEmpty.templateDescription": "Inizia da un workflow preconfigurato. Ideale per imparare Dify e i pattern comuni.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Pacchetto app o file DSL",
|
||||
"importAppFormats": "Supporta .ifpkg, .yaml e .yml",
|
||||
"importAppUrlPlaceholder": "Incolla qui un link a un pacchetto app (.ifpkg) o a un file DSL",
|
||||
"importDSL": "Importa file DSL",
|
||||
"importFromDSL": "Importazione da DSL",
|
||||
"importFromDSLFile": "Da file DSL",
|
||||
"importFromDSLUrl": "Dall'URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Generatore di Testi",
|
||||
"newApp.completionShortDescription": "Assistente AI per le attività di generazione del testo",
|
||||
"newApp.completionUserDescription": "Crea rapidamente un assistente AI per le attività di generazione di testo con una configurazione semplice.",
|
||||
"newApp.dropDSLToCreateApp": "Trascina il file DSL qui per creare l'app",
|
||||
"newApp.dropDSLToCreateApp": "Trascina qui un pacchetto app o un file DSL per creare un’app",
|
||||
"newApp.forBeginners": "Tipi di app più semplici",
|
||||
"newApp.foundResult": "{{count}} Risultato",
|
||||
"newApp.foundResults": "{{count}} Risultati",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Ho capito",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Parti da una tela vuota quando sai già cosa creare.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Crea da zero",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Hai un file DSL di Dify? Importalo qui per ripristinare un'app che hai condiviso o di cui hai fatto il backup.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importa un file DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importa un pacchetto app o un file DSL per ripristinare un’app condivisa o di cui è stato eseguito il backup.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importa App",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Sei nuovo su Dify? Segui prima una lezione guidata — vedrai Workflow, Agent e altro in azione.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Oppure inizia con Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Sfoglia i template di Dify e scegline uno adatto a ciò che vuoi creare. Un ottimo modo per iniziare senza scrivere nulla da zero.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Nessuna app da visualizzare",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Puoi sfogliare e aprire le app in questo workspace, ma creare o modificare app richiede un permesso. Passa a un workspace con accesso o contatta il Workspace Owner o Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio e in sola visualizzazione per te",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Usa Crea per aggiungere una nuova app — scegli un template, parti da zero o importa un file DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Usa “Crea” per aggiungere un’app da un modello, iniziare da zero o importare un pacchetto app o un file DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Crea una nuova app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Tocca una qualsiasi app per aprire la sua pagina di orchestrazione — modifica prompt, modelli e logica, oppure gestisci le impostazioni da lì.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Apri e gestisci ogni app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Visualizza i punti di accesso",
|
||||
"agent.acl.delete": "Elimina agente",
|
||||
"agent.acl.edit": "Modifica le informazioni e la configurazione dell'agente",
|
||||
"agent.acl.import_export_dsl": "Importa ed esporta il DSL dell'agente",
|
||||
"agent.acl.import_export_dsl": "Importa ed esporta agenti",
|
||||
"agent.acl.log_manage": "Visualizza e gestisci i log",
|
||||
"agent.acl.monitor": "Visualizza il monitoraggio dell'agente",
|
||||
"agent.acl.preview": "Visualizza le informazioni di base dell'agente",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Elimina app",
|
||||
"app.acl.deploy": "Distribuisci app",
|
||||
"app.acl.edit": "Modifica le informazioni e orchestra l'app",
|
||||
"app.acl.import_export_dsl": "Importa ed esporta il DSL dell'app",
|
||||
"app.acl.import_export_dsl": "Importa ed esporta app",
|
||||
"app.acl.log_and_annotation": "Visualizza e gestisci log e annotazioni",
|
||||
"app.acl.monitor": "Visualizza il monitoraggio dell'app",
|
||||
"app.acl.preview": "Anteprima app",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "パラメータは必要ありません",
|
||||
"appSelector.params": "アプリパラメータ",
|
||||
"appSelector.placeholder": "アプリを選択...",
|
||||
"chooseAppFile": "アプリパッケージまたは DSL ファイルを選択",
|
||||
"deleteAppConfirmContent": "アプリを削除すると、元に戻すことはできません。他のユーザーはもはやこのアプリにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。",
|
||||
"deleteAppConfirmInputLabel": "確認するには、下のボックスに<appName>{{appName}}</appName>と入力してください:",
|
||||
"deleteAppConfirmInputPlaceholder": "アプリ名を入力…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "アプリ情報を編集する",
|
||||
"editDone": "アプリ情報が更新されました",
|
||||
"editFailed": "アプリ情報の更新に失敗しました",
|
||||
"export": "DSL をエクスポート",
|
||||
"exportApp": "アプリをエクスポート",
|
||||
"exportAppFailed": "アプリのエクスポートに失敗しました",
|
||||
"exportAppSuccess": "アプリパッケージ(.ifpkg)をダウンロードしました",
|
||||
"exportFailed": "DSL のエクスポートに失敗しました。",
|
||||
"exportSecret.description": "選択すると、エクスポートされるアプリパッケージにシークレット値が含まれます。",
|
||||
"exportSecret.title": "シークレット値を含めてアプリをエクスポート",
|
||||
"filterEmpty.noApps": "ここにアプリはありません",
|
||||
"firstEmpty.blankDescription": "空白のキャンバスから始めて、アプリを一歩ずつ作り上げます。",
|
||||
"firstEmpty.importDescription": "DSL としてエクスポート済みの Dify アプリをお持ちですか?取り込めば、続きから作業を再開できます。",
|
||||
"firstEmpty.importDescription": "アプリパッケージまたは DSL ファイルをインポートして、共有またはバックアップされたアプリを復元できます。",
|
||||
"firstEmpty.learnDifyTitle": "テンプレートで Dify を学ぶ",
|
||||
"firstEmpty.or": "または",
|
||||
"firstEmpty.templateDescription": "用意されたアプリを選んでカスタマイズ。Dify を最速で体感できる方法です。",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "アプリパッケージまたは DSL ファイル",
|
||||
"importAppFormats": "対応形式:.ifpkg、.yaml、.yml",
|
||||
"importAppUrlPlaceholder": "アプリパッケージ(.ifpkg)または DSL ファイルのリンクをここに貼り付けます",
|
||||
"importDSL": "DSL ファイルをインポート",
|
||||
"importFromDSL": "DSL からインポート",
|
||||
"importFromDSLFile": "DSL ファイルから",
|
||||
"importFromDSLUrl": "URL から",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "テキスト ジェネレーター",
|
||||
"newApp.completionShortDescription": "複数ターンチャット向けに強化されたワークフロー",
|
||||
"newApp.completionUserDescription": "簡単な構成でテキスト生成タスク用の AI アシスタントをすばやく構築します。",
|
||||
"newApp.dropDSLToCreateApp": "アプリを作成するにはここにDSLファイルをドロップしてください",
|
||||
"newApp.dropDSLToCreateApp": "アプリパッケージまたは DSL ファイルをここにドロップしてアプリを作成",
|
||||
"newApp.forBeginners": "初心者向けの基本的なアプリタイプ",
|
||||
"newApp.foundResult": "{{count}}件の結果",
|
||||
"newApp.foundResults": "{{count}}件の結果",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "了解",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "何を作るかが決まっているなら、空のキャンバスから始めましょう。",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "空白から作成",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Dify の DSL ファイルがあれば、ここからインポートして共有・バックアップしたアプリを復元できます。",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "DSL ファイルをインポート",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "アプリパッケージまたは DSL ファイルをインポートして、共有またはバックアップされたアプリを復元できます。",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "アプリをインポート",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "初めての方は、まず Learn Dify レッスンを試してみましょう — Workflow や Agent などが実際に動くのを見られます。",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "または Learn Dify から始める",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Dify のテンプレートを探して、作りたいものに合うものを選びましょう。ゼロから始める必要はありません。",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "表示できるアプリはまだありません",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "このワークスペースのアプリを閲覧できます。アプリを作成または編集するには、権限のあるワークスペースに切り替えるか、ワークスペースオーナーまたは管理者に連絡してください。",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio は閲覧のみ可能です",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Create から新しいアプリを追加できます — テンプレート、空白、DSL ファイルから選べます。",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "「作成」から、テンプレート、空白のアプリ、アプリパッケージまたは DSL ファイルのインポートを選んで新しいアプリを追加できます。",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "新しいアプリを作成",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "任意のアプリをタップして編成ページを開きます — そこでプロンプト、モデル、ロジックを編集したり、設定を管理できます。",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "各アプリを開いて管理",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "アクセスポイントを表示",
|
||||
"agent.acl.delete": "エージェントを削除",
|
||||
"agent.acl.edit": "エージェント情報と設定の編集",
|
||||
"agent.acl.import_export_dsl": "エージェント DSL のインポートとエクスポート",
|
||||
"agent.acl.import_export_dsl": "エージェントのインポートとエクスポート",
|
||||
"agent.acl.log_manage": "ログの表示と管理",
|
||||
"agent.acl.monitor": "エージェントのモニタリングを表示",
|
||||
"agent.acl.preview": "エージェントの基本情報を表示",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "アプリを削除",
|
||||
"app.acl.deploy": "アプリをデプロイ",
|
||||
"app.acl.edit": "アプリ情報の編集とアプリのオーケストレーション",
|
||||
"app.acl.import_export_dsl": "アプリ DSL のインポートとエクスポート",
|
||||
"app.acl.import_export_dsl": "アプリのインポートとエクスポート",
|
||||
"app.acl.log_and_annotation": "ログと注釈の表示と管理",
|
||||
"app.acl.monitor": "アプリのモニタリングを表示",
|
||||
"app.acl.preview": "アプリをプレビュー",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "매개 변수가 필요하지 않습니다.",
|
||||
"appSelector.params": "앱 매개 변수",
|
||||
"appSelector.placeholder": "앱 선택...",
|
||||
"chooseAppFile": "앱 패키지 또는 DSL 파일 선택",
|
||||
"deleteAppConfirmContent": "앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.",
|
||||
"deleteAppConfirmInputLabel": "확인하려면 아래 상자에 <appName>{{appName}}</appName>을 입력하세요:",
|
||||
"deleteAppConfirmInputPlaceholder": "앱 이름 입력…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "앱 정보 편집하기",
|
||||
"editDone": "앱 정보가 업데이트되었습니다",
|
||||
"editFailed": "앱 정보 업데이트 실패",
|
||||
"export": "DSL 내보내기",
|
||||
"exportApp": "앱 내보내기",
|
||||
"exportAppFailed": "앱을 내보내지 못했습니다",
|
||||
"exportAppSuccess": "앱 패키지(.ifpkg)를 다운로드했습니다",
|
||||
"exportFailed": "DSL 내보내기 실패",
|
||||
"exportSecret.description": "선택하면 내보낸 앱 패키지에 비밀 값이 포함됩니다.",
|
||||
"exportSecret.title": "비밀 값을 포함하여 앱 내보내기",
|
||||
"filterEmpty.noApps": "여기에 앱이 없습니다",
|
||||
"firstEmpty.blankDescription": "무엇을 만들지 알고 있다면 빈 캔버스에서 시작하세요.",
|
||||
"firstEmpty.importDescription": "Dify DSL 정의 파일에서 앱을 복원합니다.",
|
||||
"firstEmpty.importDescription": "앱 패키지 또는 DSL 파일을 가져와 공유하거나 백업한 앱을 복원하세요.",
|
||||
"firstEmpty.learnDifyTitle": "Dify 배우기",
|
||||
"firstEmpty.or": "또는",
|
||||
"firstEmpty.templateDescription": "미리 만들어진 workflow에서 시작하세요. Dify와 일반적인 패턴을 배우기에 좋습니다.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "앱 패키지 또는 DSL 파일",
|
||||
"importAppFormats": ".ifpkg, .yaml 및 .yml 지원",
|
||||
"importAppUrlPlaceholder": "앱 패키지(.ifpkg) 또는 DSL 링크를 여기에 붙여넣으세요",
|
||||
"importDSL": "DSL 파일 가져오기",
|
||||
"importFromDSL": "DSL 에서 가져오기",
|
||||
"importFromDSLFile": "DSL 파일에서",
|
||||
"importFromDSLUrl": "URL 에서",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "텍스트 생성기",
|
||||
"newApp.completionShortDescription": "텍스트 생성 작업을 위한 AI 도우미",
|
||||
"newApp.completionUserDescription": "간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.",
|
||||
"newApp.dropDSLToCreateApp": "여기에 DSL 파일을 드롭하여 앱을 불러오세요.",
|
||||
"newApp.dropDSLToCreateApp": "여기에 앱 패키지 또는 DSL 파일을 놓아 앱을 만드세요",
|
||||
"newApp.forBeginners": "초보자용 기본 앱 유형",
|
||||
"newApp.foundResult": "{{count}} 결과",
|
||||
"newApp.foundResults": "{{count}} 결과",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "확인",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "무엇을 만들지 이미 알고 있다면 빈 캔버스에서 시작하세요.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "빈 상태에서 만들기",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Dify DSL 파일이 있나요? 여기서 가져와 공유했거나 백업한 앱을 복원하세요.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "DSL 파일 가져오기",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "앱 패키지 또는 DSL 파일을 가져와 공유하거나 백업한 앱을 복원하세요.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "앱 가져오기",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Dify가 처음이신가요? 먼저 가이드 레슨을 따라가 보세요 — Workflow, Agent 등이 실제로 작동하는 모습을 볼 수 있습니다.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "또는 Learn Dify로 시작하기",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Dify의 템플릿을 둘러보고 만들려는 것과 맞는 템플릿을 선택하세요. 처음부터 아무것도 작성하지 않고 시작하기에 좋은 방법입니다.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "아직 볼 수 있는 앱이 없습니다",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "이 워크스페이스의 앱을 둘러보고 열 수 있지만, 앱을 만들거나 편집하려면 권한이 필요합니다. 접근 권한이 있는 워크스페이스로 전환하거나 Workspace Owner 또는 Admin에게 문의하세요.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio는 보기 전용입니다",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "만들기를 사용해 새 앱을 추가하세요 — 템플릿에서 선택하거나, 빈 상태에서 시작하거나, DSL 파일을 가져올 수 있습니다.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "“만들기”를 사용하여 템플릿에서 시작하거나 빈 앱을 만들거나 앱 패키지 또는 DSL 파일을 가져오세요.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "새 앱 만들기",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "앱을 탭하면 오케스트레이션 페이지가 열립니다 — 거기서 프롬프트, 모델, 로직을 편집하거나 설정을 관리할 수 있습니다.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "각 앱 열고 관리하기",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "액세스 포인트 보기",
|
||||
"agent.acl.delete": "에이전트 삭제",
|
||||
"agent.acl.edit": "에이전트 정보 및 구성 편집",
|
||||
"agent.acl.import_export_dsl": "에이전트 DSL 가져오기 및 내보내기",
|
||||
"agent.acl.import_export_dsl": "에이전트 가져오기 및 내보내기",
|
||||
"agent.acl.log_manage": "로그 보기 및 관리",
|
||||
"agent.acl.monitor": "에이전트 모니터링 보기",
|
||||
"agent.acl.preview": "에이전트 기본 정보 보기",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "앱 삭제",
|
||||
"app.acl.deploy": "앱 배포",
|
||||
"app.acl.edit": "앱 정보 편집 및 앱 오케스트레이션",
|
||||
"app.acl.import_export_dsl": "앱 DSL 가져오기 및 내보내기",
|
||||
"app.acl.import_export_dsl": "앱 가져오기 및 내보내기",
|
||||
"app.acl.log_and_annotation": "로그 및 주석 보기 및 관리",
|
||||
"app.acl.monitor": "앱 모니터링 보기",
|
||||
"app.acl.preview": "앱 미리보기",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "ບໍ່ຕ້ອງມີພາຣາມີເຕີ",
|
||||
"appSelector.params": "ພາຣາມີເຕີຂອງແອັບ",
|
||||
"appSelector.placeholder": "ເລືອກແອັບ...",
|
||||
"chooseAppFile": "ເລືອກແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL",
|
||||
"deleteAppConfirmContent": "ການລຶບແອັບບໍ່ສາມາດກັບຄືນມາໄດ້. ຜູ້ໃຊ້ຈະບໍ່ສາມາດເຂົ້າເຖິງແອັບຂອງທ່ານໄດ້ອີກຕໍ່ໄປ, ແລະ ການຕັ້ງຄ່າ prompt ລວມທັງບັນທຶກທັງໝົດຈະຖືກລຶບຖາວອນ.",
|
||||
"deleteAppConfirmInputLabel": "ເພື່ອຢືນຢັນ, ໃຫ້ພິມ <appName>{{appName}}</appName> ໃນຫ້ອງລຸ່ມນີ້:",
|
||||
"deleteAppConfirmInputPlaceholder": "ປ້ອນຊື່ແອັບ…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "ແກ້ໄຂຂໍ້ມູນແອັບ",
|
||||
"editDone": "ອັບເດດຂໍ້ມູນແອັບແລ້ວ",
|
||||
"editFailed": "ອັບເດດຂໍ້ມູນແອັບບໍ່ສຳເລັດ",
|
||||
"export": "ສົ່ງອອກ DSL",
|
||||
"exportApp": "ສົ່ງອອກແອັບ",
|
||||
"exportAppFailed": "ສົ່ງອອກແອັບບໍ່ສຳເລັດ",
|
||||
"exportAppSuccess": "ດາວໂຫຼດແພັກເກດແອັບ (.ifpkg) ແລ້ວ",
|
||||
"exportFailed": "ສົ່ງອອກ DSL ບໍ່ສຳເລັດ.",
|
||||
"exportSecret.description": "ຖ້າເລືອກ ຄ່າລັບຈະລວມຢູ່ໃນແພັກເກດແອັບທີ່ສົ່ງອອກ.",
|
||||
"exportSecret.title": "ສົ່ງອອກແອັບພ້ອມຄ່າລັບ",
|
||||
"filterEmpty.noApps": "ບໍ່ມີແອັບຢູ່ນີ້",
|
||||
"firstEmpty.blankDescription": "ເລີ່ມຈາກໜ້າເປົ່າຫວ່າງ ແລະ ສ້າງແອັບຂອງທ່ານໄປເທື່ອລະຂັ້ນຕອນ.",
|
||||
"firstEmpty.importDescription": "ມີແອັບ Dify ທີ່ສົ່ງອອກເປັນ DSL ຢູ່ແລ້ວບໍ? ນຳເຂົ້າມັນເພື່ອສືບຕໍ່ວຽກທີ່ທ່ານເຮັດຄ້າງໄວ້.",
|
||||
"firstEmpty.importDescription": "ນຳເຂົ້າແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL ເພື່ອກູ້ຄືນແອັບທີ່ແບ່ງປັນ ຫຼື ສຳຮອງໄວ້.",
|
||||
"firstEmpty.learnDifyTitle": "ຮຽນຮູ້ Dify ຈາກແມ່ແບບ",
|
||||
"firstEmpty.or": "ຫຼື",
|
||||
"firstEmpty.templateDescription": "ເລືອກແອັບທີ່ພ້ອມໃຊ້ງານ ແລະ ປັບແຕ່ງຕາມໃຈ. ວິທີທີ່ໄວທີ່ສຸດໃນການເບິ່ງການເຮັດວຽກຂອງ Dify.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "ແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL",
|
||||
"importAppFormats": "ຮອງຮັບ .ifpkg, .yaml ແລະ .yml",
|
||||
"importAppUrlPlaceholder": "ວາງລິ້ງແພັກເກດແອັບ (.ifpkg) ຫຼື DSL ທີ່ນີ້",
|
||||
"importDSL": "ນຳເຂົ້າໄຟລ໌ DSL",
|
||||
"importFromDSL": "ນຳເຂົ້າຈາກ DSL",
|
||||
"importFromDSLFile": "ຈາກໄຟລ໌ DSL",
|
||||
"importFromDSLUrl": "ຈາກ URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "ຕົວສ້າງຂໍ້ຄວາມ",
|
||||
"newApp.completionShortDescription": "ຜູ້ຊ່ວຍ AI ສຳລັບວຽກສ້າງຂໍ້ຄວາມ",
|
||||
"newApp.completionUserDescription": "ສ້າງຜູ້ຊ່ວຍ AI ສຳລັບວຽກສ້າງຂໍ້ຄວາມໄດ້ຢ່າງວ່ອງໄວດ້ວຍການຕັ້ງຄ່າງ່າຍໆ.",
|
||||
"newApp.dropDSLToCreateApp": "ວາງໄຟລ໌ DSL ຢູ່ນີ້ເພື່ອສ້າງແອັບ",
|
||||
"newApp.dropDSLToCreateApp": "ວາງແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL ທີ່ນີ້ເພື່ອສ້າງແອັບ",
|
||||
"newApp.forBeginners": "ປະເພດແອັບພື້ນຖານເພີ່ມເຕີມ",
|
||||
"newApp.foundResult": "{{count}} ຜົນລາຍການ",
|
||||
"newApp.foundResults": "{{count}} ຜົນລາຍການ",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "ຮັບຊາບແລ້ວ",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "ເລີ່ມຕົ້ນຈາກໜ້າຫວ່າງເປົ່າເມື່ອທ່ານຮູ້ແລ້ວວ່າຕ້ອງການສ້າງຫຍັງ.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "ສ້າງຈາກໜ້າຫວ່າງ",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "ມີໄຟລ໌ Dify DSL ບໍ? ນຳເຂົ້າທີ່ນີ້ເພື່ອຕັ້ງແອັບທີ່ທ່ານໄດ້ແບ່ງປັນ ຫຼື ສຳຮອງໄວ້ຄືນໃໝ່.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "ນຳເຂົ້າໄຟລ໌ DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "ນຳເຂົ້າແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL ເພື່ອກູ້ຄືນແອັບທີ່ແບ່ງປັນ ຫຼື ສຳຮອງໄວ້.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "ນຳເຂົ້າແອັບ",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "ໃໝ່ສຳລັບ Dify ບໍ? ເບິ່ງບົດຮຽນແນະນຳກ່ອນ — ທ່ານຈະໄດ້ເຫັນການເຮັດວຽກຂອງ Workflows, Agents ແລະ ອື່ນໆ.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "ຫຼື ເລີ່ມຕົ້ນດ້ວຍການຮຽນຮູ້ Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "ເລືອກເບິ່ງແມ່ແບບຂອງ Dify ແລະ ເລືອກອັນທີ່ກົງກັບສິ່ງທີ່ທ່ານຕ້ອງການສ້າງ. ເປັນວິທີທີ່ດີໃນການເລີ່ມຕົ້ນໂດຍບໍ່ຕ້ອງຂຽນເອງທັງໝົດ.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "ຍັງບໍ່ມີແອັບໃຫ້ເບິ່ງເທື່ອ",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "ທ່ານສາມາດເລືອກເບິ່ງແອັບໃນພື້ນທີ່ເຮັດວຽກນີ້ໄດ້. ເພື່ອສ້າງ ຫຼື ແກ້ໄຂແອັບ, ໃຫ້ປ່ຽນໄປໃຊ້ພື້ນທີ່ເຮັດວຽກທີ່ທ່ານມີສິດ, ຫຼື ຕິດຕໍ່ເຈົ້າຂອງພື້ນທີ່ເຮັດວຽກ ຫຼື ຜູ້ດູແລ.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "ສະຕູດິໂອສຳລັບທ່ານແມ່ນເບິ່ງໄດ້ຢ່າງດຽວ",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "ໃຊ້ປຸ່ມ 'ສ້າງ' ເພື່ອເພີ່ມແອັບໃໝ່ — ເລືອກຈາກແມ່ແບບ, ເລີ່ມຈາກໜ້າຫວ່າງ ຫຼື ນຳເຂົ້າໄຟລ໌ DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "ໃຊ້ “ສ້າງ” ເພື່ອເພີ່ມແອັບຈາກແມ່ແບບ, ເລີ່ມຈາກແອັບເປົ່າ ຫຼື ນຳເຂົ້າແພັກເກດແອັບ ຫຼື ໄຟລ໌ DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "ສ້າງແອັບໃໝ່",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "ແຕະທີ່ແອັບໃດໜຶ່ງເພື່ອເປີດໜ້າການຈັດການ — ແກ້ໄຂຄຳສັ່ງ, ໂມເດວ ແລະ ຕັກກະ, ຫຼື ຈັດການການຕັ້ງຄ່າຈາກບ່ອນນັ້ນ.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "ເປີດ ແລະ ຈັດການແຕ່ລະແອັບ",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "ເບິ່ງຈຸດເຂົ້າເຖິງ",
|
||||
"agent.acl.delete": "ລຶບຕົວແທນ",
|
||||
"agent.acl.edit": "ແກ້ໄຂຂໍ້ມູນ ແລະ ການຕັ້ງຄ່າຕົວແທນ",
|
||||
"agent.acl.import_export_dsl": "ນຳເຂົ້າ ແລະ ສົ່ງອອກ DSL ຂອງຕົວແທນ",
|
||||
"agent.acl.import_export_dsl": "ນຳເຂົ້າ ແລະ ສົ່ງອອກເອເຈນ",
|
||||
"agent.acl.log_manage": "ເບິ່ງ ແລະ ຈັດການບັນທຶກ",
|
||||
"agent.acl.monitor": "ເບິ່ງການຕິດຕາມຕົວແທນ",
|
||||
"agent.acl.preview": "ເບິ່ງຂໍ້ມູນພື້ນຖານຂອງຕົວແທນ",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "ລຶບແອັບ",
|
||||
"app.acl.deploy": "ຕິດຕັ້ງແອັບ",
|
||||
"app.acl.edit": "ແກ້ໄຂຂໍ້ມູນແອັບ ແລະ ຈັດການລະບົບແອັບ",
|
||||
"app.acl.import_export_dsl": "ນຳເຂົ້າ ແລະ ສົ່ງອອກ DSL ຂອງແອັບ",
|
||||
"app.acl.import_export_dsl": "ນຳເຂົ້າ ແລະ ສົ່ງອອກແອັບ",
|
||||
"app.acl.log_and_annotation": "ເບິ່ງ ແລະ ຈັດການບັນທຶກ ແລະ ຄຳອະທິບາຍ",
|
||||
"app.acl.monitor": "ເບິ່ງການຕິດຕາມແອັບ",
|
||||
"app.acl.preview": "ເບິ່ງຕົວຢ່າງແອັບ",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "No parameters needed",
|
||||
"appSelector.params": "APP PARAMETERS",
|
||||
"appSelector.placeholder": "Select an app...",
|
||||
"chooseAppFile": "Kies een app-pakket of DSL-bestand",
|
||||
"deleteAppConfirmContent": "Deleting the app is irreversible. Users will no longer be able to access your app, and all prompt configurations and logs will be permanently deleted.",
|
||||
"deleteAppConfirmInputLabel": "To confirm, type <appName>{{appName}}</appName> in the box below:",
|
||||
"deleteAppConfirmInputPlaceholder": "Enter app name…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Edit App Info",
|
||||
"editDone": "App info updated",
|
||||
"editFailed": "Failed to update app info",
|
||||
"export": "Export DSL",
|
||||
"exportApp": "App exporteren",
|
||||
"exportAppFailed": "App exporteren mislukt",
|
||||
"exportAppSuccess": "App-pakket (.ifpkg) gedownload",
|
||||
"exportFailed": "Export DSL failed.",
|
||||
"exportSecret.description": "Indien geselecteerd, worden geheime waarden opgenomen in het geëxporteerde app-pakket.",
|
||||
"exportSecret.title": "App met geheime waarden exporteren",
|
||||
"filterEmpty.noApps": "Geen apps hier",
|
||||
"firstEmpty.blankDescription": "Begin met een leeg canvas wanneer je weet wat je wilt bouwen.",
|
||||
"firstEmpty.importDescription": "Herstel een app vanuit een Dify DSL-definitiebestand.",
|
||||
"firstEmpty.importDescription": "Importeer een app-pakket of DSL-bestand om een gedeelde app of back-up te herstellen.",
|
||||
"firstEmpty.learnDifyTitle": "Leer Dify",
|
||||
"firstEmpty.or": "Of",
|
||||
"firstEmpty.templateDescription": "Begin met een vooraf gebouwde workflow. Handig om Dify en veelgebruikte patronen te leren.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "App-pakket of DSL-bestand",
|
||||
"importAppFormats": "Ondersteunt .ifpkg, .yaml en .yml",
|
||||
"importAppUrlPlaceholder": "Plak hier een link naar een app-pakket (.ifpkg) of DSL-bestand",
|
||||
"importDSL": "Import DSL file",
|
||||
"importFromDSL": "Import from DSL",
|
||||
"importFromDSLFile": "From DSL file",
|
||||
"importFromDSLUrl": "From URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Text Generator",
|
||||
"newApp.completionShortDescription": "AI assistant for text generation tasks",
|
||||
"newApp.completionUserDescription": "Quickly build an AI assistant for text generation tasks with simple configuration.",
|
||||
"newApp.dropDSLToCreateApp": "Drop DSL file here to create app",
|
||||
"newApp.dropDSLToCreateApp": "Sleep hier een app-pakket of DSL-bestand naartoe om een app te maken",
|
||||
"newApp.forBeginners": "More basic app types",
|
||||
"newApp.foundResult": "{{count}} Result",
|
||||
"newApp.foundResults": "{{count}} Results",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Begrepen",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Begin met een leeg canvas als je al weet wat je wilt bouwen.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Vanaf nul beginnen",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Heb je een Dify DSL-bestand? Importeer het hier om een gedeelde of geback-upte app te herstellen.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Een DSL-bestand importeren",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importeer een app-pakket of DSL-bestand om een gedeelde app of back-up te herstellen.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "App importeren",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Nieuw bij Dify? Doorloop eerst een begeleide les — je ziet Workflows, Agents en meer in actie.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Of begin met Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Bekijk de templates van Dify en kies er een die past bij wat je wilt bouwen. Een prima manier om te beginnen zonder alles vanaf nul te schrijven.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Nog geen apps om te bekijken",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Je kunt apps in deze workspace bekijken en openen, maar maken of bewerken vereist toestemming. Wissel naar een workspace met toegang of neem contact op met de Workspace Owner of Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio is alleen-lezen voor jou",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Gebruik Create om een nieuwe app toe te voegen — kies een template, begin vanaf nul of importeer een DSL-bestand.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Gebruik “Maken” om een app toe te voegen vanuit een sjabloon, vanaf nul te beginnen of een app-pakket of DSL-bestand te importeren.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Maak een nieuwe app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Tik op een app om de orkestratiepagina te openen — bewerk daar prompts, modellen en logica of beheer de instellingen.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Open en beheer elke app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Toegangspunten bekijken",
|
||||
"agent.acl.delete": "Agent verwijderen",
|
||||
"agent.acl.edit": "Agent-informatie en -configuratie bewerken",
|
||||
"agent.acl.import_export_dsl": "Agent-DSL importeren en exporteren",
|
||||
"agent.acl.import_export_dsl": "Agents importeren en exporteren",
|
||||
"agent.acl.log_manage": "Logs bekijken en beheren",
|
||||
"agent.acl.monitor": "Agent-monitoring bekijken",
|
||||
"agent.acl.preview": "Basisinformatie van agent bekijken",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "App verwijderen",
|
||||
"app.acl.deploy": "App implementeren",
|
||||
"app.acl.edit": "App-informatie bewerken en app orkestreren",
|
||||
"app.acl.import_export_dsl": "App-DSL importeren en exporteren",
|
||||
"app.acl.import_export_dsl": "Apps importeren en exporteren",
|
||||
"app.acl.log_and_annotation": "Logs en annotaties bekijken en beheren",
|
||||
"app.acl.monitor": "App-monitoring bekijken",
|
||||
"app.acl.preview": "App voorbeeld bekijken",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Nie są potrzebne żadne parametry",
|
||||
"appSelector.params": "PARAMETRY APLIKACJI",
|
||||
"appSelector.placeholder": "Wybierz aplikację...",
|
||||
"chooseAppFile": "Wybierz pakiet aplikacji lub plik DSL",
|
||||
"deleteAppConfirmContent": "Usunięcie aplikacji jest nieodwracalne. Użytkownicy nie będą mieli już dostępu do twojej aplikacji, a wszystkie konfiguracje monitów i dzienniki zostaną trwale usunięte.",
|
||||
"deleteAppConfirmInputLabel": "Aby potwierdzić, wpisz <appName>{{appName}}</appName> w polu poniżej:",
|
||||
"deleteAppConfirmInputPlaceholder": "Wpisz nazwę aplikacji…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Edytuj informacje o aplikacji",
|
||||
"editDone": "Informacje o aplikacji zaktualizowane",
|
||||
"editFailed": "Nie udało się zaktualizować informacji o aplikacji",
|
||||
"export": "Eksportuj DSL",
|
||||
"exportApp": "Eksportuj aplikację",
|
||||
"exportAppFailed": "Nie udało się wyeksportować aplikacji",
|
||||
"exportAppSuccess": "Pobrano pakiet aplikacji (.ifpkg)",
|
||||
"exportFailed": "Eksport DSL nie powiódł się.",
|
||||
"exportSecret.description": "Po zaznaczeniu tajne wartości zostaną uwzględnione w eksportowanym pakiecie aplikacji.",
|
||||
"exportSecret.title": "Eksportuj aplikację z tajnymi wartościami",
|
||||
"filterEmpty.noApps": "Brak aplikacji tutaj",
|
||||
"firstEmpty.blankDescription": "Zacznij od pustego obszaru, gdy wiesz, co chcesz zbudować.",
|
||||
"firstEmpty.importDescription": "Przywróć aplikację z pliku definicji Dify DSL.",
|
||||
"firstEmpty.importDescription": "Zaimportuj pakiet aplikacji lub plik DSL, aby przywrócić udostępnioną aplikację lub jej kopię zapasową.",
|
||||
"firstEmpty.learnDifyTitle": "Poznaj Dify",
|
||||
"firstEmpty.or": "Lub",
|
||||
"firstEmpty.templateDescription": "Zacznij od gotowego workflow. Dobre do nauki Dify i typowych wzorców.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Pakiet aplikacji lub plik DSL",
|
||||
"importAppFormats": "Obsługuje .ifpkg, .yaml i .yml",
|
||||
"importAppUrlPlaceholder": "Wklej tutaj link do pakietu aplikacji (.ifpkg) lub pliku DSL",
|
||||
"importDSL": "Importuj plik DSL",
|
||||
"importFromDSL": "Importowanie z DSL",
|
||||
"importFromDSLFile": "Z pliku DSL",
|
||||
"importFromDSLUrl": "Z adresu URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Generator tekstu",
|
||||
"newApp.completionShortDescription": "Asystent AI do zadań generowania tekstu",
|
||||
"newApp.completionUserDescription": "Szybko zbuduj asystenta AI do zadań generowania tekstu za pomocą prostej konfiguracji.",
|
||||
"newApp.dropDSLToCreateApp": "Upuść plik DSL tutaj, aby utworzyć aplikację",
|
||||
"newApp.dropDSLToCreateApp": "Upuść tutaj pakiet aplikacji lub plik DSL, aby utworzyć aplikację",
|
||||
"newApp.forBeginners": "Prostsze typy aplikacji",
|
||||
"newApp.foundResult": "{{count}} Wynik",
|
||||
"newApp.foundResults": "{{count}} Wyniki",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Rozumiem",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Zacznij od pustego płótna, gdy już wiesz, co chcesz zbudować.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Utwórz od zera",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Masz plik DSL Dify? Zaimportuj go tutaj, aby przywrócić udostępnioną lub zarchiwizowaną aplikację.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Zaimportuj plik DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Zaimportuj pakiet aplikacji lub plik DSL, aby przywrócić udostępnioną aplikację lub jej kopię zapasową.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importuj aplikację",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Nowość w Dify? Przejdź najpierw lekcję z przewodnikiem — zobaczysz Workflows, Agents i inne funkcje w działaniu.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Albo zacznij od Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Przeglądaj szablony Dify i wybierz ten, który pasuje do tego, co chcesz zbudować. Świetny sposób, aby zacząć bez pisania wszystkiego od zera.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Brak aplikacji do wyswietlenia",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Mozesz przegladac i otwierac aplikacje w tym workspace, ale tworzenie lub edycja wymaga uprawnien. Przejdz do workspace z dostepem albo skontaktuj sie z Workspace Owner lub Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio jest dla Ciebie tylko do odczytu",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Użyj przycisku Utwórz, aby dodać nową aplikację — wybierz szablon, zacznij od zera lub zaimportuj plik DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Użyj „Utwórz”, aby dodać aplikację z szablonu, zacząć od zera lub zaimportować pakiet aplikacji albo plik DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Utwórz nową aplikację",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Kliknij dowolną aplikację, aby otworzyć jej stronę orkiestracji — stamtąd edytujesz prompty, modele i logikę oraz zarządzasz jej ustawieniami.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Otwieraj każdą aplikację i nią zarządzaj",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Wyświetlaj punkty dostępu",
|
||||
"agent.acl.delete": "Usuń agenta",
|
||||
"agent.acl.edit": "Edytuj informacje i konfigurację agenta",
|
||||
"agent.acl.import_export_dsl": "Importuj i eksportuj DSL agenta",
|
||||
"agent.acl.import_export_dsl": "Importowanie i eksportowanie agentów",
|
||||
"agent.acl.log_manage": "Wyświetlaj logi i zarządzaj nimi",
|
||||
"agent.acl.monitor": "Wyświetlaj monitoring agenta",
|
||||
"agent.acl.preview": "Wyświetlaj podstawowe informacje o agencie",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Usuń aplikację",
|
||||
"app.acl.deploy": "Wdróż aplikację",
|
||||
"app.acl.edit": "Edytuj informacje o aplikacji i orkiestruj aplikację",
|
||||
"app.acl.import_export_dsl": "Importuj i eksportuj DSL aplikacji",
|
||||
"app.acl.import_export_dsl": "Importowanie i eksportowanie aplikacji",
|
||||
"app.acl.log_and_annotation": "Wyświetlaj logi i adnotacje oraz zarządzaj nimi",
|
||||
"app.acl.monitor": "Wyświetlaj monitoring aplikacji",
|
||||
"app.acl.preview": "Podgląd aplikacji",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Não são necessários parâmetros",
|
||||
"appSelector.params": "PARÂMETROS DO APLICATIVO",
|
||||
"appSelector.placeholder": "Selecione um aplicativo...",
|
||||
"chooseAppFile": "Escolha um pacote de aplicativo ou arquivo DSL",
|
||||
"deleteAppConfirmContent": "A exclusão do aplicativo é irreversível. Os usuários não poderão mais acessar seu aplicativo e todas as configurações de prompt e logs serão permanentemente excluídas.",
|
||||
"deleteAppConfirmInputLabel": "Para confirmar, digite <appName>{{appName}}</appName> na caixa abaixo:",
|
||||
"deleteAppConfirmInputPlaceholder": "Digite o nome do aplicativo…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Editar Informações do Aplicativo",
|
||||
"editDone": "Informações do aplicativo atualizadas",
|
||||
"editFailed": "Falha ao atualizar informações do aplicativo",
|
||||
"export": "Exportar DSL",
|
||||
"exportApp": "Exportar aplicativo",
|
||||
"exportAppFailed": "Falha ao exportar aplicativo",
|
||||
"exportAppSuccess": "Pacote de aplicativo (.ifpkg) baixado",
|
||||
"exportFailed": "Falha ao exportar DSL.",
|
||||
"exportSecret.description": "Se selecionado, os valores secretos serão incluídos no pacote de aplicativo exportado.",
|
||||
"exportSecret.title": "Exportar aplicativo com valores secretos",
|
||||
"filterEmpty.noApps": "Nenhum app aqui",
|
||||
"firstEmpty.blankDescription": "Comece com uma tela em branco quando souber o que deseja criar.",
|
||||
"firstEmpty.importDescription": "Restaure um app a partir de um arquivo de definição DSL do Dify.",
|
||||
"firstEmpty.importDescription": "Importe um pacote de aplicativo ou arquivo DSL para restaurar um aplicativo compartilhado ou salvo em backup.",
|
||||
"firstEmpty.learnDifyTitle": "Aprenda Dify",
|
||||
"firstEmpty.or": "Ou",
|
||||
"firstEmpty.templateDescription": "Comece com um workflow pré-criado. Ótimo para aprender Dify e padrões comuns.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Pacote de aplicativo ou arquivo DSL",
|
||||
"importAppFormats": "Suporta .ifpkg, .yaml e .yml",
|
||||
"importAppUrlPlaceholder": "Cole aqui um link para um pacote de aplicativo (.ifpkg) ou arquivo DSL",
|
||||
"importDSL": "Importar arquivo DSL",
|
||||
"importFromDSL": "Importar de DSL",
|
||||
"importFromDSLFile": "Do arquivo DSL",
|
||||
"importFromDSLUrl": "Do URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Gerador de Texto",
|
||||
"newApp.completionShortDescription": "Assistente de IA para tarefas de geração de texto",
|
||||
"newApp.completionUserDescription": "Crie rapidamente um assistente de IA para tarefas de geração de texto com configuração simples.",
|
||||
"newApp.dropDSLToCreateApp": "Cole o arquivo DSL aqui para criar o aplicativo",
|
||||
"newApp.dropDSLToCreateApp": "Solte um pacote de aplicativo ou arquivo DSL aqui para criar um aplicativo",
|
||||
"newApp.forBeginners": "Tipos de aplicativos mais básicos",
|
||||
"newApp.foundResult": "{{count}} Resultado",
|
||||
"newApp.foundResults": "{{count}} Resultados",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Entendi",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Comece com uma tela em branco quando já souber o que construir.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Criar do zero",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Tem um arquivo DSL do Dify? Importe-o aqui para restaurar um app que você compartilhou ou fez backup.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importar um arquivo DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importe um pacote de aplicativo ou arquivo DSL para restaurar um aplicativo compartilhado ou salvo em backup.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importar App",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Novo no Dify? Faça primeiro uma lição guiada — você verá Workflows, Agents e muito mais em ação.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Ou comece com o Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Navegue pelos templates do Dify e escolha um que corresponda ao que você quer construir. Uma ótima forma de começar sem escrever nada do zero.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Ainda nao ha apps para ver",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Voce pode navegar e abrir apps neste workspace, mas criar ou editar apps requer permissao. Troque para um workspace com acesso ou contate o Workspace Owner ou Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio esta somente para visualizacao para voce",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Use Criar para adicionar um novo app — escolha um template, comece do zero ou importe um arquivo DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Use “Criar” para adicionar um aplicativo a partir de um modelo, começar do zero ou importar um pacote de aplicativo ou arquivo DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Criar um novo app",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Toque em qualquer app para abrir sua página de orquestração — edite prompts, modelos e lógica, ou gerencie suas configurações a partir dela.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Abra e gerencie cada app",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Visualizar pontos de acesso",
|
||||
"agent.acl.delete": "Excluir agente",
|
||||
"agent.acl.edit": "Editar informações e configuração do agente",
|
||||
"agent.acl.import_export_dsl": "Importar e exportar o DSL do agente",
|
||||
"agent.acl.import_export_dsl": "Importar e exportar agentes",
|
||||
"agent.acl.log_manage": "Visualizar e gerenciar logs",
|
||||
"agent.acl.monitor": "Visualizar o monitoramento do agente",
|
||||
"agent.acl.preview": "Visualizar informações básicas do agente",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Excluir aplicativo",
|
||||
"app.acl.deploy": "Implantar aplicativo",
|
||||
"app.acl.edit": "Editar informações e orquestrar o aplicativo",
|
||||
"app.acl.import_export_dsl": "Importar e exportar o DSL do aplicativo",
|
||||
"app.acl.import_export_dsl": "Importar e exportar aplicativos",
|
||||
"app.acl.log_and_annotation": "Visualizar e gerenciar logs e anotações",
|
||||
"app.acl.monitor": "Visualizar o monitoramento do aplicativo",
|
||||
"app.acl.preview": "Visualizar aplicativo",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Nu sunt necesari parametri",
|
||||
"appSelector.params": "PARAMETRII APLICAȚIEI",
|
||||
"appSelector.placeholder": "Selectați o aplicație...",
|
||||
"chooseAppFile": "Alege un pachet de aplicație sau un fișier DSL",
|
||||
"deleteAppConfirmContent": "Ștergerea aplicației este ireversibilă. Utilizatorii nu vor mai putea accesa aplicația ta, iar toate configurațiile promptului și jurnalele vor fi șterse permanent.",
|
||||
"deleteAppConfirmInputLabel": "Pentru confirmare, tastați <appName>{{appName}}</appName> în caseta de mai jos:",
|
||||
"deleteAppConfirmInputPlaceholder": "Introduceți numele aplicației…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Editează Info Aplicație",
|
||||
"editDone": "Informațiile despre aplicație au fost actualizate",
|
||||
"editFailed": "Actualizarea informațiilor despre aplicație a eșuat",
|
||||
"export": "Exportă DSL",
|
||||
"exportApp": "Exportă aplicația",
|
||||
"exportAppFailed": "Exportul aplicației a eșuat",
|
||||
"exportAppSuccess": "Pachetul aplicației (.ifpkg) a fost descărcat",
|
||||
"exportFailed": "Exportul DSL a eșuat.",
|
||||
"exportSecret.description": "Dacă este selectat, valorile secrete vor fi incluse în pachetul de aplicație exportat.",
|
||||
"exportSecret.title": "Exportă aplicația cu valori secrete",
|
||||
"filterEmpty.noApps": "Nu există aplicații aici",
|
||||
"firstEmpty.blankDescription": "Începe cu un canvas gol când știi ce vrei să construiești.",
|
||||
"firstEmpty.importDescription": "Restaurează o aplicație dintr-un fișier de definiție Dify DSL.",
|
||||
"firstEmpty.importDescription": "Importă un pachet de aplicație sau un fișier DSL pentru a restaura o aplicație partajată sau salvată într-o copie de rezervă.",
|
||||
"firstEmpty.learnDifyTitle": "Învață Dify",
|
||||
"firstEmpty.or": "Sau",
|
||||
"firstEmpty.templateDescription": "Începe cu un workflow preconfigurat. Bun pentru a învăța Dify și tiparele comune.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Pachet de aplicație sau fișier DSL",
|
||||
"importAppFormats": "Acceptă .ifpkg, .yaml și .yml",
|
||||
"importAppUrlPlaceholder": "Lipește aici un link către un pachet de aplicație (.ifpkg) sau un fișier DSL",
|
||||
"importDSL": "Importă fișier DSL",
|
||||
"importFromDSL": "Import din DSL",
|
||||
"importFromDSLFile": "Din fișierul DSL",
|
||||
"importFromDSLUrl": "De la URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Generator de text",
|
||||
"newApp.completionShortDescription": "Asistent AI pentru sarcini de generare de text",
|
||||
"newApp.completionUserDescription": "Construiește rapid un asistent AI pentru sarcinile de generare a textului cu o configurare simplă.",
|
||||
"newApp.dropDSLToCreateApp": "Trageți fișierul DSL aici pentru a crea aplicația",
|
||||
"newApp.dropDSLToCreateApp": "Plasează aici un pachet de aplicație sau un fișier DSL pentru a crea o aplicație",
|
||||
"newApp.forBeginners": "Tipuri de aplicații mai simple",
|
||||
"newApp.foundResult": "{{count}} Rezultat",
|
||||
"newApp.foundResults": "{{count}} Rezultatele",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Am înțeles",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Începe de la o pânză goală atunci când știi deja ce vrei să construiești.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Creează de la zero",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Ai un fișier Dify DSL? Importă-l aici pentru a restaura o aplicație pe care ai partajat-o sau ai salvat-o.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importă un fișier DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Importă un pachet de aplicație sau un fișier DSL pentru a restaura o aplicație partajată sau salvată într-o copie de rezervă.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Importați aplicația",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Ești nou în Dify? Parcurge mai întâi o lecție ghidată — vei vedea Workflows, Agents și altele în acțiune.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Sau începe cu Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Răsfoiește șabloanele Dify și alege unul care se potrivește cu ceea ce vrei să construiești. O modalitate excelentă de a începe fără a scrie ceva de la zero.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Nu exista inca aplicatii de vazut",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Poti rasfoi si deschide aplicatii in acest workspace, dar crearea sau editarea necesita permisiune. Treci la un workspace unde ai acces sau contacteaza Workspace Owner sau Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio este doar pentru vizualizare pentru tine",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Folosește Creează pentru a adăuga o nouă aplicație — alege dintr-un șablon, începe de la zero sau importă un fișier DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Folosește „Creează” pentru a adăuga o aplicație dintr-un șablon, a începe de la zero sau a importa un pachet de aplicație ori un fișier DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Creează o nouă aplicație",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Apasă pe orice aplicație pentru a deschide pagina de orchestrare — editează prompturi, modele și logică sau gestionează setările de acolo.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Deschide și gestionează fiecare aplicație",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Vizualizează punctele de acces",
|
||||
"agent.acl.delete": "Șterge agentul",
|
||||
"agent.acl.edit": "Editează informațiile și configurația agentului",
|
||||
"agent.acl.import_export_dsl": "Importă și exportă DSL-ul agentului",
|
||||
"agent.acl.import_export_dsl": "Importă și exportă agenți",
|
||||
"agent.acl.log_manage": "Vizualizează și gestionează jurnalele",
|
||||
"agent.acl.monitor": "Vizualizează monitorizarea agentului",
|
||||
"agent.acl.preview": "Vizualizează informațiile de bază ale agentului",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Șterge aplicația",
|
||||
"app.acl.deploy": "Implementează aplicația",
|
||||
"app.acl.edit": "Editează informațiile aplicației și orchestrează aplicația",
|
||||
"app.acl.import_export_dsl": "Importă și exportă DSL-ul aplicației",
|
||||
"app.acl.import_export_dsl": "Importă și exportă aplicații",
|
||||
"app.acl.log_and_annotation": "Vizualizează și gestionează jurnalele și adnotările",
|
||||
"app.acl.monitor": "Vizualizează monitorizarea aplicației",
|
||||
"app.acl.preview": "Previzualizează aplicația",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"appSelector.noParams": "Параметры не нужны",
|
||||
"appSelector.params": "ПАРАМЕТРЫ ПРИЛОЖЕНИЯ",
|
||||
"appSelector.placeholder": "Выберите приложение...",
|
||||
"chooseAppFile": "Выберите пакет приложения или файл DSL",
|
||||
"deleteAppConfirmContent": "Удаление приложения необратимо. Пользователи больше не смогут получить доступ к вашему приложению, и все настройки подсказок и журналы будут безвозвратно удалены.",
|
||||
"deleteAppConfirmInputLabel": "Для подтверждения введите <appName>{{appName}}</appName> в поле ниже:",
|
||||
"deleteAppConfirmInputPlaceholder": "Введите название приложения…",
|
||||
@@ -42,14 +43,15 @@
|
||||
"editAppTitle": "Редактировать информацию о приложении",
|
||||
"editDone": "Информация о приложении обновлена",
|
||||
"editFailed": "Не удалось обновить информацию о приложении",
|
||||
"export": "Экспортировать DSL",
|
||||
"exportApp": "Экспортировать приложение",
|
||||
"exportAppFailed": "Не удалось экспортировать приложение",
|
||||
"exportAppSuccess": "Пакет приложения (.ifpkg) скачан",
|
||||
"exportFailed": "Ошибка экспорта DSL.",
|
||||
"exportSecret.description": "Если выбрано, секретные значения будут включены в экспортируемый пакет приложения.",
|
||||
"exportSecret.title": "Экспортировать приложение с секретными значениями",
|
||||
"filterEmpty.noApps": "Здесь нет приложений",
|
||||
"firstEmpty.blankDescription": "Начните с пустого холста, если уже знаете, что хотите создать.",
|
||||
"firstEmpty.importDescription": "Восстановите приложение из файла определения Dify DSL.",
|
||||
"firstEmpty.importDescription": "Импортируйте пакет приложения или файл DSL, чтобы восстановить приложение, которым поделились или для которого создана резервная копия.",
|
||||
"firstEmpty.learnDifyTitle": "Изучить Dify",
|
||||
"firstEmpty.or": "Или",
|
||||
"firstEmpty.templateDescription": "Начните с готового workflow. Подходит для изучения Dify и распространенных шаблонов.",
|
||||
@@ -122,7 +124,6 @@
|
||||
"importAppDescription": "Пакет приложения или файл DSL",
|
||||
"importAppFormats": "Поддерживаются .ifpkg, .yaml и .yml",
|
||||
"importAppUrlPlaceholder": "Вставьте сюда ссылку на пакет приложения (.ifpkg) или файл DSL",
|
||||
"importDSL": "Импортировать файл DSL",
|
||||
"importFromDSL": "Импортировать из DSL",
|
||||
"importFromDSLFile": "Из файла DSL",
|
||||
"importFromDSLUrl": "Из URL",
|
||||
@@ -175,7 +176,7 @@
|
||||
"newApp.completeApp": "Генератор текста",
|
||||
"newApp.completionShortDescription": "AI-помощник для задач генерации текста",
|
||||
"newApp.completionUserDescription": "Быстро создайте помощника с искусственным интеллектом для задач генерации текста с простой настройкой.",
|
||||
"newApp.dropDSLToCreateApp": "Перетащите файл DSL сюда, чтобы создать приложение",
|
||||
"newApp.dropDSLToCreateApp": "Перетащите сюда пакет приложения или файл DSL, чтобы создать приложение",
|
||||
"newApp.forBeginners": "Более простые типы приложений",
|
||||
"newApp.foundResult": "{{count}} Результат",
|
||||
"newApp.foundResults": "{{count}} Результаты",
|
||||
|
||||
@@ -610,8 +610,8 @@
|
||||
"stepByStepTour.guides.primaryActionLabel": "Понятно",
|
||||
"stepByStepTour.guides.studio.empty.blank.description": "Начните с пустого холста, если вы уже знаете, что хотите создать.",
|
||||
"stepByStepTour.guides.studio.empty.blank.title": "Создать с нуля",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Есть файл Dify DSL? Импортируйте его здесь, чтобы восстановить приложение, которым вы поделились или которое сохранили.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Импортировать файл DSL",
|
||||
"stepByStepTour.guides.studio.empty.dsl.description": "Импортируйте пакет приложения или файл DSL, чтобы восстановить приложение, которым поделились или для которого создана резервная копия.",
|
||||
"stepByStepTour.guides.studio.empty.dsl.title": "Импортировать приложение",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.description": "Впервые в Dify? Сначала пройдите пошаговый урок — вы увидите Workflows, Agents и многое другое в действии.",
|
||||
"stepByStepTour.guides.studio.empty.learnDify.title": "Или начните с Learn Dify",
|
||||
"stepByStepTour.guides.studio.empty.template.description": "Просмотрите шаблоны Dify и выберите тот, который соответствует тому, что вы хотите создать. Отличный способ начать, не создавая ничего с нуля.",
|
||||
@@ -620,7 +620,7 @@
|
||||
"stepByStepTour.guides.studio.noCreate.empty.title": "Пока нет приложений для просмотра",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.description": "Вы можете просматривать и открывать приложения в этом рабочем пространстве, но создание или редактирование требует разрешения. Переключитесь на рабочее пространство с доступом или обратитесь к Workspace Owner или Admin.",
|
||||
"stepByStepTour.guides.studio.noCreate.withApps.title": "Studio доступна вам только для просмотра",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Используйте «Создать», чтобы добавить новое приложение — выберите шаблон, начните с нуля или импортируйте файл DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.description": "Нажмите «Создать», чтобы добавить приложение из шаблона, начать с нуля или импортировать пакет приложения или файл DSL.",
|
||||
"stepByStepTour.guides.studio.withApps.create.title": "Создать новое приложение",
|
||||
"stepByStepTour.guides.studio.withApps.manage.description": "Нажмите на любое приложение, чтобы открыть страницу его оркестрации — редактируйте промпты, модели и логику или управляйте его настройками отсюда.",
|
||||
"stepByStepTour.guides.studio.withApps.manage.title": "Открытие и управление каждым приложением",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"agent.acl.access_point_view": "Просмотр точек доступа",
|
||||
"agent.acl.delete": "Удаление агента",
|
||||
"agent.acl.edit": "Редактирование информации и конфигурации агента",
|
||||
"agent.acl.import_export_dsl": "Импорт и экспорт DSL агента",
|
||||
"agent.acl.import_export_dsl": "Импорт и экспорт агентов",
|
||||
"agent.acl.log_manage": "Просмотр и управление журналами",
|
||||
"agent.acl.monitor": "Просмотр мониторинга агента",
|
||||
"agent.acl.preview": "Просмотр основной информации об агенте",
|
||||
@@ -19,7 +19,7 @@
|
||||
"app.acl.delete": "Удаление приложения",
|
||||
"app.acl.deploy": "Развертывание приложения",
|
||||
"app.acl.edit": "Редактирование информации о приложении и оркестрация приложения",
|
||||
"app.acl.import_export_dsl": "Импорт и экспорт DSL приложения",
|
||||
"app.acl.import_export_dsl": "Импорт и экспорт приложений",
|
||||
"app.acl.log_and_annotation": "Просмотр и управление журналами и аннотациями",
|
||||
"app.acl.monitor": "Просмотр мониторинга приложения",
|
||||
"app.acl.preview": "Предпросмотр приложения",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user