From 808a32c4573e181a12177291b420e2827879f410 Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 2 Feb 2026 15:52:03 +0800 Subject: [PATCH] fix: add pending state to export button to prevent duplicate clicks Use useTransition to disable the export button and show loading state in the DSL export confirm modal during async export operations. --- web/app/components/apps/app-card.tsx | 15 +++++++++------ .../workflow/dsl-export-confirm-modal.tsx | 17 ++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 7415ba6d29..208b8891d5 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -11,7 +11,7 @@ import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLi import dynamic from 'next/dynamic' import { useRouter } from 'next/navigation' import * as React from 'react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState, useTransition } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { AppTypeIcon } from '@/app/components/app/type-selector' @@ -79,6 +79,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showAccessControl, setShowAccessControl] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) + const [exporting, startExport] = useTransition() const onConfirmDelete = useCallback(async () => { try { @@ -186,14 +187,14 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { const exportCheck = async () => { if (app.mode !== AppModeEnum.WORKFLOW && app.mode !== AppModeEnum.ADVANCED_CHAT) { - onExport() + await onExport() return } try { const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`) const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') if (list.length === 0) { - onExport() + await onExport() return } setSecretEnvList(list) @@ -235,11 +236,13 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { e.preventDefault() setShowDuplicateModal(true) } - const onClickExport = async (e: React.MouseEvent) => { + const onClickExport = (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() - exportCheck() + startExport(async () => { + await exportCheck() + }) } const onClickSwitch = async (e: React.MouseEvent) => { e.stopPropagation() @@ -288,7 +291,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { - {(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && ( diff --git a/web/app/components/workflow/dsl-export-confirm-modal.tsx b/web/app/components/workflow/dsl-export-confirm-modal.tsx index e698de722e..31f6ed3432 100644 --- a/web/app/components/workflow/dsl-export-confirm-modal.tsx +++ b/web/app/components/workflow/dsl-export-confirm-modal.tsx @@ -3,7 +3,7 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiCloseLine, RiLock2Line } from '@remixicon/react' import { noop } from 'es-toolkit/function' import * as React from 'react' -import { useState } from 'react' +import { useState, useTransition } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Checkbox from '@/app/components/base/checkbox' @@ -13,7 +13,7 @@ import { cn } from '@/utils/classnames' export type DSLExportConfirmModalProps = { envList: EnvironmentVariable[] - onConfirm: (state: boolean) => void + onConfirm: (state: boolean) => void | Promise onClose: () => void } @@ -25,10 +25,13 @@ const DSLExportConfirmModal = ({ const { t } = useTranslation() const [exportSecrets, setExportSecrets] = useState(false) + const [exporting, startExport] = useTransition() const submit = () => { - onConfirm(exportSecrets) - onClose() + startExport(async () => { + await onConfirm(exportSecrets) + onClose() + }) } return ( @@ -38,7 +41,7 @@ const DSLExportConfirmModal = ({ className={cn('w-[480px] max-w-[480px]')} >
{t('env.export.title', { ns: 'workflow' })}
-
+
@@ -77,8 +80,8 @@ const DSLExportConfirmModal = ({
setExportSecrets(!exportSecrets)}>{t('env.export.checkbox', { ns: 'workflow' })}
- - + +
)