From efe506a0f57576777dd2cd4f0c0b862c1a8136af Mon Sep 17 00:00:00 2001
From: Joel
Date: Tue, 8 Sep 2026 04:22:04 +0000
Subject: [PATCH] fix: correct page landmarks and document layout rules
(#41938)
---
.../references/accessibility-ui.md | 9 +++
web/AGENTS.md | 2 +
.../components/app/access-config/index.tsx | 4 +-
web/app/components/app/access-point/index.tsx | 4 +-
web/app/components/app/deploy/index.tsx | 4 +-
.../datasets/access-config/index.tsx | 4 +-
.../datasets/list/__tests__/datasets.spec.tsx | 61 ++++++++-----------
web/app/components/datasets/list/datasets.tsx | 4 +-
.../integrations/__tests__/page.spec.tsx | 5 +-
web/app/components/integrations/index.tsx | 17 ++++--
.../main-nav/__tests__/index.spec.tsx | 2 +-
web/app/components/main-nav/index.tsx | 5 +-
.../creator-profile/__tests__/view.spec.tsx | 28 +++++++--
.../creator-profile/creator-sidebar.tsx | 4 +-
.../marketplace/creator-profile/view.tsx | 4 +-
web/app/signin/__tests__/layout.spec.tsx | 21 +++++++
web/app/signin/_header.tsx | 4 +-
web/app/signin/layout.tsx | 8 +--
web/docs/landmarks.md | 57 +++++++++++++++++
web/features/new-rag/add-source-page.tsx | 4 +-
web/features/new-rag/sources-page.tsx | 4 +-
.../__tests__/detail-page-builder.spec.tsx | 10 +--
.../__tests__/detail-page-clipboard.spec.tsx | 9 +--
.../__tests__/detail-page-navigation.spec.tsx | 12 ++++
.../__tests__/detail-page-publishing.spec.tsx | 2 +-
.../skills/__tests__/detail-page.fixture.tsx | 8 +--
web/features/skills/detail/builder-panel.tsx | 12 ++--
web/features/skills/detail/file-editor.tsx | 4 +-
web/features/skills/detail/file-tree.tsx | 18 ++++--
web/features/skills/detail/shell.tsx | 12 ++--
web/features/skills/detail/version-panel.tsx | 9 +--
web/i18n/ar-TN/common.json | 1 +
web/i18n/de-DE/common.json | 1 +
web/i18n/en-US/common.json | 1 +
web/i18n/es-ES/common.json | 1 +
web/i18n/fa-IR/common.json | 1 +
web/i18n/fr-FR/common.json | 1 +
web/i18n/hi-IN/common.json | 1 +
web/i18n/id-ID/common.json | 1 +
web/i18n/it-IT/common.json | 1 +
web/i18n/ja-JP/common.json | 1 +
web/i18n/ko-KR/common.json | 1 +
web/i18n/lo-LA/common.json | 1 +
web/i18n/nl-NL/common.json | 1 +
web/i18n/pl-PL/common.json | 1 +
web/i18n/pt-BR/common.json | 1 +
web/i18n/ro-RO/common.json | 1 +
web/i18n/ru-RU/common.json | 1 +
web/i18n/sl-SI/common.json | 1 +
web/i18n/th-TH/common.json | 1 +
web/i18n/tr-TR/common.json | 1 +
web/i18n/uk-UA/common.json | 1 +
web/i18n/vi-VN/common.json | 1 +
web/i18n/zh-Hans/common.json | 1 +
web/i18n/zh-Hant/common.json | 1 +
55 files changed, 260 insertions(+), 115 deletions(-)
create mode 100644 web/app/signin/__tests__/layout.spec.tsx
create mode 100644 web/docs/landmarks.md
diff --git a/.agents/skills/frontend-code-review/references/accessibility-ui.md b/.agents/skills/frontend-code-review/references/accessibility-ui.md
index e8a841502e0..ed3dc42b6e2 100644
--- a/.agents/skills/frontend-code-review/references/accessibility-ui.md
+++ b/.agents/skills/frontend-code-review/references/accessibility-ui.md
@@ -26,6 +26,14 @@ Flag:
Prefer semantic HTML before ARIA.
+## Page Landmarks
+
+When reviewing Web page layouts, navigation, or landmark changes, read [Page landmarks].
+Inspect the composed page with its parent layouts for missing or duplicate main regions,
+inappropriate nesting or roles, and ambiguous or broken landmark names. Check affected
+unit and E2E locators when semantic elements change. Web owns these composition rules;
+use the existing naming contract for label-source decisions.
+
## Accessible Names And Descriptions
Read [Accessible names and descriptions] when a change affects labels, ARIA naming, help/error relationships, or hidden text. That document owns the shared implementation and review contract.
@@ -123,4 +131,5 @@ Flag:
- Hardcoded dates, times, numbers, or currency formats instead of `Intl.*`.
[Accessible names and descriptions]: ../../../../packages/dify-ui/docs/accessible-names-and-descriptions.md
+[Page landmarks]: ../../../../web/docs/landmarks.md
[Web Interface Guidelines]: https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
diff --git a/web/AGENTS.md b/web/AGENTS.md
index e87014aa21a..dc59e66c9be 100644
--- a/web/AGENTS.md
+++ b/web/AGENTS.md
@@ -9,6 +9,7 @@
Web owns application-specific requirements and consumes shared architecture guidance from skills and primitive contracts from Dify UI. Link to those owners instead of redefining their rules here.
- For truncated text disclosure and native `title` decisions, follow [Truncated Text Disclosure].
+- When adding or changing page layouts, navigation, or landmark elements, follow [Page landmarks] and inspect the composed page, including parent layouts.
- User-facing strings must use `web/i18n/en-US/` keys. When adding or renaming a key, update every supported locale with the correct localized value.
- For new backend calls and migrated surfaces, use generated `consoleQuery` / `consoleClient` APIs from `@/service/console`. Do not add handwritten REST helpers or DTO mirrors, mock-backed app state, or direct edits to generated contracts.
- Prefer `@langgenius/dify-ui/*` primitives, data attributes, and design tokens. Use the [Dify UI package index] to find a primitive; read the relevant contract directly when it is already known. Preserve a visible focus indicator on the final focusable element.
@@ -36,5 +37,6 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
[Dify UI package index]: ../packages/dify-ui/README.md
[IconButton contract]: ../packages/dify-ui/src/icon-button/README.md
[Input Group contract]: ../packages/dify-ui/src/input-group/README.md
+[Page landmarks]: docs/landmarks.md
[Truncated Text Disclosure]: docs/truncated-text-disclosure.md
[form contract]: ../packages/dify-ui/docs/forms.md
diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx
index 1452615d87b..79acb7e7b3c 100644
--- a/web/app/components/app/access-config/index.tsx
+++ b/web/app/components/app/access-config/index.tsx
@@ -202,7 +202,7 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
-
+
)
}
diff --git a/web/app/components/app/access-point/index.tsx b/web/app/components/app/access-point/index.tsx
index cda649d7c58..6aa7750dc16 100644
--- a/web/app/components/app/access-point/index.tsx
+++ b/web/app/components/app/access-point/index.tsx
@@ -67,7 +67,7 @@ function AccessPointContent({
environment === selectedEnvironment ? highlightedAccessPoint : null
return (
-
+
@@ -145,7 +145,7 @@ function AccessPointContent({
-
+
)
}
diff --git a/web/app/components/app/deploy/index.tsx b/web/app/components/app/deploy/index.tsx
index 8ef0fa7f57e..2b82aabfb7f 100644
--- a/web/app/components/app/deploy/index.tsx
+++ b/web/app/components/app/deploy/index.tsx
@@ -108,7 +108,7 @@ function AppDeployContent({
return (
<>
-
+
{deploymentRequest && (
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
-
+
onBatchRemoveAccessPolicyMemberBindings={handleBatchRemoveAccessPolicyMemberBindings}
onAddAccessSubject={handleAddAccessSubject}
/>
-
+
)
}
diff --git a/web/app/components/datasets/list/__tests__/datasets.spec.tsx b/web/app/components/datasets/list/__tests__/datasets.spec.tsx
index c039b886710..8e282b8ecc6 100644
--- a/web/app/components/datasets/list/__tests__/datasets.spec.tsx
+++ b/web/app/components/datasets/list/__tests__/datasets.spec.tsx
@@ -299,33 +299,22 @@ describe('Datasets', () => {
expect(screen.queryByRole('status', { name: /common\.loading/ })).not.toBeInTheDocument()
expect(screen.getByText('Dataset 1')).toBeInTheDocument()
})
-
- it('should show Loading component when isFetchingNextPage is true', () => {
- render( )
- expect(screen.getByRole('navigation')).toBeInTheDocument()
- })
-
- it('should NOT show Loading component when isFetchingNextPage is false', () => {
- render( )
- expect(screen.getByRole('navigation')).toBeInTheDocument()
- })
})
describe('DatasetList null handling', () => {
- it('should handle null datasetList gracefully', () => {
- render( )
- expect(screen.getByRole('navigation')).toBeInTheDocument()
- })
-
- it('should handle undefined datasetList gracefully', () => {
- render( )
- expect(screen.getByRole('navigation')).toBeInTheDocument()
- })
-
- it('should handle empty pages array', () => {
- render( )
- expect(screen.getByRole('navigation')).toBeInTheDocument()
- })
+ it.each([null, undefined, createDatasetListData([])])(
+ 'shows the empty state when dataset data is unavailable or empty (%j)',
+ (datasetList) => {
+ render(
+ No knowledge bases}
+ />,
+ )
+ expect(screen.getByText('No knowledge bases')).toBeInTheDocument()
+ },
+ )
})
describe('IntersectionObserver', () => {
@@ -409,20 +398,18 @@ describe('Datasets', () => {
})
})
- describe('Styles', () => {
- it('should have correct grid styling', () => {
- render( )
- const nav = screen.getByRole('navigation')
- expect(nav).toHaveClass(
- 'relative',
- 'grid',
- 'grow',
- 'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
- 'content-start',
- 'gap-3',
- 'px-8',
- 'pt-2',
+ describe('Landmarks', () => {
+ it('keeps knowledge cards in the page content without creating another navigation landmark', () => {
+ render(
+ ,
)
+ expect(screen.getByText('Dataset 1')).toBeInTheDocument()
+ expect(screen.queryByRole('navigation')).not.toBeInTheDocument()
})
})
diff --git a/web/app/components/datasets/list/datasets.tsx b/web/app/components/datasets/list/datasets.tsx
index 28c7fbee3c9..fbf6ea88c33 100644
--- a/web/app/components/datasets/list/datasets.tsx
+++ b/web/app/components/datasets/list/datasets.tsx
@@ -69,7 +69,7 @@ const Datasets = ({
return (
<>
-
+
{showDatasetSkeleton ? (
$.loading, { ns: 'common' })} />
) : (
@@ -92,7 +92,7 @@ const Datasets = ({
{!showDatasetSkeleton && !hasAnyDataset && emptyElement}
{isFetchingNextPage && }
-
+
>
)
}
diff --git a/web/app/components/integrations/__tests__/page.spec.tsx b/web/app/components/integrations/__tests__/page.spec.tsx
index d17044f0bfc..e1e85cd0650 100644
--- a/web/app/components/integrations/__tests__/page.spec.tsx
+++ b/web/app/components/integrations/__tests__/page.spec.tsx
@@ -366,7 +366,10 @@ describe('IntegrationsPage', () => {
expect(screen.getByTestId('model-provider-page')).toBeInTheDocument()
expect(screen.getAllByText('common.settings.provider')).toHaveLength(2)
expect(container.firstElementChild).toHaveClass('bg-components-panel-bg')
- expect(container.querySelector('aside')).toHaveClass('bg-components-panel-bg')
+ expect(
+ screen.getByRole('navigation', { name: 'common.settings.integrations' }),
+ ).toBeInTheDocument()
+ expect(screen.queryByRole('complementary')).not.toBeInTheDocument()
})
it('does not replace the document title when embedded in a modal', () => {
diff --git a/web/app/components/integrations/index.tsx b/web/app/components/integrations/index.tsx
index 9a4f5bcbe55..16aa348862c 100644
--- a/web/app/components/integrations/index.tsx
+++ b/web/app/components/integrations/index.tsx
@@ -12,7 +12,7 @@ import {
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
-import { useEffect, useState } from 'react'
+import { useEffect, useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UpdateSettingDialog from '@/app/components/header/account-setting/update-setting-dialog'
import {
@@ -126,6 +126,7 @@ export default function IntegrationsPage({
syncDocumentTitle = false,
}: IntegrationsPageProps) {
const { t } = useTranslation()
+ const navigationTitleId = useId()
const docLink = useDocLink()
const router = useRouter()
const section = useIntegrationSection(routeSection)
@@ -253,7 +254,7 @@ export default function IntegrationsPage({
{syncDocumentTitle && (
)}
-
-
+
)
}
diff --git a/web/app/signin/__tests__/layout.spec.tsx b/web/app/signin/__tests__/layout.spec.tsx
new file mode 100644
index 00000000000..43b748d468b
--- /dev/null
+++ b/web/app/signin/__tests__/layout.spec.tsx
@@ -0,0 +1,21 @@
+import { screen, within } from '@testing-library/react'
+import { renderWithConsoleQuery as render } from '@/test/console/query-data'
+import SignInLayout from '../layout'
+
+describe('SignInLayout landmarks', () => {
+ it('separates the site header, sign-in content, and copyright', () => {
+ render(
+
+ Sign in
+ ,
+ { systemFeatures: { branding: { enabled: false } } },
+ )
+
+ const main = screen.getByRole('main')
+ expect(within(main).getByRole('heading', { name: 'Sign in' })).toBeInTheDocument()
+ expect(screen.getByRole('banner')).not.toContainElement(main)
+ expect(screen.getByRole('contentinfo')).toHaveTextContent('LangGenius')
+ expect(main).not.toContainElement(screen.getByRole('banner'))
+ expect(main).not.toContainElement(screen.getByRole('contentinfo'))
+ })
+})
diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx
index 6bb0baed44e..93bbd93ceb6 100644
--- a/web/app/signin/_header.tsx
+++ b/web/app/signin/_header.tsx
@@ -19,7 +19,7 @@ const Header = () => {
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
-
+
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo ? (
{
-
+
)
}
diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx
index 5252ab9441d..b1eb5541c67 100644
--- a/web/app/signin/layout.tsx
+++ b/web/app/signin/layout.tsx
@@ -15,15 +15,15 @@ export default function SignInLayout({ children }: any) {
)}
>
-
+
{systemFeatures.branding.enabled === false && (
-
+
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
-
+
)}
diff --git a/web/docs/landmarks.md b/web/docs/landmarks.md
new file mode 100644
index 00000000000..a64d1b4fe1f
--- /dev/null
+++ b/web/docs/landmarks.md
@@ -0,0 +1,57 @@
+# Page Landmarks
+
+Web owns landmark composition across route layouts and feature components. Use the
+[W3C landmark guide] for role semantics and the Dify UI [naming contract] for accessible
+names. This guide records how those contracts apply to Web pages.
+
+## Page Ownership
+
+- Place perceivable page content in meaningful landmark regions and check coverage after
+ composing the layout and its children.
+- Trace the parent layouts before adding a page-level landmark. Inspect the composed
+ page, including parallel route slots and portals, rather than judging a component alone.
+- [MainNavLayout] provides the console's `main` and skip-navigation target. Descendant
+ pages and editors must not create another `main`. Hiding the main navigation does not
+ remove this wrapper. A route outside this layout must establish its own main-content
+ boundary in its owning layout or page.
+- Keep the skip link and its target under the same layout owner. Preserve the target ID
+ and focus behavior when changing wrappers; a landmark role alone does not move focus.
+- Keep page and section headings meaningful inside these boundaries. A heading does not
+ create a landmark or automatically name its surrounding section.
+
+## Choose Roles by Content
+
+- Use `nav` for a meaningful group of navigation links. A grid of resource cards is not
+ automatically navigation merely because its cards can open detail pages.
+- A visual sidebar is not automatically complementary content. File controls, editors,
+ and builder or version panels are parts of the current task. Use ordinary containers
+ or named sections according to their navigation value. Follow APG's recommendation
+ to keep genuine complementary landmarks at the top level.
+- Reserve named `section` regions for content useful to reach directly. Avoid turning
+ every wrapper, card, or modal body into a landmark. Prefer an existing visible heading
+ as the naming source, following the [naming contract].
+- Give repeated landmarks distinct names by purpose unless their content and purpose
+ are identical. Keep names localized and avoid repeating the role in the name.
+- Page-level `header` and `footer` can expose `banner` and `contentinfo`. Their implicit
+ roles depend on ancestors; local panel headers and footers do not necessarily expose
+ those landmarks. Check the composed context before adding explicit roles.
+
+## Verify the Composed Page
+
+- Check the main-content boundary, landmark hierarchy, names, and heading relationships
+ in the relevant loaded, empty, collapsed, and open-overlay states. Exclude hidden
+ skeletons from the exposed landmark inventory. Check that label references still
+ resolve when conditional content changes.
+- When changing a semantic element, inspect affected unit tests and E2E locators,
+ including CSS selectors such as `closest('main')`. Locate the intended feature or
+ control instead of relying on an extra page landmark or positional selection among
+ duplicate landmarks.
+- Follow [Web testing policy] for regression coverage. At composition boundaries, keep
+ the components that own landmark semantics real; a mocked sidebar cannot prove the
+ real sidebar's role. Distinguish DOM assertions and Cucumber dry-runs from actual
+ browser or assistive-technology verification.
+
+[MainNavLayout]: ../app/components/main-nav/layout.tsx
+[W3C landmark guide]: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions
+[Web testing policy]: test.md
+[naming contract]: ../../packages/dify-ui/docs/accessible-names-and-descriptions.md
diff --git a/web/features/new-rag/add-source-page.tsx b/web/features/new-rag/add-source-page.tsx
index f5146738950..43899bb3db6 100644
--- a/web/features/new-rag/add-source-page.tsx
+++ b/web/features/new-rag/add-source-page.tsx
@@ -960,7 +960,7 @@ export function AddSourcePage({
return (
<>
-
+
{t(($) => $['newKnowledge.addSource'])}
@@ -1111,7 +1111,7 @@ export function AddSourcePage({
)}
-
+
+
@@ -749,6 +749,6 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
) : null}
>
)}
-
+
)
}
diff --git a/web/features/skills/__tests__/detail-page-builder.spec.tsx b/web/features/skills/__tests__/detail-page-builder.spec.tsx
index 10bf5f6ba92..fbeb3586b5e 100644
--- a/web/features/skills/__tests__/detail-page-builder.spec.tsx
+++ b/web/features/skills/__tests__/detail-page-builder.spec.tsx
@@ -93,7 +93,7 @@ describe('SkillDetailPage builder', () => {
).not.toBeInTheDocument()
})
- it('moves the collapsed Skill Builder entry into the file tab header', async () => {
+ it('reopens Skill Builder from the collapsed entry', async () => {
const user = userEvent.setup()
renderSkillDetailPage()
@@ -106,12 +106,14 @@ describe('SkillDetailPage builder', () => {
const openBuilderButton = screen.getByRole('button', {
name: 'skill.skillManagement.detail.builder.open',
})
- expect(openBuilderButton.closest('main')).toBeInTheDocument()
+ expect(
+ screen.queryByRole('region', { name: 'skill.skillManagement.detail.builder.title' }),
+ ).not.toBeInTheDocument()
await user.click(openBuilderButton)
expect(
- await screen.findByRole('button', {
- name: 'skill.skillManagement.detail.builder.close',
+ await screen.findByRole('region', {
+ name: 'skill.skillManagement.detail.builder.title',
}),
).toBeInTheDocument()
})
diff --git a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx
index fdfbe6cde15..dc2f4b459cb 100644
--- a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx
+++ b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import {
createDataTransfer,
createSkillDetail,
+ getFileTabButton,
getFileTreeButton,
getFileTreeContextRegion,
getFileTreeItem,
@@ -247,13 +248,7 @@ describe('SkillDetailPage clipboard', () => {
)
})
await waitFor(() => {
- const editorMain = screen.getAllByRole('main').at(-1)
- if (!editorMain) throw new Error('file editor not found')
- expect(
- within(editorMain).getByRole('button', {
- name: 'SKILL copy.md',
- }),
- ).toBeInTheDocument()
+ expect(getFileTabButton('SKILL copy.md')).toHaveAccessibleName('SKILL copy.md')
})
})
diff --git a/web/features/skills/__tests__/detail-page-navigation.spec.tsx b/web/features/skills/__tests__/detail-page-navigation.spec.tsx
index cfb50da5893..f33b8ceac72 100644
--- a/web/features/skills/__tests__/detail-page-navigation.spec.tsx
+++ b/web/features/skills/__tests__/detail-page-navigation.spec.tsx
@@ -51,12 +51,21 @@ describe('SkillDetailPage navigation', () => {
const user = userEvent.setup()
renderSkillDetailPage()
+ expect(
+ await screen.findByRole('region', { name: /skillManagement\.detail\.fileCount/ }),
+ ).toBeInTheDocument()
+ expect(screen.queryByRole('complementary')).not.toBeInTheDocument()
+ expect(screen.queryByRole('main')).not.toBeInTheDocument()
+
await user.click(
await screen.findByRole('button', {
name: 'skill.skillManagement.detail.collapseSidebar',
}),
)
expect(screen.queryByTestId('skill-detail-sidebar-header')).not.toBeInTheDocument()
+ expect(
+ screen.queryByRole('region', { name: /skillManagement\.detail\.fileCount/ }),
+ ).not.toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', {
@@ -64,6 +73,9 @@ describe('SkillDetailPage navigation', () => {
}),
)
expect(await screen.findByTestId('skill-detail-sidebar-header')).toBeInTheDocument()
+ expect(
+ screen.getByRole('region', { name: /skillManagement\.detail\.fileCount/ }),
+ ).toBeInTheDocument()
})
it('shows the sidebar while the collapsed rail is hovered', async () => {
diff --git a/web/features/skills/__tests__/detail-page-publishing.spec.tsx b/web/features/skills/__tests__/detail-page-publishing.spec.tsx
index 55e56ccdb55..3e9de6dd156 100644
--- a/web/features/skills/__tests__/detail-page-publishing.spec.tsx
+++ b/web/features/skills/__tests__/detail-page-publishing.spec.tsx
@@ -517,7 +517,7 @@ describe('SkillDetailPage publishing', () => {
await screen.findByRole('button', { name: 'skill.skillManagement.detail.versionHistory' }),
)
- await screen.findByText('skill.skillManagement.detail.versions')
+ await screen.findByRole('region', { name: 'skill.skillManagement.detail.versions' })
expect(screen.getAllByRole('button', { current: true })).toHaveLength(1)
await user.click(
diff --git a/web/features/skills/__tests__/detail-page.fixture.tsx b/web/features/skills/__tests__/detail-page.fixture.tsx
index f3d49ed6adc..595c1959233 100644
--- a/web/features/skills/__tests__/detail-page.fixture.tsx
+++ b/web/features/skills/__tests__/detail-page.fixture.tsx
@@ -645,13 +645,9 @@ export async function openFileTreeActions(user: ReturnType) {
- const triggers = Array.from(document.querySelectorAll('aside .i-ri-add-line'))
- .map((icon) => icon.closest('button'))
- .filter((button): button is HTMLButtonElement => button instanceof HTMLButtonElement)
- const trigger = triggers.at(-1)
- if (!(trigger instanceof HTMLButtonElement)) throw new Error('root create menu trigger not found')
+ const fileTree = screen.getByRole('region', { name: /skillManagement\.detail\.fileCount/ })
- await user.click(trigger)
+ await user.click(within(fileTree).getByRole('button', { name: 'common.operation.add' }))
}
export async function confirmUploadReview() {
diff --git a/web/features/skills/detail/builder-panel.tsx b/web/features/skills/detail/builder-panel.tsx
index 7cf354b2e52..766abead7eb 100644
--- a/web/features/skills/detail/builder-panel.tsx
+++ b/web/features/skills/detail/builder-panel.tsx
@@ -11,7 +11,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery, useQueryClient } from '@tanstack/react-query'
-import { useEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useId, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Markdown } from '@/app/components/base/markdown'
import {
@@ -298,6 +298,7 @@ export function SkillBuilderPanel({
}) {
const { t } = useTranslation('skill')
const queryClient = useQueryClient()
+ const titleId = useId()
const [prompt, setPrompt] = useState('')
const initialBuilderModeRef = useRef({
isEditMode: !isDefaultSkillBuilderDraft(detail),
@@ -803,7 +804,10 @@ export function SkillBuilderPanel({
}
return (
-
+
-
+
{t(($) => $['skillManagement.detail.builder.title'])}
@@ -1119,6 +1123,6 @@ export function SkillBuilderPanel({
-
+
)
}
diff --git a/web/features/skills/detail/file-editor.tsx b/web/features/skills/detail/file-editor.tsx
index ccb19b09196..0f7767bcb81 100644
--- a/web/features/skills/detail/file-editor.tsx
+++ b/web/features/skills/detail/file-editor.tsx
@@ -1075,7 +1075,7 @@ export function FileEditor({
}
return (
-
+
)}
-
+
)
}
diff --git a/web/features/skills/detail/file-tree.tsx b/web/features/skills/detail/file-tree.tsx
index 6a40d2cb225..80a4ab271c9 100644
--- a/web/features/skills/detail/file-tree.tsx
+++ b/web/features/skills/detail/file-tree.tsx
@@ -55,7 +55,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
import { formatForDisplay, matchesKeyboardEvent, useHotkey } from '@tanstack/react-hotkeys'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import copy from 'copy-to-clipboard'
-import { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react'
+import { useCallback, useEffect, useEffectEvent, useId, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon'
import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle'
@@ -175,6 +175,7 @@ export function FileTree({
const { t: tCommon } = useTranslation('common')
const queryClient = useQueryClient()
const sidebarRef = useRef(null)
+ const filesTitleId = useId()
const uploadInputRef = useRef(null)
const [inlineAction, setInlineAction] = useState()
const [draggingPaths, setDraggingPaths] = useState([])
@@ -1105,7 +1106,7 @@ export function FileTree({
const creatorName = detail?.created_by_name ?? detail?.created_by ?? '-'
if (collapsed && !sidebarFloating) {
return (
-
-
+
)
}
return (
<>
-
-
+
{t(
($) =>
fileCount === 1
@@ -1325,6 +1330,7 @@ export function FileTree({
{!readonly && (
$['operation.add'])}
className="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
disabled={!detail || isMutating}
>
@@ -1601,7 +1607,7 @@ export function FileTree({
-
+
>
)
}
diff --git a/web/features/skills/detail/shell.tsx b/web/features/skills/detail/shell.tsx
index 5c467a0ec06..c380b180207 100644
--- a/web/features/skills/detail/shell.tsx
+++ b/web/features/skills/detail/shell.tsx
@@ -6,7 +6,7 @@ import { SkillBuilderGridTexture } from './builder-grid-texture'
export function DetailSkeleton() {
return (
-
+
@@ -38,9 +38,9 @@ export function DetailSkeleton() {
-
+
-
@@ -68,9 +68,9 @@ export function DetailSkeleton() {
-
+
-
@@ -99,7 +99,7 @@ export function DetailSkeleton() {
-
+
)
}
diff --git a/web/features/skills/detail/version-panel.tsx b/web/features/skills/detail/version-panel.tsx
index 1c1d8c39e6c..207733753d6 100644
--- a/web/features/skills/detail/version-panel.tsx
+++ b/web/features/skills/detail/version-panel.tsx
@@ -33,7 +33,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
-import { useState } from 'react'
+import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useTimestamp from '@/hooks/use-timestamp'
import { consoleQuery } from '@/service/console'
@@ -506,6 +506,7 @@ export function VersionPanel({
const { t } = useTranslation('skill')
const { t: tWorkflow } = useTranslation('workflow')
const [filterValue, setFilterValue] = useState('all')
+ const titleId = useId()
const filteredVersions = versions.filter((version) => {
if (filterValue === 'onlyNamed') return !!version.version_name
@@ -513,10 +514,10 @@ export function VersionPanel({
})
return (
-
+
-
+
{t(($) => $['skillManagement.detail.versions'])}
@@ -574,6 +575,6 @@ export function VersionPanel({
-
+
)
}
diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json
index cb58e11eacb..9c6632d6811 100644
--- a/web/i18n/ar-TN/common.json
+++ b/web/i18n/ar-TN/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "تعيين النموذج الافتراضي لإدخال تحويل النص إلى كلام في المحادثة.",
"modelProvider.upgradeForLoadBalancing": "قم بترقية خطتك لتمكين موازنة التحميل.",
"modelProvider.used": "مستخدم",
+ "navigation.primary": "القائمة الرئيسية",
"navigation.skipToMain": "الانتقال إلى المحتوى الرئيسي",
"noData": "لا توجد بيانات",
"operation.add": "إضافة",
diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json
index 3eca0f3ce2f..c583056c60f 100644
--- a/web/i18n/de-DE/common.json
+++ b/web/i18n/de-DE/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Legen Sie das Standardmodell für die Text-zu-Sprache-Eingabe in Konversationen fest.",
"modelProvider.upgradeForLoadBalancing": "Aktualisieren Sie Ihren Plan, um den Lastenausgleich zu aktivieren.",
"modelProvider.used": "verwendet",
+ "navigation.primary": "Hauptmenü",
"navigation.skipToMain": "Zum Hauptinhalt springen",
"noData": "Keine Daten",
"operation.add": "Hinzufügen",
diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json
index f7f58407bab..46aecf806ea 100644
--- a/web/i18n/en-US/common.json
+++ b/web/i18n/en-US/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.",
"modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.",
"modelProvider.used": "used",
+ "navigation.primary": "Main menu",
"navigation.skipToMain": "Skip to main content",
"noData": "No data",
"operation.add": "Add",
diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json
index 87bba9f0858..f323f0b3a7a 100644
--- a/web/i18n/es-ES/common.json
+++ b/web/i18n/es-ES/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Establece el modelo predeterminado para la entrada de texto a voz en la conversación.",
"modelProvider.upgradeForLoadBalancing": "Actualiza tu plan para habilitar el Balanceo de Carga.",
"modelProvider.used": "usados",
+ "navigation.primary": "Menú principal",
"navigation.skipToMain": "Saltar al contenido principal",
"noData": "Sin datos",
"operation.add": "Agregar",
diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json
index 69c94939366..af9f507a995 100644
--- a/web/i18n/fa-IR/common.json
+++ b/web/i18n/fa-IR/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "مدل پیشفرض را برای ورودی متن به گفتار در مکالمه تنظیم کنید.",
"modelProvider.upgradeForLoadBalancing": "برای فعال کردن تعادل بار، طرح خود را ارتقا دهید.",
"modelProvider.used": "استفادهشده",
+ "navigation.primary": "منوی اصلی",
"navigation.skipToMain": "پرش به محتوای اصلی",
"noData": "بدون داده",
"operation.add": "افزودن",
diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json
index 4640a108c3a..81174aa8c38 100644
--- a/web/i18n/fr-FR/common.json
+++ b/web/i18n/fr-FR/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Définissez le modèle par défaut pour l'entrée de texte à la parole dans une conversation.",
"modelProvider.upgradeForLoadBalancing": "Mettez à niveau votre plan pour activer l’équilibrage de charge.",
"modelProvider.used": "utilisés",
+ "navigation.primary": "Menu principal",
"navigation.skipToMain": "Aller au contenu principal",
"noData": "Aucune donnée",
"operation.add": "Ajouter",
diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json
index 275177d98f1..2e3dd49ffe6 100644
--- a/web/i18n/hi-IN/common.json
+++ b/web/i18n/hi-IN/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "संवाद में पाठ-से-भाषण इनपुट के लिए डिफ़ॉल्ट मॉडल सेट करें।",
"modelProvider.upgradeForLoadBalancing": "लोड बैलेंसिंग सक्षम करने के लिए अपनी योजना अपग्रेड करें।",
"modelProvider.used": "उपयोग किया गया",
+ "navigation.primary": "मुख्य मेन्यू",
"navigation.skipToMain": "मुख्य सामग्री पर जाएं",
"noData": "कोई डेटा नहीं",
"operation.add": "जोड़ें",
diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json
index 586dc35e203..7d07124d432 100644
--- a/web/i18n/id-ID/common.json
+++ b/web/i18n/id-ID/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Atur model default untuk input teks-ke-ucapan dalam percakapan.",
"modelProvider.upgradeForLoadBalancing": "Tingkatkan paket Anda untuk mengaktifkan Penyeimbangan Beban.",
"modelProvider.used": "digunakan",
+ "navigation.primary": "Menu utama",
"navigation.skipToMain": "Lewati ke konten utama",
"noData": "Tidak ada data",
"operation.add": "Tambah",
diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json
index eab7932c4e4..1c64dab4cc2 100644
--- a/web/i18n/it-IT/common.json
+++ b/web/i18n/it-IT/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Imposta il modello predefinito per l'input da testo a voce nella conversazione.",
"modelProvider.upgradeForLoadBalancing": "Aggiorna il tuo piano per abilitare il Bilanciamento del Carico.",
"modelProvider.used": "utilizzati",
+ "navigation.primary": "Menu principale",
"navigation.skipToMain": "Vai al contenuto principale",
"noData": "Nessun dato",
"operation.add": "Aggiungi",
diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json
index 364681fcde2..84bc0b27994 100644
--- a/web/i18n/ja-JP/common.json
+++ b/web/i18n/ja-JP/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "会話でのテキスト-to-音声入力に使用するデフォルトモデルを設定します。",
"modelProvider.upgradeForLoadBalancing": "負荷分散を利用するには、プランのアップグレードが必要です。",
"modelProvider.used": "使用済み",
+ "navigation.primary": "メインメニュー",
"navigation.skipToMain": "メインコンテンツへスキップ",
"noData": "データなし",
"operation.add": "追加",
diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json
index 31dfd475da1..3dd3122a045 100644
--- a/web/i18n/ko-KR/common.json
+++ b/web/i18n/ko-KR/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "대화에서의 텍스트-to-음성 입력에 사용되는 기본 모델을 설정합니다.",
"modelProvider.upgradeForLoadBalancing": "로드 밸런싱을 사용하도록 계획을 업그레이드합니다.",
"modelProvider.used": "사용됨",
+ "navigation.primary": "주 메뉴",
"navigation.skipToMain": "본문으로 건너뛰기",
"noData": "데이터 없음",
"operation.add": "추가",
diff --git a/web/i18n/lo-LA/common.json b/web/i18n/lo-LA/common.json
index 55b0e58ecd2..14ee09c918f 100644
--- a/web/i18n/lo-LA/common.json
+++ b/web/i18n/lo-LA/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "ຕັ້ງຄ່າໂມເດວເລີ່ມຕົ້ນສຳລັບການປ່ຽນຂໍ້ຄວາມເປັນສຽງໃນການສົນທະນາ.",
"modelProvider.upgradeForLoadBalancing": "ອັບເກຣດແຜນຂອງທ່ານເພື່ອເປີດໃຊ້ Load Balancing.",
"modelProvider.used": "ໃຊ້ໄປແລ້ວ",
+ "navigation.primary": "ເມນູຫຼັກ",
"navigation.skipToMain": "ຂ້າມໄປເນື້ອຫາຫຼັກ",
"noData": "ບໍ່ມີຂໍ້ມູນ",
"operation.add": "ເພີ່ມ",
diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json
index 52fb04cec72..940df2dcee4 100644
--- a/web/i18n/nl-NL/common.json
+++ b/web/i18n/nl-NL/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.",
"modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.",
"modelProvider.used": "gebruikt",
+ "navigation.primary": "Hoofdmenu",
"navigation.skipToMain": "Naar hoofdinhoud springen",
"noData": "No data",
"operation.add": "Add",
diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json
index 9068c36ff1d..94e6a92e7a7 100644
--- a/web/i18n/pl-PL/common.json
+++ b/web/i18n/pl-PL/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Ustaw domyślny model dla konwersji tekstu na mowę w rozmowach.",
"modelProvider.upgradeForLoadBalancing": "Uaktualnij swój plan, aby włączyć równoważenie obciążenia.",
"modelProvider.used": "wykorzystano",
+ "navigation.primary": "Menu główne",
"navigation.skipToMain": "Przejdź do głównej treści",
"noData": "Brak danych",
"operation.add": "Dodaj",
diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json
index 2a603c397c5..f62115dca2d 100644
--- a/web/i18n/pt-BR/common.json
+++ b/web/i18n/pt-BR/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Defina o modelo padrão para entrada de texto para fala na conversa.",
"modelProvider.upgradeForLoadBalancing": "Atualize seu plano para habilitar o balanceamento de carga.",
"modelProvider.used": "usados",
+ "navigation.primary": "Menu principal",
"navigation.skipToMain": "Pular para o conteúdo principal",
"noData": "Sem dados",
"operation.add": "Adicionar",
diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json
index 18c8e7ac479..8fe4a66677b 100644
--- a/web/i18n/ro-RO/common.json
+++ b/web/i18n/ro-RO/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Setați modelul implicit pentru intrarea de conversie vorbire-la-text în conversație.",
"modelProvider.upgradeForLoadBalancing": "Actualizați-vă planul pentru a activa Load Balancing.",
"modelProvider.used": "utilizate",
+ "navigation.primary": "Meniu principal",
"navigation.skipToMain": "Sări la conținutul principal",
"noData": "Fără date",
"operation.add": "Adaugă",
diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json
index 2ed10ac7214..e9518fc0c85 100644
--- a/web/i18n/ru-RU/common.json
+++ b/web/i18n/ru-RU/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Установите модель по умолчанию для ввода текста в речь в разговоре.",
"modelProvider.upgradeForLoadBalancing": "Обновите свой тарифный план, чтобы включить балансировку нагрузки.",
"modelProvider.used": "использовано",
+ "navigation.primary": "Главное меню",
"navigation.skipToMain": "Перейти к основному содержанию",
"noData": "Нет данных",
"operation.add": "Добавить",
diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json
index b2d67f7400d..b52e8034294 100644
--- a/web/i18n/sl-SI/common.json
+++ b/web/i18n/sl-SI/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Nastavite privzeti model za pretvorbo besedila v govor v pogovoru.",
"modelProvider.upgradeForLoadBalancing": "Nadgradite svoj načrt, da omogočite uravnoteženje obremenitev.",
"modelProvider.used": "porabljeno",
+ "navigation.primary": "Glavni meni",
"navigation.skipToMain": "Preskoči na glavno vsebino",
"noData": "Ni podatkov",
"operation.add": "Dodaj",
diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json
index ebe96553cb6..484ba053ea6 100644
--- a/web/i18n/th-TH/common.json
+++ b/web/i18n/th-TH/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "ตั้งค่าโมเดลเริ่มต้นสําหรับการป้อนข้อมูลเป็นข้อความเป็นคําพูดในการสนทนา",
"modelProvider.upgradeForLoadBalancing": "อัปเกรดแผนของคุณเพื่อเปิดใช้งานการปรับสมดุลโหลด",
"modelProvider.used": "ใช้แล้ว",
+ "navigation.primary": "เมนูหลัก",
"navigation.skipToMain": "ข้ามไปยังเนื้อหาหลัก",
"noData": "ไม่มีข้อมูล",
"operation.add": "เพิ่ม",
diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json
index 1a979ec22ea..1e77c007587 100644
--- a/web/i18n/tr-TR/common.json
+++ b/web/i18n/tr-TR/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Konuşmada metinden konuşmaya giriş için varsayılan modeli ayarlayın.",
"modelProvider.upgradeForLoadBalancing": "Yük Dengelemeyi etkinleştirmek için planınızı yükseltin.",
"modelProvider.used": "kullanıldı",
+ "navigation.primary": "Ana menü",
"navigation.skipToMain": "Ana içeriğe geç",
"noData": "Veri yok",
"operation.add": "Ekle",
diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json
index a97f8aacce3..13cbc6f247e 100644
--- a/web/i18n/uk-UA/common.json
+++ b/web/i18n/uk-UA/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Встановіть модель за замовчуванням для введення тексту в мовлення в розмові.",
"modelProvider.upgradeForLoadBalancing": "Оновіть свій план, щоб увімкнути балансування навантаження.",
"modelProvider.used": "використано",
+ "navigation.primary": "Головне меню",
"navigation.skipToMain": "Перейти до основного вмісту",
"noData": "Немає даних",
"operation.add": "Додати",
diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json
index 300eb97cbbc..3187f278e78 100644
--- a/web/i18n/vi-VN/common.json
+++ b/web/i18n/vi-VN/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "Thiết lập mô hình mặc định cho đầu vào văn bản thành tiếng nói trong cuộc trò chuyện.",
"modelProvider.upgradeForLoadBalancing": "Nâng cấp gói của bạn để bật Cân bằng tải.",
"modelProvider.used": "đã dùng",
+ "navigation.primary": "Menu chính",
"navigation.skipToMain": "Chuyển đến nội dung chính",
"noData": "Không có dữ liệu",
"operation.add": "Thêm",
diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json
index 26552a33ee8..e50818cf9a1 100644
--- a/web/i18n/zh-Hans/common.json
+++ b/web/i18n/zh-Hans/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "设置对话中文字转语音输出的默认使用模型。",
"modelProvider.upgradeForLoadBalancing": "升级以解锁负载均衡功能",
"modelProvider.used": "已用",
+ "navigation.primary": "主菜单",
"navigation.skipToMain": "跳转到主要内容",
"noData": "暂无数据",
"operation.add": "添加",
diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json
index 7b001d7afb5..b59a431d51f 100644
--- a/web/i18n/zh-Hant/common.json
+++ b/web/i18n/zh-Hant/common.json
@@ -441,6 +441,7 @@
"modelProvider.ttsModel.tip": "設定對話中文字轉語音輸出的預設使用模型。",
"modelProvider.upgradeForLoadBalancing": "升級您的計劃以啟用 Load Balancing。",
"modelProvider.used": "已用",
+ "navigation.primary": "主選單",
"navigation.skipToMain": "跳至主要內容",
"noData": "無資料",
"operation.add": "新增",