mirror of
https://mirror.skon.top/github.com/langgenius/dify.git
synced 2026-04-20 23:40:16 +08:00
chore: export dsl add loading (#35427)
This commit is contained in:
@@ -75,9 +75,9 @@ const defaultProps = {
|
||||
setSecretEnvList: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
onCopy: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
onExport: vi.fn(async () => {}),
|
||||
exportCheck: vi.fn(),
|
||||
handleConfirmExport: vi.fn(),
|
||||
handleConfirmExport: vi.fn(async () => {}),
|
||||
onConfirmDelete: vi.fn(),
|
||||
}
|
||||
|
||||
@@ -238,6 +238,42 @@ describe('AppInfoModals', () => {
|
||||
expect(defaultProps.handleConfirmExport).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable export confirm button and avoid duplicate submits while confirming export', async () => {
|
||||
let resolveConfirmExport: () => void
|
||||
const handleConfirmExport = vi.fn(() => new Promise<void>((resolve) => {
|
||||
resolveConfirmExport = resolve
|
||||
}))
|
||||
const user = userEvent.setup()
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
<AppInfoModals
|
||||
{...defaultProps}
|
||||
activeModal="exportWarning"
|
||||
handleConfirmExport={handleConfirmExport}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
|
||||
const confirmButton = await screen.findByRole('button', { name: 'common.operation.confirm' })
|
||||
|
||||
const firstClick = user.click(confirmButton)
|
||||
await waitFor(() => {
|
||||
expect(confirmButton).toBeDisabled()
|
||||
expect(confirmButton).toHaveTextContent('common.operation.exporting')
|
||||
})
|
||||
await user.click(confirmButton)
|
||||
|
||||
expect(handleConfirmExport).toHaveBeenCalledTimes(1)
|
||||
|
||||
resolveConfirmExport!()
|
||||
await firstClick
|
||||
await waitFor(() => {
|
||||
expect(confirmButton).not.toBeDisabled()
|
||||
expect(confirmButton).toHaveTextContent('common.operation.confirm')
|
||||
})
|
||||
})
|
||||
|
||||
it('should call exportCheck when backup on importDSL modal', async () => {
|
||||
const user = userEvent.setup()
|
||||
await act(async () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import dynamic from '@/next/dynamic'
|
||||
@@ -34,7 +34,7 @@ type AppInfoModalsProps = {
|
||||
onCopy: DuplicateAppModalProps['onConfirm']
|
||||
onExport: (include?: boolean) => Promise<void>
|
||||
exportCheck: () => void
|
||||
handleConfirmExport: () => void
|
||||
handleConfirmExport: () => Promise<void>
|
||||
onConfirmDelete: () => void
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ const AppInfoModals = ({
|
||||
}: AppInfoModalsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
|
||||
const [isConfirmingExport, setIsConfirmingExport] = useState(false)
|
||||
const isDeleteConfirmDisabled = confirmDeleteInput !== appDetail.name
|
||||
|
||||
const handleDeleteDialogClose = () => {
|
||||
@@ -60,6 +61,19 @@ const AppInfoModals = ({
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const handleExportWarningConfirm = useCallback(async () => {
|
||||
if (isConfirmingExport)
|
||||
return
|
||||
|
||||
setIsConfirmingExport(true)
|
||||
try {
|
||||
await handleConfirmExport()
|
||||
}
|
||||
finally {
|
||||
setIsConfirmingExport(false)
|
||||
}
|
||||
}, [handleConfirmExport, isConfirmingExport])
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeModal === 'switch' && (
|
||||
@@ -161,8 +175,15 @@ const AppInfoModals = ({
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton tone="default" onClick={handleConfirmExport}>
|
||||
{t('operation.confirm', { ns: 'common' })}
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={isConfirmingExport}
|
||||
disabled={isConfirmingExport}
|
||||
onClick={handleExportWarningConfirm}
|
||||
>
|
||||
{isConfirmingExport
|
||||
? t('operation.exporting', { ns: 'common' })
|
||||
: t('operation.confirm', { ns: 'common' })}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
}, [appDetail?.id])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,7 +89,7 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
@@ -183,7 +183,6 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
const handleConfirmExport = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
closeModal()
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
|
||||
@@ -196,6 +195,9 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) {
|
||||
catch {
|
||||
toast(t('exportFailed', { ns: 'app' }), { type: 'error' })
|
||||
}
|
||||
finally {
|
||||
closeModal()
|
||||
}
|
||||
}, [appDetail, closeModal, onExport, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import DSLExportConfirmModal from '../dsl-export-confirm-modal'
|
||||
|
||||
@@ -105,6 +105,42 @@ describe('DSLExportConfirmModal', () => {
|
||||
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(() => {
|
||||
expect(confirmButton).toBeDisabled()
|
||||
expect(confirmButton).toHaveTextContent('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
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCloseLine, RiLock2Line } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
@@ -13,7 +13,7 @@ import Modal from '@/app/components/base/modal'
|
||||
|
||||
export type DSLExportConfirmModalProps = {
|
||||
envList: EnvironmentVariable[]
|
||||
onConfirm: (state: boolean) => void
|
||||
onConfirm: (state: boolean) => void | Promise<void>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -25,11 +25,21 @@ const DSLExportConfirmModal = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
const submit = () => {
|
||||
onConfirm(exportSecrets)
|
||||
onClose()
|
||||
}
|
||||
const submit = useCallback(async () => {
|
||||
if (isExporting)
|
||||
return
|
||||
|
||||
setIsExporting(true)
|
||||
try {
|
||||
await onConfirm(exportSecrets)
|
||||
onClose()
|
||||
}
|
||||
finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}, [exportSecrets, isExporting, onClose, onConfirm])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -38,7 +48,10 @@ const DSLExportConfirmModal = ({
|
||||
className={cn('w-[480px] max-w-[480px]')}
|
||||
>
|
||||
<div className="relative pb-6 title-2xl-semi-bold text-text-primary">{t('env.export.title', { ns: 'workflow' })}</div>
|
||||
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onClose}>
|
||||
<div
|
||||
className={cn('absolute top-4 right-4 p-2', !isExporting && 'cursor-pointer')}
|
||||
onClick={() => !isExporting && onClose()}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
@@ -72,13 +85,31 @@ const DSLExportConfirmModal = ({
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={exportSecrets}
|
||||
disabled={isExporting}
|
||||
onCheck={() => setExportSecrets(!exportSecrets)}
|
||||
/>
|
||||
<div className="cursor-pointer system-sm-medium text-text-primary" onClick={() => setExportSecrets(!exportSecrets)}>{t('env.export.checkbox', { ns: 'workflow' })}</div>
|
||||
<div
|
||||
className={cn('system-sm-medium text-text-primary', !isExporting && 'cursor-pointer')}
|
||||
onClick={() => !isExporting && setExportSecrets(!exportSecrets)}
|
||||
>
|
||||
{t('env.export.checkbox', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse pt-6">
|
||||
<Button className="ml-2" variant="primary" onClick={submit}>{exportSecrets ? t('env.export.export', { ns: 'workflow' }) : t('env.export.ignore', { ns: 'workflow' })}</Button>
|
||||
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="primary"
|
||||
loading={isExporting}
|
||||
disabled={isExporting}
|
||||
onClick={submit}
|
||||
>
|
||||
{isExporting
|
||||
? t('operation.exporting', { ns: 'common' })
|
||||
: exportSecrets
|
||||
? t('env.export.export', { ns: 'workflow' })
|
||||
: t('env.export.ignore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button disabled={isExporting} onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "اكتمل التنزيل.",
|
||||
"operation.duplicate": "تكرار",
|
||||
"operation.edit": "تعديل",
|
||||
"operation.exporting": "جارٍ التصدير",
|
||||
"operation.format": "تنسيق",
|
||||
"operation.getForFree": "احصل عليه مجانا",
|
||||
"operation.imageCopied": "تم نسخ الصورة",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Download abgeschlossen.",
|
||||
"operation.duplicate": "Duplikat",
|
||||
"operation.edit": "Bearbeiten",
|
||||
"operation.exporting": "Exportiere",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Kostenlos erhalten",
|
||||
"operation.imageCopied": "Kopiertes Bild",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Download Completed.",
|
||||
"operation.duplicate": "Duplicate",
|
||||
"operation.edit": "Edit",
|
||||
"operation.exporting": "Exporting",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Get for free",
|
||||
"operation.imageCopied": "Image copied",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Descarga completada.",
|
||||
"operation.duplicate": "Duplicar",
|
||||
"operation.edit": "Editar",
|
||||
"operation.exporting": "Exportando",
|
||||
"operation.format": "Formato",
|
||||
"operation.getForFree": "Obtener gratis",
|
||||
"operation.imageCopied": "Imagen copiada",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "دانلود کامل شد.",
|
||||
"operation.duplicate": "تکرار",
|
||||
"operation.edit": "ویرایش",
|
||||
"operation.exporting": "در حال خروجی گرفتن",
|
||||
"operation.format": "قالب",
|
||||
"operation.getForFree": "دریافت رایگان",
|
||||
"operation.imageCopied": "تصویر کپی شده",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Téléchargement terminé.",
|
||||
"operation.duplicate": "Dupliquer",
|
||||
"operation.edit": "Modifier",
|
||||
"operation.exporting": "Exportation",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Obtenez gratuitement",
|
||||
"operation.imageCopied": "Image copied",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "डाउनलोड पूरा हुआ।",
|
||||
"operation.duplicate": "डुप्लिकेट",
|
||||
"operation.edit": "संपादित करें",
|
||||
"operation.exporting": "निर्यात हो रहा है",
|
||||
"operation.format": "फॉर्मेट",
|
||||
"operation.getForFree": "मुफ्त में प्राप्त करें",
|
||||
"operation.imageCopied": "कॉपी की गई छवि",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Unduh Selesai.",
|
||||
"operation.duplicate": "Duplikat",
|
||||
"operation.edit": "Mengedit",
|
||||
"operation.exporting": "Mengekspor",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Dapatkan gratis",
|
||||
"operation.imageCopied": "Gambar yang disalin",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Download completato.",
|
||||
"operation.duplicate": "Duplica",
|
||||
"operation.edit": "Modifica",
|
||||
"operation.exporting": "Esportazione in corso",
|
||||
"operation.format": "Formato",
|
||||
"operation.getForFree": "Ottieni gratuitamente",
|
||||
"operation.imageCopied": "Immagine copiata",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "ダウンロード完了",
|
||||
"operation.duplicate": "複製",
|
||||
"operation.edit": "編集",
|
||||
"operation.exporting": "エクスポート中",
|
||||
"operation.format": "フォーマット",
|
||||
"operation.getForFree": "無料で入手",
|
||||
"operation.imageCopied": "コピーした画像",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "다운로드 완료.",
|
||||
"operation.duplicate": "중복",
|
||||
"operation.edit": "편집",
|
||||
"operation.exporting": "내보내는 중",
|
||||
"operation.format": "형식",
|
||||
"operation.getForFree": "무료로 받기",
|
||||
"operation.imageCopied": "복사된 이미지",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Download Completed.",
|
||||
"operation.duplicate": "Duplicate",
|
||||
"operation.edit": "Edit",
|
||||
"operation.exporting": "Exporteren",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Get for free",
|
||||
"operation.imageCopied": "Image copied",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Pobieranie zakończone.",
|
||||
"operation.duplicate": "Duplikuj",
|
||||
"operation.edit": "Edytuj",
|
||||
"operation.exporting": "Eksportowanie",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Zdobądź za darmo",
|
||||
"operation.imageCopied": "Skopiowany obraz",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Download concluído.",
|
||||
"operation.duplicate": "Duplicada",
|
||||
"operation.edit": "Editar",
|
||||
"operation.exporting": "Exportando",
|
||||
"operation.format": "Formato",
|
||||
"operation.getForFree": "Obter gratuitamente",
|
||||
"operation.imageCopied": "Imagem copiada",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Descărcarea a fost finalizată.",
|
||||
"operation.duplicate": "Duplică",
|
||||
"operation.edit": "Editează",
|
||||
"operation.exporting": "Se exportă",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Obține gratuit",
|
||||
"operation.imageCopied": "Imagine copiată",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Загрузка завершена.",
|
||||
"operation.duplicate": "Дублировать",
|
||||
"operation.edit": "Редактировать",
|
||||
"operation.exporting": "Экспорт",
|
||||
"operation.format": "Формат",
|
||||
"operation.getForFree": "Получить бесплатно",
|
||||
"operation.imageCopied": "Скопированное изображение",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Prenos končan.",
|
||||
"operation.duplicate": "Podvoji",
|
||||
"operation.edit": "Uredi",
|
||||
"operation.exporting": "Izvažanje",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Dobite brezplačno",
|
||||
"operation.imageCopied": "Kopirana slika",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "ดาวน์โหลดเสร็จสิ้นแล้ว.",
|
||||
"operation.duplicate": "สำเนา",
|
||||
"operation.edit": "แก้ไข",
|
||||
"operation.exporting": "กำลังส่งออก",
|
||||
"operation.format": "รูปแบบ",
|
||||
"operation.getForFree": "รับฟรี",
|
||||
"operation.imageCopied": "ภาพที่คัดลอก",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "İndirme Tamamlandı.",
|
||||
"operation.duplicate": "Çoğalt",
|
||||
"operation.edit": "Düzenle",
|
||||
"operation.exporting": "Dışa aktarılıyor",
|
||||
"operation.format": "Format",
|
||||
"operation.getForFree": "Ücretsiz edinin",
|
||||
"operation.imageCopied": "Kopyalanan görüntü",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Завантаження завершено.",
|
||||
"operation.duplicate": "дублікат",
|
||||
"operation.edit": "Редагувати",
|
||||
"operation.exporting": "Експорт",
|
||||
"operation.format": "Формат",
|
||||
"operation.getForFree": "Отримати безкоштовно",
|
||||
"operation.imageCopied": "Скопійоване зображення",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "Tải xuống đã hoàn thành.",
|
||||
"operation.duplicate": "Nhân bản",
|
||||
"operation.edit": "Chỉnh sửa",
|
||||
"operation.exporting": "Đang xuất",
|
||||
"operation.format": "Định dạng",
|
||||
"operation.getForFree": "Nhận miễn phí",
|
||||
"operation.imageCopied": "Hình ảnh sao chép",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "下载完毕",
|
||||
"operation.duplicate": "复制",
|
||||
"operation.edit": "编辑",
|
||||
"operation.exporting": "导出中",
|
||||
"operation.format": "格式化",
|
||||
"operation.getForFree": "免费获取",
|
||||
"operation.imageCopied": "图片已复制",
|
||||
|
||||
@@ -494,6 +494,7 @@
|
||||
"operation.downloadSuccess": "下載完成。",
|
||||
"operation.duplicate": "複製",
|
||||
"operation.edit": "編輯",
|
||||
"operation.exporting": "匯出中",
|
||||
"operation.format": "格式",
|
||||
"operation.getForFree": "免費獲取",
|
||||
"operation.imageCopied": "複製的圖片",
|
||||
|
||||
Reference in New Issue
Block a user