From 1b4ae3bcd2e6158b94758380a7600a818a9e6cb2 Mon Sep 17 00:00:00 2001
From: yyh <92089059+lyzno1@users.noreply.github.com>
Date: Fri, 4 Sep 2026 11:39:50 +0000
Subject: [PATCH] fix(button): align loading accessibility contract (#41823)
---
.../references/accessibility-ui.md | 7 +-
.../docs/accessible-names-and-descriptions.md | 8 +--
.../src/alert-dialog/index.stories.tsx | 22 +++++-
packages/dify-ui/src/button/README.md | 67 +++++++++----------
packages/dify-ui/src/button/index.stories.tsx | 42 +++++++++---
packages/dify-ui/src/button/index.tsx | 5 ++
.../webapp-reset-password/check-code/page.tsx | 8 +--
.../webapp-reset-password/page.tsx | 1 -
.../webapp-signin/check-code/page.tsx | 8 +--
.../components/mail-and-code-auth.tsx | 2 +-
.../account-page/AvatarWithEdit.tsx | 2 +-
web/app/account/oauth/authorize/page.tsx | 2 +-
.../add-access-subject-popover.tsx | 2 +-
.../app-sidebar/app-info/app-info-modals.tsx | 10 ++-
.../app/annotation/batch-action.tsx | 6 +-
.../access-control-form.tsx | 1 -
.../app/app-publisher/version-info-modal.tsx | 6 +-
.../environment-table/deploy-menu/index.tsx | 1 -
.../app/deploy/environment-table/index.tsx | 1 -
.../undeploy-confirm-dialog/index.tsx | 7 +-
.../components/apps/app-card/interactions.tsx | 5 +-
...import-from-marketplace-template-modal.tsx | 2 +-
.../components/base/app-icon-picker/index.tsx | 8 +--
.../form/__tests__/actions.spec.tsx | 8 +++
.../base/form/components/form/actions.tsx | 2 +-
.../website/base/__tests__/url-input.spec.tsx | 36 ++--------
.../create/website/base/url-input.tsx | 2 +-
.../firecrawl/__tests__/index.spec.tsx | 5 +-
.../documents/components/operations.tsx | 6 +-
.../base/options/__tests__/index.spec.tsx | 54 ++++++++-------
.../website-crawl/base/options/index.tsx | 12 ++--
.../detail/completed/common/batch-action.tsx | 6 +-
.../datasets/settings/form/index.tsx | 2 +-
.../__tests__/compliance.spec.tsx | 18 +++--
.../header/account-dropdown/compliance.tsx | 24 ++++---
.../__tests__/index.spec.tsx | 27 ++++++++
.../edit-workspace-modal/index.tsx | 14 ++--
.../members-page/invite-modal/index.tsx | 8 +--
.../__tests__/config-model.spec.tsx | 4 +-
.../model-auth/config-model.tsx | 9 +--
.../__tests__/marketplace-section.spec.tsx | 39 +++++++++++
.../model-selector/marketplace-section.tsx | 22 +++---
.../provider-card-actions.tsx | 2 +-
.../role-list/copy-members-confirm-dialog.tsx | 2 +-
.../__tests__/index.spec.tsx | 5 ++
.../workflow-log-archives-page/index.tsx | 16 ++---
.../main-nav/components/web-apps-section.tsx | 1 -
.../install-from-github/steps/loaded.tsx | 4 +-
.../__tests__/add-oauth-button.spec.tsx | 1 -
.../authorize/oauth-visibility-dialog.tsx | 8 +--
.../components/header-modals.tsx | 2 +-
.../components/plugins/plugin-item/action.tsx | 2 +-
.../plugins/plugin-mutation-model/index.tsx | 7 +-
.../update-plugin/from-market-place.tsx | 19 +++---
.../rag-pipeline/components/conversion.tsx | 2 +-
.../__tests__/actions.spec.tsx | 13 ++++
.../document-processing/actions.tsx | 5 +-
.../components/update-dsl-modal.tsx | 2 +-
.../components/snippet-header/index.tsx | 7 +-
.../snippets/create-snippet-dialog.tsx | 2 +-
.../snippets/import-snippet-dsl-dialog.tsx | 1 -
.../__tests__/get-schema.spec.tsx | 25 +++++++
.../get-schema.tsx | 2 +-
.../edit-custom-collection-modal/test-api.tsx | 1 -
web/app/components/tools/mcp/index.tsx | 6 +-
.../setting/build-in/config-credentials.tsx | 1 -
.../dsl-export-confirm-modal.spec.tsx | 1 +
.../workflow/dsl-export-confirm-modal.tsx | 15 +++--
.../nodes/data-source/before-run-form.tsx | 2 +-
.../delivery-method/test-email-sender.tsx | 2 +-
.../delete-confirm-modal.tsx | 11 +--
.../panel/version-history-panel/empty.tsx | 2 +-
.../restore-confirm-modal.tsx | 12 +---
.../components/workflow/update-dsl-modal.tsx | 2 +-
.../confirm-clear-session-dialog.tsx | 2 +-
.../orchestrate/build-draft-bar.tsx | 2 +-
.../orchestrate/publish-bar/index.tsx | 24 ++++---
.../__tests__/add-source-page.spec.tsx | 59 ++++++++++++++++
.../__tests__/document-detail-page.spec.tsx | 57 +++++++++++-----
.../new-rag/__tests__/documents-page.spec.tsx | 10 +--
.../__tests__/website-crawl-preview.spec.tsx | 17 ++++-
web/features/new-rag/add-source-page.tsx | 35 +++++++---
.../components/add-source-exit-dialog.tsx | 2 +-
web/features/new-rag/crawl-selection-form.tsx | 4 +-
web/features/new-rag/document-chunk-tree.tsx | 11 +--
.../new-rag/document-detail-header.tsx | 16 ++---
web/features/new-rag/document-detail-page.tsx | 3 -
.../new-rag/document-detail-status.tsx | 40 +++++++----
web/features/new-rag/document-list.tsx | 5 --
web/features/new-rag/documents-page.tsx | 5 --
.../new-rag/knowledge-space-shell.tsx | 6 +-
web/features/new-rag/new-knowledge-list.tsx | 16 +++--
.../new-rag/processing-tasks-drawer.tsx | 8 ---
web/features/new-rag/sources-page.tsx | 3 +-
.../new-rag/website-crawl-preview.tsx | 29 ++++----
.../skills/detail/sidebar-actions.tsx | 6 +-
web/features/skills/page.tsx | 17 ++---
97 files changed, 637 insertions(+), 444 deletions(-)
create mode 100644 web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/marketplace-section.spec.tsx
diff --git a/.agents/skills/frontend-code-review/references/accessibility-ui.md b/.agents/skills/frontend-code-review/references/accessibility-ui.md
index eb9cdd47728..f7bb6d6918f 100644
--- a/.agents/skills/frontend-code-review/references/accessibility-ui.md
+++ b/.agents/skills/frontend-code-review/references/accessibility-ui.md
@@ -63,7 +63,6 @@ Flag:
- Missing stable `name` on form fields that submit or validate.
- Incorrect input `type`, `inputMode`, `autoComplete`, or `spellCheck` for email, token, URL, number, search, code, or username fields.
- Labels that are not clickable.
-- Submit buttons disabled before a request starts, preventing normal submit behavior.
- Non-submit buttons inside forms missing `type="button"`.
- Errors not associated with fields or not reachable by screen readers.
- Error recovery that does not focus or expose the first invalid field.
@@ -77,7 +76,11 @@ Prefer visible labels and associate them through the appropriate field primitive
Flag:
-- Loading state without `aria-busy`, `role="status"`, or another accessible update path when it changes user interaction.
+- Loading controls whose accessible name disappears, or whose user-relevant progress has no
+ feature-owned status path. Follow the Dify UI Button contract for focused loading buttons; do not
+ add `aria-busy` to a button as a generic pending-state substitute.
+- The same pending state passed to both Dify UI Button `loading` and `disabled`, which duplicates
+ state ownership and obscures whether `disabled` expresses independent unavailability.
- Spinner or decorative loading icon exposed to screen readers.
- Disabled controls that hide the reason users cannot proceed.
- `aria-disabled` used without manually blocking click, Space, and Enter.
diff --git a/packages/dify-ui/docs/accessible-names-and-descriptions.md b/packages/dify-ui/docs/accessible-names-and-descriptions.md
index d948c3290ff..71c3cbaafa6 100644
--- a/packages/dify-ui/docs/accessible-names-and-descriptions.md
+++ b/packages/dify-ui/docs/accessible-names-and-descriptions.md
@@ -38,7 +38,7 @@ text is not a label relationship by proximity alone.
| Surface | Contract |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Text button or link | Follow [Button]. Let meaningful child text name the action; do not repeat it in `aria-label`. |
+| Text button or link | Let meaningful child text name the action; do not repeat it in `aria-label`. Follow [Button] when a focused loading button changes its label. |
| Form control | Follow [Forms]. Use its label primitive or an associated native `label`, preserving label activation. |
| Icon-only command | Follow [IconButton]. Its component-specific contract requires one accessible-name source and a decorative glyph. |
| Dialog or named region | Reuse the visible title through the primitive title API or `aria-labelledby`; use `aria-label` only when no suitable visible title exists. |
@@ -58,9 +58,9 @@ A description is optional when the name is sufficient. For a file action, the na
the operation and file, while the description explains retention or recovery. Avoid repeating the
same sentence in both. See the [name and description computation specification][accname].
-A name or description attribute is not an announcement mechanism. Keep progress and asynchronous
-updates with their existing feature owner. Follow [Button] for loading behavior and [Forms] for
-field error relationships.
+A name or description attribute is not a general announcement mechanism. Keep progress and
+asynchronous updates with their existing feature owner. Follow [Button] for the narrower case of a
+focused loading button whose visible label changes, and [Forms] for field error relationships.
## Overrides and References
diff --git a/packages/dify-ui/src/alert-dialog/index.stories.tsx b/packages/dify-ui/src/alert-dialog/index.stories.tsx
index bc2cf398148..d0a8fa7efe4 100644
--- a/packages/dify-ui/src/alert-dialog/index.stories.tsx
+++ b/packages/dify-ui/src/alert-dialog/index.stories.tsx
@@ -152,6 +152,7 @@ export const Controlled: Story = {
const LoadingConfirmDemo = () => {
const [pending, setPending] = React.useState(false)
const [open, setOpen] = React.useState(false)
+ const confirmLabelId = React.useId()
const handleConfirm = () => {
setPending(true)
@@ -180,8 +181,13 @@ const LoadingConfirmDemo = () => {
Cancel
-
- {pending ? 'Archiving…' : 'Archive'}
+
+ {pending ? 'Archiving…' : 'Archive'}
@@ -191,4 +197,16 @@ const LoadingConfirmDemo = () => {
export const LoadingConfirm: Story = {
render: () => ,
+ play: async ({ canvas, canvasElement, userEvent }) => {
+ const body = within(canvasElement.ownerDocument.body)
+
+ await userEvent.click(canvas.getByRole('button', { name: 'Archive workspace' }))
+ const confirmButton = body.getByRole('button', { name: 'Archive' })
+ await userEvent.click(confirmButton)
+
+ await expect(confirmButton).toHaveAccessibleName('Archiving…')
+ await expect(confirmButton).toHaveAttribute('aria-disabled', 'true')
+ await expect(confirmButton).not.toHaveAttribute('aria-busy')
+ await expect(confirmButton).toHaveFocus()
+ },
}
diff --git a/packages/dify-ui/src/button/README.md b/packages/dify-ui/src/button/README.md
index 8ece6c6bdc0..b8a1e41f43b 100644
--- a/packages/dify-ui/src/button/README.md
+++ b/packages/dify-ui/src/button/README.md
@@ -41,19 +41,9 @@ button semantics. It is not a link mode.
| `disabled` | The action is unavailable. | Native-disabled and removed from the tab order. |
| `loading` | The action was triggered and is now pending. | Activation is blocked while the button retains focus. |
-Internally, Dify UI maps these states to Base UI's interaction contract:
-
-```tsx
-disabled={disabled || loading}
-focusableWhenDisabled={focusableWhenDisabled ?? loading}
-```
-
-Base UI recommends disabling a loading button while setting `focusableWhenDisabled` so that an
-action does not lose focus after it is triggered. Dify UI's `loading` prop owns that wiring and
-adds the visible spinner. The loading button remains in the tab order with `aria-disabled`
-instead of the native [`disabled`] attribute. Unlike native disabled, [`aria-disabled`] preserves
-focusability but requires the component to suppress activation. Callers should pass the pending
-state only to `loading`:
+`loading` owns Base UI's disabled interaction, retained focus, and the decorative spinner. The
+button remains in the tab order with `aria-disabled`, and Dify UI suppresses activation. Pass the
+pending state only to `loading`:
```tsx
@@ -62,44 +52,55 @@ state only to `loading`:
Keep independent availability conditions in `disabled`:
```tsx
-
)}
- void refresh()} disabled={pending}>
+ void refresh()}>
{t(($) => $['newKnowledge.refreshConnectionStatus'])}
diff --git a/web/features/new-rag/components/add-source-exit-dialog.tsx b/web/features/new-rag/components/add-source-exit-dialog.tsx
index cd64cb7079b..036ff4298d1 100644
--- a/web/features/new-rag/components/add-source-exit-dialog.tsx
+++ b/web/features/new-rag/components/add-source-exit-dialog.tsx
@@ -55,7 +55,7 @@ export function AddSourceExitDialog({
{tCommon(($) => $['operation.cancel'])}
-
+
{t(($) => $['newKnowledge.discardDraftConfirm'])}
diff --git a/web/features/new-rag/crawl-selection-form.tsx b/web/features/new-rag/crawl-selection-form.tsx
index e16bfcf02ad..baaf540390c 100644
--- a/web/features/new-rag/crawl-selection-form.tsx
+++ b/web/features/new-rag/crawl-selection-form.tsx
@@ -419,7 +419,7 @@ function ReadyCrawlSelectionForm({
type="button"
variant="tertiary"
size="small"
- disabled={submissionLocked}
+ disabled={submitting || policyUncertain || selectionUncertain}
loading={busy}
onClick={onRecrawl}
>
@@ -552,7 +552,7 @@ function ReadyCrawlSelectionForm({
diff --git a/web/features/new-rag/document-chunk-tree.tsx b/web/features/new-rag/document-chunk-tree.tsx
index bc32b68221b..045234f49ca 100644
--- a/web/features/new-rag/document-chunk-tree.tsx
+++ b/web/features/new-rag/document-chunk-tree.tsx
@@ -40,6 +40,7 @@ export function DocumentChunkTreePanel({
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const treeHeadingId = useId()
+ const loadMoreLabelId = useId()
const [collapsedChunkIds, setCollapsedChunkIds] = useState>(() => new Set())
const [focusedChunkId, setFocusedChunkId] = useState()
const [treeHasFocus, setTreeHasFocus] = useState(false)
@@ -264,13 +265,15 @@ export function DocumentChunkTreePanel({
)}
- {isFetchNextPageError
- ? tCommon(($) => $['operation.retry'])
- : t(($) => $['newKnowledge.loadMore'])}
+
+ {isFetchNextPageError
+ ? tCommon(($) => $['operation.retry'])
+ : t(($) => $['newKnowledge.loadMore'])}
+
)}
diff --git a/web/features/new-rag/document-detail-header.tsx b/web/features/new-rag/document-detail-header.tsx
index 92c87613766..701dc2a2f1e 100644
--- a/web/features/new-rag/document-detail-header.tsx
+++ b/web/features/new-rag/document-detail-header.tsx
@@ -13,7 +13,7 @@ import {
SelectLabel,
SelectTrigger,
} from '@langgenius/dify-ui/select'
-import { useEffect, useRef } from 'react'
+import { useEffect, useId, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Link from '@/next/link'
@@ -31,7 +31,6 @@ export function DocumentDetailHeader({
reindexDisabledReasonId,
reindexing,
revisions,
- taskIsActive,
titleRef,
}: {
backPath: string
@@ -47,12 +46,12 @@ export function DocumentDetailHeader({
reindexDisabledReasonId?: string
reindexing: boolean
revisions: Array>
- taskIsActive: boolean
titleRef: RefObject
}) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const revisionTriggerRef = useRef(null)
+ const loadMoreRevisionLabelId = useId()
const loadMoreRequestedRef = useRef(false)
const wasFetchingNextPageRef = useRef(false)
useEffect(() => {
@@ -114,20 +113,21 @@ export function DocumentDetailHeader({
)}
{(hasNextRevisionPage || isFetchNextRevisionPageError) && (
{
loadMoreRequestedRef.current = true
fetchNextRevisionPage()
}}
>
- {isFetchNextRevisionPageError
- ? tCommon(($) => $['operation.retry'])
- : t(($) => $['newKnowledge.loadMoreRevisions'])}
+
+ {isFetchNextRevisionPageError
+ ? tCommon(($) => $['operation.retry'])
+ : t(($) => $['newKnowledge.loadMoreRevisions'])}
+
)}
void setSelectedRevision(revision)}
reindexDisabled={
!canEdit ||
- reindexBusy ||
- submissionPending ||
taskIsActive ||
tasksPending ||
isFetchingNextTaskPage ||
@@ -215,7 +213,6 @@ export function DocumentDetailPage({
reindexDisabledReasonId={!hasEditPermission ? REINDEX_RESTRICTION_ID : undefined}
reindexing={reindexBusy || submissionPending}
revisions={availableRevisions}
- taskIsActive={taskIsActive}
titleRef={titleRef}
/>
{!hasEditPermission && (
diff --git a/web/features/new-rag/document-detail-status.tsx b/web/features/new-rag/document-detail-status.tsx
index 23c2118f6f8..2a02459a776 100644
--- a/web/features/new-rag/document-detail-status.tsx
+++ b/web/features/new-rag/document-detail-status.tsx
@@ -1,7 +1,7 @@
import type { DocumentProcessingTask } from '@dify/contracts/knowledge-fs/types.gen'
import type { RefObject } from 'react'
import { Button } from '@langgenius/dify-ui/button'
-import { useEffect, useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export function DocumentDetailStatus({
@@ -49,6 +49,26 @@ export function DocumentDetailStatus({
const { t: tCommon } = useTranslation('common')
const permissionRetryRef = useRef(null)
const permissionRecoveryWasNeededRef = useRef(false)
+ const pendingRecoveryActionRef = useRef<'check' | 'retry' | null>(null)
+ const [pendingRecoveryAction, setPendingRecoveryAction] = useState<'check' | 'retry' | null>(null)
+ const recoveryBusy = submissionRecoveryBusy || pendingRecoveryAction !== null
+
+ const handleSubmissionRecovery = async (
+ action: 'check' | 'retry',
+ recover: () => Promise,
+ ) => {
+ if (submissionRecoveryBusy || pendingRecoveryActionRef.current) return
+
+ pendingRecoveryActionRef.current = action
+ setPendingRecoveryAction(action)
+ try {
+ await recover()
+ } finally {
+ pendingRecoveryActionRef.current = null
+ setPendingRecoveryAction(null)
+ titleRef.current?.focus()
+ }
+ }
useEffect(() => {
if (permissionRecoveryNeeded && !permissionRecoveryWasNeededRef.current)
@@ -102,7 +122,6 @@ export function DocumentDetailStatus({
{t(($) => $['newKnowledge.documentPermissionRestricted'])}
void retryWritePermission().then((recovered) => {
@@ -145,7 +164,7 @@ export function DocumentDetailStatus({
)}
- {submissionTimedOut && (
+ {(submissionTimedOut || pendingRecoveryAction) && (
{t(($) => $['newKnowledge.documentReindexConfirmationDelayed'])}
- void recheckTimedOutSubmission().finally(() => titleRef.current?.focus())
- }
+ loading={pendingRecoveryAction === 'check'}
+ disabled={recoveryBusy && pendingRecoveryAction !== 'check'}
+ onClick={() => void handleSubmissionRecovery('check', recheckTimedOutSubmission)}
>
{t(($) => $['newKnowledge.checkReindexStatus'])}
- void retryTimedOutSubmission().finally(() => titleRef.current?.focus())
- }
+ loading={pendingRecoveryAction === 'retry'}
+ disabled={recoveryBusy && pendingRecoveryAction !== 'retry'}
+ onClick={() => void handleSubmissionRecovery('retry', retryTimedOutSubmission)}
>
{t(($) => $['newKnowledge.retryReindexDocument'])}
diff --git a/web/features/new-rag/document-list.tsx b/web/features/new-rag/document-list.tsx
index 34e556f2c85..d5364439657 100644
--- a/web/features/new-rag/document-list.tsx
+++ b/web/features/new-rag/document-list.tsx
@@ -275,7 +275,6 @@ export function DocumentsEmpty({
$['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
- aria-busy={isFetchingNextDocumentPage}
loading={isFetchingNextDocumentPage}
onBlur={(event) => {
if (event.relatedTarget) restoreLoadMoreFocusRef.current = false
@@ -610,7 +607,6 @@ export function DocumentsList({
{
if (event.relatedTarget) restoreLoadMoreFocusRef.current = false
@@ -659,7 +655,6 @@ export function DocumentBulkActions({
>
$['operation.retry'])} · ${t(($) => $['newKnowledge.permissionLoadFailed'])}`}
- aria-busy={workspacePermissionKeysFetching}
loading={workspacePermissionKeysFetching}
size="small"
onBlur={(event) => {
@@ -1972,7 +1971,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
$['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
- aria-busy={documentsQuery.isRefetching}
loading={documentsQuery.isRefetching}
size="small"
onBlur={(event) => {
@@ -2005,7 +2003,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
? t(($) => $['newKnowledge.sourcesErrorDescription'])
: t(($) => $['newKnowledge.tasksErrorDescription'])
}`}
- aria-busy={dependencyRetryFetching}
loading={dependencyRetryFetching}
size="small"
onBlur={(event) => {
@@ -2051,7 +2048,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
$['operation.retry'])} · ${documentsRecoveryDescription}`}
- aria-busy={documentsQuery.isFetching}
className="mt-4"
loading={documentsQuery.isFetching}
onBlur={(event) => {
@@ -2085,7 +2081,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
? t(($) => $['newKnowledge.tasksErrorDescription'])
: t(($) => $['newKnowledge.sourcesErrorDescription'])
}`}
- aria-busy={blockingDependencyRetryFetching}
className="mt-4"
loading={blockingDependencyRetryFetching}
onBlur={(event) => {
diff --git a/web/features/new-rag/knowledge-space-shell.tsx b/web/features/new-rag/knowledge-space-shell.tsx
index 9822a3d8643..8649c00c05c 100644
--- a/web/features/new-rag/knowledge-space-shell.tsx
+++ b/web/features/new-rag/knowledge-space-shell.tsx
@@ -1,7 +1,7 @@
'use client'
import type { ReactNode } from 'react'
-import { Button } from '@langgenius/dify-ui/button'
+import { Button, buttonVariants } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery } from '@tanstack/react-query'
@@ -105,9 +105,9 @@ export function KnowledgeSpaceShell({
)}
- }>
+
{t(($) => $['newKnowledge.backToList'])}
-
+
{!notFound && (
void knowledgeSpaceQuery.refetch()}>
{tCommon(($) => $['operation.retry'])}
diff --git a/web/features/new-rag/new-knowledge-list.tsx b/web/features/new-rag/new-knowledge-list.tsx
index cb50a0ca925..eae1e017e68 100644
--- a/web/features/new-rag/new-knowledge-list.tsx
+++ b/web/features/new-rag/new-knowledge-list.tsx
@@ -1,6 +1,6 @@
'use client'
-import { Button } from '@langgenius/dify-ui/button'
+import { Button, buttonVariants } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
@@ -135,15 +135,17 @@ export function NewKnowledgeList({
{canCreate && (
- }
- variant="primary"
- size="medium"
- className="px-2 shadow-xs"
+
{createLabel}
-
+
)}
diff --git a/web/features/new-rag/processing-tasks-drawer.tsx b/web/features/new-rag/processing-tasks-drawer.tsx
index 15bf639c115..3260cd7dd83 100644
--- a/web/features/new-rag/processing-tasks-drawer.tsx
+++ b/web/features/new-rag/processing-tasks-drawer.tsx
@@ -428,7 +428,6 @@ export function ProcessingTasksDrawer({
$['operation.retry'])} · ${t(($) => $['newKnowledge.permissionLoadFailed'])}`}
- aria-busy={permissionQueryFetching}
className="mt-3"
loading={permissionQueryFetching}
size="small"
@@ -452,7 +451,6 @@ export function ProcessingTasksDrawer({
$['operation.retry'])} · ${t(($) => $['newKnowledge.tasksErrorDescription'])}`}
- aria-busy={taskQueryFetching}
className="mt-3"
loading={taskQueryFetching}
size="small"
@@ -476,7 +474,6 @@ export function ProcessingTasksDrawer({
$['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
- aria-busy={documentQueryFetching}
className="mt-3"
loading={documentQueryFetching}
size="small"
@@ -560,8 +557,6 @@ export function ProcessingTasksDrawer({
: undefined
}
size="small"
- aria-busy={pendingActions.has(task.id)}
- disabled={pendingActions.has(task.id)}
loading={pendingActions.has(task.id)}
onBlur={(event) => {
if (event.relatedTarget) focusedTaskActionRef.current = null
@@ -581,8 +576,6 @@ export function ProcessingTasksDrawer({
: undefined
}
size="small"
- aria-busy={pendingActions.has(task.id)}
- disabled={pendingActions.has(task.id)}
loading={pendingActions.has(task.id)}
onBlur={(event) => {
if (event.relatedTarget) focusedTaskActionRef.current = null
@@ -608,7 +601,6 @@ export function ProcessingTasksDrawer({
{
loadMoreRequestedRef.current = false
diff --git a/web/features/new-rag/sources-page.tsx b/web/features/new-rag/sources-page.tsx
index f394c94208f..f4acb401841 100644
--- a/web/features/new-rag/sources-page.tsx
+++ b/web/features/new-rag/sources-page.tsx
@@ -192,7 +192,6 @@ function SourceActions({
void onRemove().then((removed) => {
if (removed) setRemoveDialogOpen(false)
@@ -375,7 +374,7 @@ function SourceRow({
size="small"
variant="secondary"
loading={pendingAction === 'sync'}
- disabled={Boolean(pendingAction)}
+ disabled={pendingAction !== undefined && pendingAction !== 'sync'}
onClick={() => void syncSource()}
>
{tCommon(($) => $['operation.retry'])}
diff --git a/web/features/new-rag/website-crawl-preview.tsx b/web/features/new-rag/website-crawl-preview.tsx
index b25dd2a4001..171f96e725b 100644
--- a/web/features/new-rag/website-crawl-preview.tsx
+++ b/web/features/new-rag/website-crawl-preview.tsx
@@ -295,6 +295,8 @@ export function WebsiteCrawlPreview({
const { t } = useTranslation('dataset')
const router = useRouter()
const rootUrlErrorId = useId()
+ const primaryActionLabelId = useId()
+ const stopButtonLabelId = useId()
const [rootUrl, setRootUrl] = useState(initialDraft?.rootUrl ?? '')
const [sourceName, setSourceName] = useState(initialDraft?.sourceName ?? '')
const [urlTouched, setUrlTouched] = useState(false)
@@ -403,7 +405,8 @@ export function WebsiteCrawlPreview({
run && !starting && !stopping && !pollPaused && (active || !pagesLoaded),
)
const runId = run?.id
- const locked = starting || stopping || active || successfulPreview || uncertainOperation
+ const workflowUnavailable = stopping || active || successfulPreview || uncertainOperation
+ const locked = starting || workflowUnavailable
const dirty = Boolean(
rootUrl || sourceName || run || !includeSubpages || pageLimit !== DEFAULT_PAGE_LIMIT,
)
@@ -1162,11 +1165,14 @@ export function WebsiteCrawlPreview({
className="mt-4 w-full"
disabled={
!configuration ||
- (locked && requestError !== 'POLL_FAILED' && !canReconcileUncertainOperation)
+ (workflowUnavailable &&
+ requestError !== 'POLL_FAILED' &&
+ !canReconcileUncertainOperation)
}
loading={starting}
+ aria-labelledby={primaryActionLabelId}
>
- {primaryLabel}
+ {primaryLabel}
)}
@@ -1193,12 +1199,15 @@ export function WebsiteCrawlPreview({
variant="tertiary"
size="small"
className="ml-auto shrink-0"
- disabled={stopping}
+ loading={stopping}
+ aria-labelledby={stopButtonLabelId}
onClick={() => void stop()}
>
- {stopping
- ? t(($) => $['newKnowledge.stoppingCrawl'])
- : t(($) => $['newKnowledge.stopCrawl'])}
+
+ {stopping
+ ? t(($) => $['newKnowledge.stoppingCrawl'])
+ : t(($) => $['newKnowledge.stopCrawl'])}
+
{requestError === 'CANCEL_FAILED' && (
@@ -1333,11 +1342,7 @@ export function WebsiteCrawlPreview({
{t(($) => $['newKnowledge.keepEditing'])}
- void discardAndCancel()}
- >
+ void discardAndCancel()}>
{t(($) => $['newKnowledge.discardSourceChangesConfirm'])}
diff --git a/web/features/skills/detail/sidebar-actions.tsx b/web/features/skills/detail/sidebar-actions.tsx
index 06f2cc59ba2..2b6189d5de6 100644
--- a/web/features/skills/detail/sidebar-actions.tsx
+++ b/web/features/skills/detail/sidebar-actions.tsx
@@ -71,10 +71,10 @@ function SkillDetailDeleteDialog({
})
const references = referencesQuery.data?.data ?? []
const referenceCount = Math.max(detail.reference_count ?? 0, references.length)
- const isDeleteDisabled =
- deleteMutation.isPending ||
+ const isDeleteUnavailable =
(open && (referencesQuery.isFetching || !referencesQuery.isSuccess)) ||
(referenceCount > 0 && confirmDeleteInput !== detail.display_name)
+ const isDeleteDisabled = deleteMutation.isPending || isDeleteUnavailable
const description =
referenceCount > 0
? t(
@@ -186,7 +186,7 @@ function SkillDetailDeleteDialog({
{tCommon(($) => (referenceCount > 0 ? $['operation.confirm'] : $['operation.delete']))}
diff --git a/web/features/skills/page.tsx b/web/features/skills/page.tsx
index 3cc34a36e00..0aec1f3509d 100644
--- a/web/features/skills/page.tsx
+++ b/web/features/skills/page.tsx
@@ -274,8 +274,7 @@ function DeleteSkillDialog({
})
const references = referencesQuery.data?.data ?? []
const referenceCount = Math.max(skill.reference_count ?? 0, references.length)
- const isDeleteDisabled =
- deleteMutation.isPending ||
+ const isDeleteUnavailable =
(open && (referencesQuery.isFetching || !referencesQuery.isSuccess)) ||
(referenceCount > 0 && confirmDeleteInput !== skill.display_name)
const description =
@@ -290,7 +289,7 @@ function DeleteSkillDialog({
: t(($) => $['skillManagement.deleteDialog.description'])
const handleDelete = () => {
- if (isDeleteDisabled) return
+ if (deleteMutation.isPending || isDeleteUnavailable) return
deleteMutation.mutate(
{
@@ -388,7 +387,7 @@ function DeleteSkillDialog({
{tCommon(($) => $['operation.delete'])}
@@ -674,8 +673,6 @@ function SkillsToolbar({
}) {
const { t } = useTranslation('skill')
const [keyword, setKeyword] = useQueryState(skillQueryParamNames.keyword, skillKeywordQueryParser)
- const isMutating = creating || importing
-
return (
@@ -692,7 +689,7 @@ function SkillsToolbar({
{canEdit && (
@@ -704,7 +701,7 @@ function SkillsToolbar({
@@ -889,7 +886,7 @@ export default function SkillsPage() {
useDocumentTitle(t(($) => $['skillManagement.title']))
const handleCreate = () => {
- if (createMutation.isPending) return
+ if (createMutation.isPending || importMutation.isPending) return
createMutation.mutate(
{
@@ -914,7 +911,7 @@ export default function SkillsPage() {
}
const handleFileChange = (file: File | undefined) => {
- if (!file || importMutation.isPending) return
+ if (!file || importMutation.isPending || createMutation.isPending) return
importMutation.mutate(
{