mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
# Conflicts: # api/.env.example # api/uv.lock # web/app/components/app/create-app-modal/index.tsx # web/app/components/app/create-from-dsl-modal/index.tsx # web/app/components/apps/app-card.tsx # web/pnpm-lock.yaml
This commit is contained in:
@ -33,6 +33,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import AppIcon from '../base/app-icon'
|
||||
import AppOperations from './app-operations'
|
||||
|
||||
@ -161,13 +162,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
||||
appID: appDetail.id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${appDetail.name}.yml`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${appDetail.name}.yml` })
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
|
||||
@ -11,6 +11,7 @@ import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/kn
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import ActionButton from '../../base/action-button'
|
||||
import Confirm from '../../base/confirm'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||
@ -64,13 +65,8 @@ const DropDown = ({
|
||||
pipelineId: pipeline_id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${name}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${name}.pipeline` })
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
|
||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../base/button'
|
||||
import Tooltip from '../base/tooltip'
|
||||
import { getKeyboardKeyNameBySystem } from '../workflow/utils'
|
||||
import ShortcutsName from '../workflow/shortcuts-name'
|
||||
|
||||
type TooltipContentProps = {
|
||||
expand: boolean
|
||||
@ -20,18 +20,7 @@ const TooltipContent = ({
|
||||
return (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{
|
||||
TOGGLE_SHORTCUT.map(key => (
|
||||
<span
|
||||
key={key}
|
||||
className="system-kbd inline-flex items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 text-text-tertiary"
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<ShortcutsName keys={TOGGLE_SHORTCUT} textColor="secondary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import Button from '../../../base/button'
|
||||
import AddAnnotationModal from '../add-annotation-modal'
|
||||
import BatchAddModal from '../batch-add-annotation-modal'
|
||||
@ -56,28 +57,23 @@ const HeaderOptions: FC<Props> = ({
|
||||
)
|
||||
|
||||
const JSONLOutput = () => {
|
||||
const a = document.createElement('a')
|
||||
const content = listTransformer(list).join('\n')
|
||||
const file = new Blob([content], { type: 'application/jsonl' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `annotations-${locale}.jsonl`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `annotations-${locale}.jsonl` })
|
||||
}
|
||||
|
||||
const fetchList = async () => {
|
||||
const fetchList = React.useCallback(async () => {
|
||||
const { data }: any = await fetchExportAnnotationList(appId)
|
||||
setList(data as AnnotationItemBasic[])
|
||||
}
|
||||
}, [appId])
|
||||
|
||||
useEffect(() => {
|
||||
fetchList()
|
||||
}, [])
|
||||
}, [fetchList])
|
||||
useEffect(() => {
|
||||
if (controlUpdateList)
|
||||
fetchList()
|
||||
}, [controlUpdateList])
|
||||
}, [controlUpdateList, fetchList])
|
||||
|
||||
const [showBulkImportModal, setShowBulkImportModal] = useState(false)
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
||||
|
||||
@ -57,7 +57,8 @@ import Divider from '../../base/divider'
|
||||
import Loading from '../../base/loading'
|
||||
import Toast from '../../base/toast'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
@ -410,13 +411,7 @@ const AppPublisher = ({
|
||||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
|
||||
import type { IConfigVarProps } from './index'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { vi } from 'vitest'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@ -240,7 +240,9 @@ describe('ConfigVar', () => {
|
||||
const saveButton = await screen.findByRole('button', { name: 'common.operation.save' })
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => {
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error when variable key is duplicated', async () => {
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
|
||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import type { RuntimeMode } from '@/types/app'
|
||||
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiCheckLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiCheckLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@ -32,6 +31,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
|
||||
type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
@ -342,10 +342,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<Button onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
<Button disabled={isAppsFull || !name} className="gap-1" variant="primary" onClick={handleCreateApp}>
|
||||
<span>{t('newApp.Create', { ns: 'app' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from '@/service/apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import DSLConfirmModal from './dsl-confirm-modal'
|
||||
import Uploader from './uploader'
|
||||
|
||||
@ -327,8 +328,10 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||
disabled={buttonDisabled}
|
||||
variant="primary"
|
||||
onClick={handleCreateApp}
|
||||
className="gap-1"
|
||||
>
|
||||
{t('newApp.import', { ns: 'app' })}
|
||||
<span>{t('newApp.import', { ns: 'app' })}</span>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,6 +35,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { formatTime } from '@/utils/time'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
@ -172,13 +173,8 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
appID: app.id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${app.name}.${isDownLoadBundle ? 'zip' : 'yaml'}`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${app.name}.yml` })
|
||||
}
|
||||
catch {
|
||||
notify({
|
||||
@ -363,7 +359,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
dateFormat: `${t('segment.dateTimeFormat', { ns: 'datasetDocuments' })}`,
|
||||
})
|
||||
return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${timeText}`
|
||||
}, [app.updated_at, app.created_at])
|
||||
}, [app.updated_at, app.created_at, t])
|
||||
|
||||
const onlineUserAvatars = useMemo(() => {
|
||||
if (!onlineUsers.length)
|
||||
|
||||
@ -105,6 +105,7 @@ const Apps = () => {
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp
|
||||
appId={currentTryAppParams?.appId || ''}
|
||||
app={currentTryAppParams?.app}
|
||||
category={currentTryAppParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
|
||||
@ -15,11 +15,11 @@ import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import FileImageRender from '../file-image-render'
|
||||
import FileTypeIcon from '../file-type-icon'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
getFileAppearanceType,
|
||||
getFileExtension,
|
||||
@ -140,7 +140,7 @@ const FileInAttachmentItem = ({
|
||||
showDownloadAction && (
|
||||
<ActionButton onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
downloadFile(url || base64Url || '', name)
|
||||
downloadUrl({ url: url || base64Url || '', fileName: name, target: '_blank' })
|
||||
}}
|
||||
>
|
||||
<RiDownloadLine className="h-4 w-4" />
|
||||
|
||||
@ -8,9 +8,9 @@ import Button from '@/app/components/base/button'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import FileImageRender from '../file-image-render'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
} from '../utils'
|
||||
|
||||
@ -85,7 +85,7 @@ const FileImageItem = ({
|
||||
className="absolute bottom-0.5 right-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
downloadFile(download_url || '', name)
|
||||
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
|
||||
}}
|
||||
>
|
||||
<RiDownloadLine className="h-4 w-4 text-text-tertiary" />
|
||||
|
||||
@ -12,10 +12,10 @@ import VideoPreview from '@/app/components/base/file-uploader/video-preview'
|
||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import FileTypeIcon from '../file-type-icon'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
getFileAppearanceType,
|
||||
getFileExtension,
|
||||
@ -100,7 +100,7 @@ const FileItem = ({
|
||||
className="absolute -right-1 -top-1 hidden group-hover/file-item:flex"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
downloadFile(download_url || '', name)
|
||||
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
|
||||
}}
|
||||
>
|
||||
<RiDownloadLine className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { MockInstance } from 'vitest'
|
||||
import mime from 'mime'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { upload } from '@/service/base'
|
||||
@ -6,7 +5,6 @@ import { TransferMethod } from '@/types/app'
|
||||
import { FILE_EXTS } from '../prompt-editor/constants'
|
||||
import { FileAppearanceTypeEnum } from './types'
|
||||
import {
|
||||
downloadFile,
|
||||
fileIsUploaded,
|
||||
fileUpload,
|
||||
getFileAppearanceType,
|
||||
@ -782,74 +780,4 @@ describe('file-uploader utils', () => {
|
||||
} as any)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('downloadFile', () => {
|
||||
let mockAnchor: HTMLAnchorElement
|
||||
let createElementMock: MockInstance
|
||||
let appendChildMock: MockInstance
|
||||
let removeChildMock: MockInstance
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock createElement and appendChild
|
||||
mockAnchor = {
|
||||
href: '',
|
||||
download: '',
|
||||
style: { display: '' },
|
||||
target: '',
|
||||
title: '',
|
||||
click: vi.fn(),
|
||||
} as unknown as HTMLAnchorElement
|
||||
|
||||
createElementMock = vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as any)
|
||||
appendChildMock = vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||
return node
|
||||
})
|
||||
removeChildMock = vi.spyOn(document.body, 'removeChild').mockImplementation((node: Node) => {
|
||||
return node
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should create and trigger download with correct attributes', () => {
|
||||
const url = 'https://example.com/test.pdf'
|
||||
const filename = 'test.pdf'
|
||||
|
||||
downloadFile(url, filename)
|
||||
|
||||
// Verify anchor element was created with correct properties
|
||||
expect(createElementMock).toHaveBeenCalledWith('a')
|
||||
expect(mockAnchor.href).toBe(url)
|
||||
expect(mockAnchor.download).toBe(filename)
|
||||
expect(mockAnchor.style.display).toBe('none')
|
||||
expect(mockAnchor.target).toBe('_blank')
|
||||
expect(mockAnchor.title).toBe(filename)
|
||||
|
||||
// Verify DOM operations
|
||||
expect(appendChildMock).toHaveBeenCalledWith(mockAnchor)
|
||||
expect(mockAnchor.click).toHaveBeenCalled()
|
||||
expect(removeChildMock).toHaveBeenCalledWith(mockAnchor)
|
||||
})
|
||||
|
||||
it('should handle empty filename', () => {
|
||||
const url = 'https://example.com/test.pdf'
|
||||
const filename = ''
|
||||
|
||||
downloadFile(url, filename)
|
||||
|
||||
expect(mockAnchor.download).toBe('')
|
||||
expect(mockAnchor.title).toBe('')
|
||||
})
|
||||
|
||||
it('should handle empty url', () => {
|
||||
const url = ''
|
||||
const filename = 'test.pdf'
|
||||
|
||||
downloadFile(url, filename)
|
||||
|
||||
expect(mockAnchor.href).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -249,15 +249,3 @@ export const fileIsUploaded = (file: FileEntity) => {
|
||||
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100)
|
||||
return true
|
||||
}
|
||||
|
||||
export const downloadFile = (url: string, filename: string) => {
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = filename
|
||||
anchor.style.display = 'none'
|
||||
anchor.target = '_blank'
|
||||
anchor.title = filename
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
document.body.removeChild(anchor)
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { createPortal } from 'react-dom'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
|
||||
type ImagePreviewProps = {
|
||||
url: string
|
||||
@ -60,27 +61,14 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
|
||||
const downloadImage = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.target = '_blank'
|
||||
a.download = title
|
||||
a.click()
|
||||
}
|
||||
else if (url.startsWith('data:image')) {
|
||||
// Base64 image
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.target = '_blank'
|
||||
a.download = title
|
||||
a.click()
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `Unable to open image: ${url}`,
|
||||
})
|
||||
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('data:image')) {
|
||||
downloadUrl({ url, fileName: title, target: '_blank' })
|
||||
return
|
||||
}
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `Unable to open image: ${url}`,
|
||||
})
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
@ -135,12 +123,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
catch (err) {
|
||||
console.error('Failed to copy image:', err)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${title}.png`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
downloadUrl({ url, fileName: `${title}.png` })
|
||||
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
@ -215,6 +198,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
tabIndex={-1}
|
||||
>
|
||||
{ }
|
||||
{/* eslint-disable-next-line next/no-img-element */}
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt={title}
|
||||
|
||||
@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
@ -40,11 +41,10 @@ const ShareQRCode = ({ content }: Props) => {
|
||||
}, [isShow])
|
||||
|
||||
const downloadQR = () => {
|
||||
const canvas = document.getElementsByTagName('canvas')[0]
|
||||
const link = document.createElement('a')
|
||||
link.download = 'qrcode.png'
|
||||
link.href = canvas.toDataURL()
|
||||
link.click()
|
||||
const canvas = qrCodeRef.current?.querySelector('canvas')
|
||||
if (!(canvas instanceof HTMLCanvasElement))
|
||||
return
|
||||
downloadUrl({ url: canvas.toDataURL(), fileName: 'qrcode.png' })
|
||||
}
|
||||
|
||||
const handlePanelClick = (event: React.MouseEvent) => {
|
||||
|
||||
@ -179,8 +179,10 @@ describe('RetryButton (IndexFailed)', () => {
|
||||
}, false),
|
||||
)
|
||||
|
||||
// Delay the response to test loading state
|
||||
mockRetryErrorDocs.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ result: 'success' }), 100)))
|
||||
let resolveRetry: ((value: { result: 'success' }) => void) | undefined
|
||||
mockRetryErrorDocs.mockImplementation(() => new Promise((resolve) => {
|
||||
resolveRetry = resolve
|
||||
}))
|
||||
|
||||
render(<RetryButton datasetId="test-dataset" />)
|
||||
|
||||
@ -193,6 +195,11 @@ describe('RetryButton (IndexFailed)', () => {
|
||||
expect(button).toHaveClass('cursor-not-allowed')
|
||||
expect(button).toHaveClass('text-text-disabled')
|
||||
})
|
||||
|
||||
resolveRetry?.({ result: 'success' })
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -23,9 +23,10 @@ vi.mock('@/app/components/base/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock downloadFile utility
|
||||
vi.mock('@/utils/format', () => ({
|
||||
downloadFile: vi.fn(),
|
||||
// Mock download utilities
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: vi.fn(),
|
||||
downloadUrl: vi.fn(),
|
||||
}))
|
||||
|
||||
// Capture Confirm callbacks
|
||||
@ -502,8 +503,8 @@ describe('TemplateCard', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should call downloadFile on successful export', async () => {
|
||||
const { downloadFile } = await import('@/utils/format')
|
||||
it('should call downloadBlob on successful export', async () => {
|
||||
const { downloadBlob } = await import('@/utils/download')
|
||||
mockExportPipelineDSL.mockImplementation((_id, callbacks) => {
|
||||
callbacks.onSuccess({ data: 'yaml_content' })
|
||||
return Promise.resolve()
|
||||
@ -514,7 +515,7 @@ describe('TemplateCard', () => {
|
||||
fireEvent.click(exportButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(downloadFile).toHaveBeenCalledWith(expect.objectContaining({
|
||||
expect(downloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fileName: 'Test Pipeline.pipeline',
|
||||
}))
|
||||
})
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
useInvalidCustomizedTemplateList,
|
||||
usePipelineTemplateById,
|
||||
} from '@/service/use-pipeline'
|
||||
import { downloadFile } from '@/utils/format'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import Actions from './actions'
|
||||
import Content from './content'
|
||||
import Details from './details'
|
||||
@ -108,10 +108,7 @@ const TemplateCard = ({
|
||||
await exportPipelineDSL(pipeline.id, {
|
||||
onSuccess: (res) => {
|
||||
const blob = new Blob([res.data], { type: 'application/yaml' })
|
||||
downloadFile({
|
||||
data: blob,
|
||||
fileName: `${pipeline.name}.pipeline`,
|
||||
})
|
||||
downloadBlob({ data: blob, fileName: `${pipeline.name}.pipeline` })
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('exportDSL.successTip', { ns: 'datasetPipeline' }),
|
||||
|
||||
@ -125,11 +125,25 @@ const WaterCrawl: FC<Props> = ({
|
||||
await sleep(2500)
|
||||
return await waitForCrawlFinished(jobId)
|
||||
}
|
||||
catch (e: any) {
|
||||
const errorBody = await e.json()
|
||||
catch (error: unknown) {
|
||||
let errorMessage = ''
|
||||
|
||||
const maybeErrorWithJson = error as { json?: () => Promise<unknown>, message?: unknown } | null
|
||||
if (maybeErrorWithJson?.json) {
|
||||
try {
|
||||
const errorBody = await maybeErrorWithJson.json() as { message?: unknown } | null
|
||||
if (typeof errorBody?.message === 'string')
|
||||
errorMessage = errorBody.message
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
if (!errorMessage && typeof maybeErrorWithJson?.message === 'string')
|
||||
errorMessage = maybeErrorWithJson.message
|
||||
|
||||
return {
|
||||
isError: true,
|
||||
errorMessage: errorBody.message,
|
||||
errorMessage,
|
||||
data: {
|
||||
data: [],
|
||||
},
|
||||
|
||||
@ -4,7 +4,8 @@ import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { useDocumentContext } from '../../context'
|
||||
|
||||
@ -54,7 +55,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
||||
>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-sm-medium text-components-button-secondary-text">{t('operation.cancel', { ns: 'common' })}</span>
|
||||
<span className="system-kbd rounded-[4px] bg-components-kbd-bg-gray px-[1px] text-text-tertiary">ESC</span>
|
||||
<ShortcutsName keys={['ESC']} textColor="secondary" />
|
||||
</div>
|
||||
</Button>
|
||||
{(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk && showRegenerationButton)
|
||||
@ -76,10 +77,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
||||
>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="text-components-button-primary-text">{t('operation.save', { ns: 'common' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white capitalize text-text-primary-on-surface">{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">S</span>
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', 'S']} bgColor="white" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
|
||||
type ModalState = {
|
||||
showRenameModal: boolean
|
||||
@ -65,13 +66,8 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO
|
||||
pipelineId: pipeline_id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${name}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${name}.pipeline` })
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
|
||||
@ -74,17 +74,15 @@ const AppCard = ({
|
||||
</div>
|
||||
{isExplore && (canCreate || isTrialApp) && (
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('grid h-8 w-full grid-cols-1 space-x-2', isTrialApp && 'grid-cols-2')}>
|
||||
<div className={cn('grid h-8 w-full grid-cols-2 space-x-2')}>
|
||||
<Button variant="primary" className="h-7" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('appCard.addToWorkspace', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
{isTrialApp && (
|
||||
<Button className="h-7" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button className="h-7" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -251,6 +251,7 @@ const Apps = ({
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp
|
||||
appId={appParams?.appId || ''}
|
||||
app={appParams?.app}
|
||||
category={appParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
@ -17,6 +17,7 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
|
||||
export type CreateAppModalProps = {
|
||||
show: boolean
|
||||
@ -198,10 +199,7 @@ const CreateAppModal = ({
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<span>{!isEditModal ? t('operation.create', { ns: 'common' }) : t('operation.save', { ns: 'common' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
<Button className="w-24" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
/* eslint-disable style/multiline-ternary */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { App as AppType } from '@/models/explore'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal/index'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import Button from '../../base/button'
|
||||
import App from './app'
|
||||
@ -15,6 +17,7 @@ import Tab, { TypeEnum } from './tab'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
app?: AppType
|
||||
category?: string
|
||||
onClose: () => void
|
||||
onCreate: () => void
|
||||
@ -22,13 +25,23 @@ type Props = {
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
app,
|
||||
category,
|
||||
onClose,
|
||||
onCreate,
|
||||
}) => {
|
||||
const [type, setType] = useState<TypeEnum>(TypeEnum.TRY)
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app)
|
||||
const [type, setType] = useState<TypeEnum>(() => (app && !isTrialApp ? TypeEnum.DETAIL : TypeEnum.TRY))
|
||||
const { data: appDetail, isLoading } = useGetTryAppInfo(appId)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app && !isTrialApp && type !== TypeEnum.DETAIL)
|
||||
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
setType(TypeEnum.DETAIL)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [app, isTrialApp])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
@ -45,6 +58,7 @@ const TryApp: FC<Props> = ({
|
||||
<Tab
|
||||
value={type}
|
||||
onChange={setType}
|
||||
disableTry={app ? !isTrialApp : false}
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
|
||||
@ -12,15 +12,17 @@ export enum TypeEnum {
|
||||
type Props = {
|
||||
value: TypeEnum
|
||||
onChange: (value: TypeEnum) => void
|
||||
disableTry?: boolean
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
disableTry,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const tabs = [
|
||||
{ id: TypeEnum.TRY, name: t('tryApp.tabHeader.try', { ns: 'explore' }) },
|
||||
{ id: TypeEnum.TRY, name: t('tryApp.tabHeader.try', { ns: 'explore' }), disabled: disableTry },
|
||||
{ id: TypeEnum.DETAIL, name: t('tryApp.tabHeader.detail', { ns: 'explore' }) },
|
||||
]
|
||||
return (
|
||||
|
||||
@ -12,7 +12,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '@/app/components/workflow/utils/common'
|
||||
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
|
||||
@ -356,14 +357,7 @@ const GotoAnything: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-quaternary">
|
||||
<span className="system-kbd rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100">
|
||||
{isMac() ? '⌘' : 'Ctrl'}
|
||||
</span>
|
||||
<span className="system-kbd ml-1 rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100">
|
||||
K
|
||||
</span>
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', 'K']} textColor="secondary" />
|
||||
</div>
|
||||
|
||||
<Command.List className="h-[240px] overflow-y-auto">
|
||||
|
||||
@ -10,6 +10,7 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { getDocDownloadUrl } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import Button from '../../base/button'
|
||||
import Gdpr from '../../base/icons/src/public/common/Gdpr'
|
||||
import Iso from '../../base/icons/src/public/common/Iso'
|
||||
@ -47,9 +48,7 @@ const UpgradeOrDownload: FC<UpgradeOrDownloadProps> = ({ doc_name }) => {
|
||||
mutationFn: async () => {
|
||||
try {
|
||||
const ret = await getDocDownloadUrl(doc_name)
|
||||
const a = document.createElement('a')
|
||||
a.href = ret.url
|
||||
a.click()
|
||||
downloadUrl({ url: ret.url })
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('operation.downloadSuccess', { ns: 'common' }),
|
||||
|
||||
@ -28,11 +28,12 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
useChecklistBeforePublish,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
@ -261,13 +262,7 @@ const Popup = () => {
|
||||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useWorkflowRun, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -78,14 +78,7 @@ const RunMode = ({
|
||||
)}
|
||||
{
|
||||
!isDisabled && (
|
||||
<div className="system-kbd flex items-center gap-x-0.5 text-text-tertiary">
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
<ShortcutsName keys={['alt', 'R']} textColor="secondary" />
|
||||
)
|
||||
}
|
||||
</button>
|
||||
|
||||
@ -11,6 +11,7 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
export const useDSL = () => {
|
||||
@ -37,13 +38,8 @@ export const useDSL = () => {
|
||||
pipelineId,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${knowledgeName}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${knowledgeName}.pipeline` })
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
|
||||
@ -75,9 +76,7 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
||||
{isShow && (
|
||||
<div className="body-xs-regular pointer-events-none fixed left-1/2 top-1/2 z-[70] flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary">
|
||||
<span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span>
|
||||
<kbd className="system-kbd inline-flex h-4 min-w-4 items-center justify-center rounded bg-components-kbd-bg-gray px-1 text-text-tertiary">
|
||||
{t('onboarding.escTip.key', { ns: 'workflow' })}
|
||||
</kbd>
|
||||
<ShortcutsName keys={[t('onboarding.escTip.key', { ns: 'workflow' })]} textColor="secondary" />
|
||||
<span>{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { exportAppConfig } from '@/service/apps'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
export const useDSL = () => {
|
||||
@ -37,13 +38,8 @@ export const useDSL = () => {
|
||||
include,
|
||||
workflowID: workflowId,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${appDetail.name}.yml`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
downloadBlob({ data: file, fileName: `${appDetail.name}.yml` })
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadFile } from '@/utils/format'
|
||||
import { downloadBlob } from '@/utils/download'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type Props = {
|
||||
@ -67,7 +67,7 @@ const OperationDropdown: FC<Props> = ({
|
||||
if (!needDownload || !blob)
|
||||
return
|
||||
const fileName = `${author}-${name}_${version}.zip`
|
||||
downloadFile({ data: blob, fileName })
|
||||
downloadBlob({ data: blob, fileName })
|
||||
setNeedDownload(false)
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
||||
|
||||
@ -7,9 +7,9 @@ import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -143,14 +143,7 @@ const RunMode = ({
|
||||
>
|
||||
<RiPlayLargeLine className="mr-1 size-4" />
|
||||
{text ?? t('common.run', { ns: 'workflow' })}
|
||||
<div className="system-kbd flex items-center gap-x-0.5 text-text-tertiary">
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
<ShortcutsName keys={['alt', 'R']} textColor="secondary" />
|
||||
</div>
|
||||
</TestRunMenu>
|
||||
)
|
||||
|
||||
@ -8,7 +8,8 @@ import useTheme from '@/hooks/use-theme'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../../base/button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../utils'
|
||||
|
||||
type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
@ -23,16 +24,7 @@ const PopupContent = React.memo(() => {
|
||||
<div className="system-xs-medium px-0.5 text-text-secondary">
|
||||
{t('common.versionHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{VERSION_HISTORY_SHORTCUT.map(key => (
|
||||
<span
|
||||
key={key}
|
||||
className="system-kbd rounded-[4px] bg-components-kbd-bg-white px-[1px] text-text-tertiary"
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={VERSION_HISTORY_SHORTCUT} bgColor="gray" textColor="secondary" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@ -41,7 +41,7 @@ const parseCurl = (curlCommand: string): { node: HttpNodeType | null, error: str
|
||||
case '--request':
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing HTTP method after -X or --request.' }
|
||||
node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
|
||||
node.method = (args[++i].replace(/^['"]|['"]$/g, '').toLowerCase() as Method) || Method.get
|
||||
hasData = true
|
||||
break
|
||||
case '-H':
|
||||
|
||||
@ -3,7 +3,8 @@ import { useKeyPress } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
|
||||
type AdvancedActionsProps = {
|
||||
isConfirmDisabled: boolean
|
||||
@ -11,15 +12,6 @@ type AdvancedActionsProps = {
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
const Key = (props: { keyName: string }) => {
|
||||
const { keyName } = props
|
||||
return (
|
||||
<kbd className="system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-white px-px text-text-primary-on-surface">
|
||||
{keyName}
|
||||
</kbd>
|
||||
)
|
||||
}
|
||||
|
||||
const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||
isConfirmDisabled,
|
||||
onCancel,
|
||||
@ -48,10 +40,7 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<span>{t('operation.confirm', { ns: 'common' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<Key keyName={getKeyboardKeyNameBySystem('ctrl')} />
|
||||
<Key keyName="⏎" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '⏎']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadUrl } from '@/utils/download'
|
||||
import { useNodesReadOnly } from '../hooks'
|
||||
import TipPopup from './tip-popup'
|
||||
|
||||
@ -146,26 +147,14 @@ const MoreActions: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = `${filename}.${type}`
|
||||
|
||||
if (currentWorkflow) {
|
||||
setPreviewUrl(dataUrl)
|
||||
setPreviewTitle(`${filename}.${type}`)
|
||||
setPreviewTitle(fileName)
|
||||
}
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = dataUrl
|
||||
link.download = `${filename}.${type}`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
else {
|
||||
// For current view, just download
|
||||
const link = document.createElement('a')
|
||||
link.href = dataUrl
|
||||
link.download = `${filename}.${type}`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
downloadUrl({ url: dataUrl, fileName })
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Export image failed:', error)
|
||||
|
||||
@ -6,11 +6,13 @@ type ShortcutsNameProps = {
|
||||
keys: readonly string[]
|
||||
className?: string
|
||||
textColor?: 'default' | 'secondary'
|
||||
bgColor?: 'gray' | 'white'
|
||||
}
|
||||
const ShortcutsName = ({
|
||||
keys,
|
||||
className,
|
||||
textColor = 'default',
|
||||
bgColor = 'gray',
|
||||
}: ShortcutsNameProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
@ -23,7 +25,9 @@ const ShortcutsName = ({
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
'system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray capitalize',
|
||||
'system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] px-1 capitalize',
|
||||
bgColor === 'gray' && 'bg-components-kbd-bg-gray',
|
||||
bgColor === 'white' && 'bg-components-kbd-bg-white text-text-primary-on-surface',
|
||||
textColor === 'secondary' && 'text-text-tertiary',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -9,8 +9,7 @@ html[data-theme="dark"] .monaco-editor .sticky-line-content:hover {
|
||||
background-color: var(--color-components-sticky-header-bg-hover) !important;
|
||||
}
|
||||
|
||||
/* Fallback: any app sticky header using input-bg variables should use the sticky header bg when sticky */
|
||||
html[data-theme="dark"] .sticky, html[data-theme="dark"] .is-sticky {
|
||||
/* Monaco editor specific sticky scroll styles in dark mode */
|
||||
html[data-theme="dark"] .monaco-editor .sticky-line-root {
|
||||
background-color: var(--color-components-sticky-header-bg) !important;
|
||||
border-bottom: 1px solid var(--color-components-sticky-header-border) !important;
|
||||
}
|
||||
Reference in New Issue
Block a user