refactor(web): use native scrolling for top-level page content (#45596)

This commit is contained in:
yyh authored and GitHub committed 2026-11-05 03:07:53 +00:00
1 parent faddecf1d8
commit 703cf36b87
10 files changed
+298 -429

No files matched your search

-20
View File
@@ -3017,11 +3017,6 @@
"count": 2
}
},
"web/app/components/integrations/index.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 4
}
},
"web/app/components/integrations/sidebar-nav-item.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 2
@@ -3463,11 +3458,6 @@
"count": 2
}
},
"web/app/components/plugins/plugin-page/plugins-panel-results.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 1
}
},
"web/app/components/plugins/plugin-tags-filter/compact-trigger.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 4
@@ -6451,11 +6441,6 @@
"count": 2
}
},
"web/features/agent-v2/roster/components/agent-roster-list.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 5
}
},
"web/features/agent-v2/roster/components/agent-workflow-references-menu.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 1
@@ -6776,11 +6761,6 @@
"count": 13
}
},
"web/features/skills/page.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 4
}
},
"web/features/tag-management/components/tag-filter.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 3
+38 -58
View File
@@ -4,13 +4,6 @@ import type { GetAppsData } from '@dify/contracts/api/console/apps/types.gen'
import type { RecommendedAppResponse } from '@dify/contracts/api/console/explore/types.gen'
import type { AppListCreationDialog } from './app-list-creation-modals'
import type { AppListUrlQuery } from './query-params'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useDebounce } from 'ahooks'
import { useAtomValue } from 'jotai'
import { throttle, useQueryStates } from 'nuqs'
@@ -132,57 +125,44 @@ export function List({ onCreateLearnDify, onTryLearnDify }: Props) {
<div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2"></div>
)}
<AppListHeader
titleId={titleId}
category={category}
tagIDs={tagIDs}
keywords={keywords}
creatorIDs={creatorIDs}
sortBy={sortBy}
onCategoryChange={changeCategory}
onTagIDsChange={changeTagIDs}
onKeywordsChange={changeKeywords}
onCreatorIDsChange={changeCreatorIDs}
onSortByChange={changeSortBy}
onCreateBlank={openCreateBlankModal}
onCreateTemplate={openCreateTemplateDialog}
onImportDSL={openCreateFromDSLModal}
onOpenTagManagement={openTagManagement}
showCreateButton={canCreateApp}
/>
<div className="relative min-h-0 grow">
<ScrollArea className="size-full overflow-hidden">
<ScrollAreaViewport
ref={scrollViewportRef}
role="region"
aria-labelledby={titleId}
className="overscroll-contain"
style={{ overflowX: 'hidden' }}
>
<ScrollAreaContent
className="flex min-h-full w-full max-w-full flex-col"
style={{ minWidth: 0 }}
>
<AppListCatalog
appListQuery={appListQuery}
canCreateApp={canCreateApp}
dragging={dragging}
hasActiveFilters={hasActiveFilters}
onCreateBlank={openCreateBlankModal}
onCreateLearnDify={onCreateLearnDify}
onCreateTemplate={openCreateTemplateDialog}
onImportDSL={openCreateFromDSLModal}
onOpenTagManagement={openTagManagement}
onTryLearnDify={onTryLearnDify}
scrollViewportRef={scrollViewportRef}
/>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div
ref={scrollViewportRef}
role="region"
aria-labelledby={titleId}
tabIndex={-1}
className="flex min-h-0 grow flex-col overflow-x-hidden overflow-y-auto overscroll-contain outline-hidden"
>
<AppListHeader
titleId={titleId}
category={category}
tagIDs={tagIDs}
keywords={keywords}
creatorIDs={creatorIDs}
sortBy={sortBy}
onCategoryChange={changeCategory}
onTagIDsChange={changeTagIDs}
onKeywordsChange={changeKeywords}
onCreatorIDsChange={changeCreatorIDs}
onSortByChange={changeSortBy}
onCreateBlank={openCreateBlankModal}
onCreateTemplate={openCreateTemplateDialog}
onImportDSL={openCreateFromDSLModal}
onOpenTagManagement={openTagManagement}
showCreateButton={canCreateApp}
/>
<AppListCatalog
appListQuery={appListQuery}
canCreateApp={canCreateApp}
dragging={dragging}
hasActiveFilters={hasActiveFilters}
onCreateBlank={openCreateBlankModal}
onCreateLearnDify={onCreateLearnDify}
onCreateTemplate={openCreateTemplateDialog}
onImportDSL={openCreateFromDSLModal}
onOpenTagManagement={openTagManagement}
onTryLearnDify={onTryLearnDify}
scrollViewportRef={scrollViewportRef}
/>
</div>
<CheckResultToast />
+24 -38
View File
@@ -19,13 +19,6 @@ import {
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { Suspense, useId, useRef, useState, useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import { UpdateSettingDialog } from '@/app/components/header/account-setting/update-setting-dialog'
@@ -278,7 +271,7 @@ export default function IntegrationsPage({
'flex shrink-0 flex-col bg-components-panel-bg px-2 py-2 transition-[width]',
isCompact
? 'h-full w-full items-stretch overflow-y-auto'
: 'w-50 items-end border-r border-divider-burn',
: 'w-50 items-end border-e border-divider-burn',
)}
data-step-by-step-tour-target={STEP_BY_STEP_TOUR_TARGETS.integration}
>
@@ -287,7 +280,7 @@ export default function IntegrationsPage({
>
<div
className={cn(
'flex shrink-0 items-start pr-0 pl-2.5',
'flex shrink-0 items-start ps-2.5 pe-0',
reserveInstallActionSlot ? 'h-14 pt-1 pb-7' : 'mb-3 pt-1 pb-0.5',
)}
>
@@ -328,7 +321,7 @@ export default function IntegrationsPage({
>
{toolsNavItemContent}
</CollapsibleTrigger>
<CollapsiblePanel className="relative space-y-px before:absolute before:-top-px before:bottom-0 before:left-[17.5px] before:w-px before:bg-divider-regular">
<CollapsiblePanel className="relative space-y-px before:absolute before:inset-s-[17.5px] before:-top-px before:bottom-0 before:w-px before:bg-divider-regular">
{toolItems.map((item) => (
<IntegrationSidebarNavItem
key={item.label}
@@ -434,34 +427,27 @@ export default function IntegrationsPage({
</Suspense>
</div>
) : (
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={scrollAreaLabel}
className="overscroll-contain"
role={scrollAreaLabel ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<Suspense key={section} fallback={null}>
<IntegrationSectionRenderer
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
</Suspense>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div
aria-label={scrollAreaLabel}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
role={scrollAreaLabel ? 'region' : undefined}
>
<Suspense key={section} fallback={null}>
<IntegrationSectionRenderer
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
</Suspense>
</div>
)}
</section>
</div>
@@ -1,13 +1,6 @@
'use client'
import type { ReactNode } from 'react'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
type IntegrationSectionLayoutProps = {
bodyClassName?: string
@@ -21,19 +14,12 @@ export function IntegrationSectionLayout({
label,
}: IntegrationSectionLayoutProps) {
return (
<ScrollArea className="min-h-0 flex-1 overflow-hidden max-md:min-h-48">
<ScrollAreaViewport
aria-label={label}
className="overscroll-contain"
role={label ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<div className={bodyClassName}>{children}</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div
aria-label={label}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain max-md:min-h-48"
role={label ? 'region' : undefined}
>
<div className={bodyClassName}>{children}</div>
</div>
)
}
@@ -4,13 +4,6 @@ import type { ToolCategory } from '@/app/components/integrations/routes'
import type { ToolsContentInset } from '@/app/components/tools/content-inset'
import type { Collection } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -198,69 +191,62 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
const body = (
<>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-hidden bg-components-panel-bg">
<ScrollArea className="min-h-0 grow overflow-hidden bg-components-panel-bg">
<ScrollAreaViewport
ref={containerRef}
aria-label={t(($) => $['menus.tools'], { ns: 'navigation' })}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className="flex min-h-full flex-col">
{activeTab !== 'mcp' && (
<ToolProviderGrid
activeTab={activeTab}
collections={filteredCollectionList}
currentProviderId={currentProviderId}
frameClassName={toolListFrameClassName}
getTagLabel={getTagLabel}
hasCategoryCollections={activeTabCollectionList.length > 0}
isLoading={isCollectionListLoading}
useIntegrationsCard={contentInset === 'compact'}
isSearchResultEmpty={isCollectionSearchEmpty}
showCreateCard={shouldShowCustomToolCreateCard}
onRefreshData={refetch}
onSelectProvider={setCurrentProviderId}
/>
)}
{!isCollectionListLoading &&
!activeTabCollectionList.length &&
activeTab === 'builtin' && (
<Empty
lightCard
text={t(($) => $.noTools, { ns: 'tools' })}
className={cn('h-56 shrink-0', toolListFrameClassName)}
/>
)}
{isCollectionSearchEmpty && activeTab === 'builtin' && (
<div className={cn('h-56 shrink-0', toolListFrameClassName)} />
)}
{marketplace && activeTab === 'builtin' && (
<BuiltinMarketplacePanel
marketplace={marketplace}
containerRef={containerRef}
contentInset={contentInset}
keywords={keywords}
tagFilterValue={tagFilterValue}
/>
)}
{activeTab === 'mcp' && (
<MCPList
providers={activeTabCollectionList}
isLoading={allToolProvidersQuery.isLoading}
searchText={keywords}
contentInset={contentInset}
createdProviderId={createdMCPProviderId}
showCreateCard={shouldShowMCPCreateCard}
onRefresh={refreshMcpTools}
onCreatedProviderHandled={handleCreatedMCPProviderHandled}
/>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div
ref={containerRef}
aria-label={t(($) => $['menus.tools'], { ns: 'navigation' })}
className="flex min-h-0 grow flex-col overflow-y-auto overscroll-contain bg-components-panel-bg"
role="region"
>
{activeTab !== 'mcp' && (
<ToolProviderGrid
activeTab={activeTab}
collections={filteredCollectionList}
currentProviderId={currentProviderId}
frameClassName={toolListFrameClassName}
getTagLabel={getTagLabel}
hasCategoryCollections={activeTabCollectionList.length > 0}
isLoading={isCollectionListLoading}
useIntegrationsCard={contentInset === 'compact'}
isSearchResultEmpty={isCollectionSearchEmpty}
showCreateCard={shouldShowCustomToolCreateCard}
onRefreshData={refetch}
onSelectProvider={setCurrentProviderId}
/>
)}
{!isCollectionListLoading &&
!activeTabCollectionList.length &&
activeTab === 'builtin' && (
<Empty
lightCard
text={t(($) => $.noTools, { ns: 'tools' })}
className={cn('h-56 shrink-0', toolListFrameClassName)}
/>
)}
{isCollectionSearchEmpty && activeTab === 'builtin' && (
<div className={cn('h-56 shrink-0', toolListFrameClassName)} />
)}
{marketplace && activeTab === 'builtin' && (
<BuiltinMarketplacePanel
marketplace={marketplace}
containerRef={containerRef}
contentInset={contentInset}
keywords={keywords}
tagFilterValue={tagFilterValue}
/>
)}
{activeTab === 'mcp' && (
<MCPList
providers={activeTabCollectionList}
isLoading={allToolProvidersQuery.isLoading}
searchText={keywords}
contentInset={contentInset}
createdProviderId={createdMCPProviderId}
showCreateCard={shouldShowMCPCreateCard}
onRefresh={refreshMcpTools}
onCreatedProviderHandled={handleCreatedMCPProviderHandled}
/>
)}
</div>
</div>
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail
@@ -6,13 +6,6 @@ import type { Collection } from '@/app/components/tools/types'
import type { MarketplaceLinks } from '@/features/marketplace/links'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
@@ -118,12 +111,7 @@ const PluginsPanelResults = ({
}, [autoLoadNextPage, containerRef, isFetching, isLastPage, loadNextPage])
return (
<ScrollArea
className={cn(
'min-h-0 grow self-stretch overflow-hidden bg-components-panel-bg',
contentFrameClassName,
)}
>
<>
<div role="status" aria-atomic="true" className="sr-only">
{isFetching
? t(($) => $.loading, { ns: 'common' })
@@ -134,85 +122,82 @@ const PluginsPanelResults = ({
(hasVisibleBuiltinTools ? filteredBuiltinTools.length : 0),
})}
</div>
<ScrollAreaViewport
<div
ref={containerRef}
aria-label={scrollAreaLabel}
className="overscroll-contain"
className={cn(
'flex min-h-0 grow flex-col self-stretch overflow-y-auto overscroll-contain bg-components-panel-bg',
hasEmbeddedMarketplace && 'pt-1',
contentFrameClassName,
)}
role={scrollAreaLabel ? 'region' : undefined}
>
<ScrollAreaContent
className={cn('flex min-h-full flex-col', hasEmbeddedMarketplace && 'pt-1')}
>
{showCategoryEmptyState && categoryEmptyState && (
<CategoryEmptyState
category={categoryEmptyState}
showMarketplaceLink={!!categoryMarketplace}
/>
)}
{(hasVisiblePlugins || hasVisibleBuiltinTools) && (
<List
pluginList={filteredList}
canDeletePlugin={canDeletePlugin}
canUpdatePlugin={canUpdatePlugin}
firstPluginTarget={firstPluginTarget}
>
{filteredBuiltinTools.map((collection, index) => (
<button
key={collection.id}
type="button"
aria-current={currentBuiltinToolID === collection.id ? 'true' : undefined}
className="min-w-0 cursor-pointer appearance-none border-0 bg-transparent p-0 text-left"
data-step-by-step-tour-target={
filteredList.length === 0 && index === 0 ? firstBuiltinToolTarget : undefined
}
onClick={() => onSelectBuiltinTool(collection.id)}
>
<IntegrationsToolProviderCard
collection={collection}
current={currentBuiltinToolID === collection.id}
showBuiltInBadge
/>
</button>
))}
</List>
)}
{!isLastPage && (
<div className="flex w-full justify-center py-4">
{isFetching ? (
<LoadingPlaceholder className="size-8" />
) : autoLoadNextPage ? null : (
<Button onClick={loadNextPage}>
{t(($) => $['common.loadMore'], { ns: 'workflow' })}
</Button>
)}
{autoLoadNextPage && (
<div ref={loadMoreAnchorRef} className="h-px w-full" aria-hidden />
)}
</div>
)}
{marketplace && hasToolMarketplacePanel && (
<BuiltinMarketplacePanel
marketplace={marketplace}
containerRef={containerRef}
contentInset={contentInset}
keywords={keywords}
tagFilterValue={tagFilterValue}
/>
)}
{marketplace && categoryMarketplace && (
<CategoryMarketplacePanel
marketplace={marketplace}
category={categoryMarketplace}
searchText={keywords}
tags={categoryMarketplace === 'trigger' ? tagFilterValue : []}
/>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
{showCategoryEmptyState && categoryEmptyState && (
<CategoryEmptyState
category={categoryEmptyState}
showMarketplaceLink={!!categoryMarketplace}
/>
)}
{(hasVisiblePlugins || hasVisibleBuiltinTools) && (
<List
pluginList={filteredList}
canDeletePlugin={canDeletePlugin}
canUpdatePlugin={canUpdatePlugin}
firstPluginTarget={firstPluginTarget}
>
{filteredBuiltinTools.map((collection, index) => (
<button
key={collection.id}
type="button"
aria-current={currentBuiltinToolID === collection.id ? 'true' : undefined}
className="min-w-0 cursor-pointer appearance-none border-0 bg-transparent p-0 text-start"
data-step-by-step-tour-target={
filteredList.length === 0 && index === 0 ? firstBuiltinToolTarget : undefined
}
onClick={() => onSelectBuiltinTool(collection.id)}
>
<IntegrationsToolProviderCard
collection={collection}
current={currentBuiltinToolID === collection.id}
showBuiltInBadge
/>
</button>
))}
</List>
)}
{!isLastPage && (
<div className="flex w-full justify-center py-4">
{isFetching ? (
<LoadingPlaceholder className="size-8" />
) : autoLoadNextPage ? null : (
<Button onClick={loadNextPage}>
{t(($) => $['common.loadMore'], { ns: 'workflow' })}
</Button>
)}
{autoLoadNextPage && (
<div ref={loadMoreAnchorRef} className="h-px w-full" aria-hidden />
)}
</div>
)}
{marketplace && hasToolMarketplacePanel && (
<BuiltinMarketplacePanel
marketplace={marketplace}
containerRef={containerRef}
contentInset={contentInset}
keywords={keywords}
tagFilterValue={tagFilterValue}
/>
)}
{marketplace && categoryMarketplace && (
<CategoryMarketplacePanel
marketplace={marketplace}
category={categoryMarketplace}
searchText={keywords}
tags={categoryMarketplace === 'trigger' ? tagFilterValue : []}
/>
)}
</div>
</>
)
}
@@ -180,7 +180,9 @@ it('focuses the next card without moving the roster scroll position', async () =
const targetName = 'Agent 15'
const { screen, queryClient, openDelete } = await setup(agents)
try {
const viewport = screen.getByRole('region', { name: 'Agents', exact: true }).element()
const viewport = screen
.getByRole('region', { name: 'Agents', exact: true })
.element().parentElement!
await expect.poll(() => viewport.scrollHeight).toBeGreaterThan(viewport.clientHeight)
viewport.scrollTo({ top: 600 })
await expect.poll(() => viewport.scrollTop).toBeGreaterThan(0)
@@ -1,8 +1,7 @@
'use client'
import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen'
import type { ScrollAreaViewportProps } from '@langgenius/dify-ui/scroll-area'
import type { RefObject } from 'react'
import type { ComponentProps, RefObject } from 'react'
import {
AlertDialog,
AlertDialogContent,
@@ -19,13 +18,6 @@ import {
} from '@langgenius/dify-ui/context-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@langgenius/dify-ui/menu'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useMutation } from '@tanstack/react-query'
import { useId, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -63,7 +55,7 @@ export type AgentRosterListState =
type AgentRosterListProps = {
state: AgentRosterListState
} & Pick<ScrollAreaViewportProps, 'aria-labelledby'>
} & Pick<ComponentProps<'div'>, 'aria-labelledby'>
type DeleteAgentDialogPayload = Pick<AgentAppPartial, 'id' | 'name'> & {
fallbackFocus: RefObject<HTMLElement | null>
@@ -106,9 +98,9 @@ function AgentRosterSkeleton() {
<SkeletonRectangle className="my-0 h-2 w-3/4 rounded-md opacity-10" />
</div>
</div>
<div className="flex items-center pt-2 pr-3 pb-3 pl-4">
<div className="flex items-center ps-4 pe-3 pt-2 pb-3">
<SkeletonRectangle className="my-0 h-3 w-6 rounded-md opacity-12" />
<SkeletonRectangle className="my-0 ml-2.5 h-3 w-28 rounded-md opacity-10" />
<SkeletonRectangle className="my-0 ms-2.5 h-3 w-28 rounded-md opacity-10" />
</div>
</div>
))}
@@ -412,7 +404,7 @@ function AgentRosterItem({
</div>
</div>
)}
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex min-w-0 items-center pt-2 pr-3 pb-3 pl-4 system-xs-regular text-text-tertiary">
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex min-w-0 items-center ps-4 pe-3 pt-2 pb-3 system-xs-regular text-text-tertiary">
<div className="flex min-w-0 flex-1 items-center gap-1.5">
{hasPublishedReferences ? (
<AgentWorkflowReferencesMenu
@@ -469,98 +461,88 @@ export function AgentRosterList({
const { t: tCommon } = useTranslation(['common'])
const isBusy = state.status === 'pending' || (state.status === 'ready' && state.isFetching)
const [deleteDialog] = useState(() => createAlertDialogHandle<DeleteAgentDialogPayload>())
const scrollViewportRef = useRef<HTMLDivElement>(null)
const regionRef = useRef<HTMLDivElement>(null)
const deletion = useMutation(
consoleQuery.agent.byAgentId.delete.mutationOptions({ context: { silent: true } }),
)
return (
<>
<ScrollArea className="h-full min-h-0 min-w-0 overflow-hidden">
<ScrollAreaViewport
ref={scrollViewportRef}
role="region"
aria-labelledby={ariaLabelledBy}
aria-busy={isBusy}
className="overscroll-contain"
style={{ overflowX: 'hidden' }}
>
<ScrollAreaContent
className="min-h-full w-full max-w-full px-2 pt-2 pb-8 sm:px-8"
style={{ minWidth: 0 }}
<div
ref={regionRef}
role="region"
aria-labelledby={ariaLabelledBy}
aria-busy={isBusy}
tabIndex={-1}
className="min-w-0 px-2 pt-2 pb-8 outline-hidden sm:px-8"
>
{state.status === 'pending' && (
<div className={AGENT_ROSTER_GRID_CLASS_NAME}>
<AgentRosterSkeleton />
</div>
)}
{state.status === 'error' && (
<AgentRosterPlaceholderState
onRetry={state.onRetry}
role="alert"
title={t(($) => $['roster.loadingError'], { ns: 'agentRoster' })}
/>
)}
{state.status === 'ready' && state.agents.length === 0 && (
<AgentRosterPlaceholderState
role={state.emptyState === 'filtered' ? 'status' : undefined}
title={
state.emptyState === 'filtered'
? t(($) => $['roster.emptySearch'], { ns: 'agentRoster' })
: t(($) => $['roster.empty'], { ns: 'agentRoster' })
}
/>
)}
{state.status === 'ready' && state.agents.length > 0 && (
<ul
// Safari list semantics: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/list-style#accessibility
// oxlint-disable-next-line jsx-a11y/no-redundant-roles -- Dify's preflight removes list markers.
role="list"
aria-label={t(($) => $['roster.listLabel'])}
className={AGENT_ROSTER_GRID_CLASS_NAME}
>
{state.status === 'pending' && (
<div className={AGENT_ROSTER_GRID_CLASS_NAME}>
<AgentRosterSkeleton />
</div>
)}
{state.status === 'error' && (
<AgentRosterPlaceholderState
onRetry={state.onRetry}
role="alert"
title={t(($) => $['roster.loadingError'], { ns: 'agentRoster' })}
{state.agents.map((agent) => (
<AgentRosterItem
key={agent.id}
agent={agent}
onDelete={(item) => {
deleteDialog.openWithPayload({
id: agent.id,
name: agent.name,
adjacentFocusTargets: getAdjacentFocusTargets(item, (sibling) =>
sibling.querySelector<HTMLElement>(':scope > a[href], :scope > button'),
),
fallbackFocus: { current: null },
})
}}
/>
)}
{state.status === 'ready' && state.agents.length === 0 && (
<AgentRosterPlaceholderState
role={state.emptyState === 'filtered' ? 'status' : undefined}
title={
state.emptyState === 'filtered'
? t(($) => $['roster.emptySearch'], { ns: 'agentRoster' })
: t(($) => $['roster.empty'], { ns: 'agentRoster' })
}
/>
)}
{state.status === 'ready' && state.agents.length > 0 && (
<ul
// Safari list semantics: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/list-style#accessibility
// oxlint-disable-next-line jsx-a11y/no-redundant-roles -- Dify's preflight removes list markers.
role="list"
aria-label={t(($) => $['roster.listLabel'])}
className={AGENT_ROSTER_GRID_CLASS_NAME}
>
{state.agents.map((agent) => (
<AgentRosterItem
key={agent.id}
agent={agent}
onDelete={(item) => {
deleteDialog.openWithPayload({
id: agent.id,
name: agent.name,
adjacentFocusTargets: getAdjacentFocusTargets(item, (sibling) =>
sibling.querySelector<HTMLElement>(':scope > a[href], :scope > button'),
),
fallbackFocus: { current: null },
})
}}
/>
))}
</ul>
)}
{state.status === 'ready' && state.footer.status === 'error' && (
<div
className="flex items-center justify-center gap-3 pt-1 system-xs-regular text-text-destructive"
role="alert"
>
<span>{t(($) => $['roster.loadingError'], { ns: 'agentRoster' })}</span>
<Button size="small" variant="secondary" onClick={state.footer.onRetry}>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
)}
{state.status === 'ready' && state.footer.status === 'load-more' && (
<div className="flex justify-center pt-1">
<Button loading={state.footer.isLoading} onClick={state.footer.onLoadMore}>
{t(($) => $['roster.loadMore'], { ns: 'agentRoster' })}
</Button>
</div>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
))}
</ul>
)}
{state.status === 'ready' && state.footer.status === 'error' && (
<div
className="flex items-center justify-center gap-3 pt-1 system-xs-regular text-text-destructive"
role="alert"
>
<span>{t(($) => $['roster.loadingError'], { ns: 'agentRoster' })}</span>
<Button size="small" variant="secondary" onClick={state.footer.onRetry}>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
)}
{state.status === 'ready' && state.footer.status === 'load-more' && (
<div className="flex justify-center pt-1">
<Button loading={state.footer.isLoading} onClick={state.footer.onLoadMore}>
{t(($) => $['roster.loadMore'], { ns: 'agentRoster' })}
</Button>
</div>
)}
</div>
<AlertDialog
handle={deleteDialog}
onOpenChange={(open, details) => {
@@ -593,7 +575,7 @@ export function AgentRosterList({
{ params: { agent_id: payload.id } },
{
onSuccess: () => {
payload.fallbackFocus.current = scrollViewportRef.current
payload.fallbackFocus.current = regionRef.current
deleteDialog.close()
},
},
+3 -5
View File
@@ -99,8 +99,8 @@ export default function RosterPage() {
useDocumentTitle(pageTitle)
return (
<div className="flex h-0 min-w-0 grow flex-col overflow-hidden bg-background-body max-md:overflow-y-auto">
<div className="shrink-0 bg-background-body px-2 pt-4 pb-2 sm:px-8">
<div className="flex h-0 min-w-0 grow flex-col overflow-x-hidden overflow-y-auto overscroll-contain bg-background-body">
<div className="sticky top-0 z-10 shrink-0 bg-background-body px-2 pt-4 pb-2 sm:px-8">
<div className="flex h-6 min-w-0 items-center justify-between gap-2 sm:gap-4">
<h1
id={pageTitleId}
@@ -130,9 +130,7 @@ export default function RosterPage() {
</div>
</div>
<div className="min-h-0 flex-1 max-md:min-h-64">
<AgentRosterList aria-labelledby={pageTitleId} state={listState} />
</div>
<AgentRosterList aria-labelledby={pageTitleId} state={listState} />
</div>
)
}
+14 -30
View File
@@ -31,13 +31,6 @@ import { Field, FieldLabel } from '@langgenius/dify-ui/field'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group'
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@langgenius/dify-ui/menu'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useQueryState } from 'nuqs'
@@ -201,7 +194,7 @@ function SkillPlaceholderState({
<button
type="button"
disabled={actions.creating || actions.importing}
className="flex w-full cursor-pointer items-center gap-3 overflow-hidden rounded-xl bg-components-button-secondary-bg px-3 py-2.5 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
className="flex w-full cursor-pointer items-center gap-3 overflow-hidden rounded-xl bg-components-button-secondary-bg px-3 py-2.5 text-start outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
onClick={actions.onCreate}
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-background-section">
@@ -225,7 +218,7 @@ function SkillPlaceholderState({
<button
type="button"
disabled={actions.creating || actions.importing}
className="flex w-full cursor-pointer items-center gap-3 overflow-hidden rounded-xl bg-components-button-secondary-bg px-3 py-2.5 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
className="flex w-full cursor-pointer items-center gap-3 overflow-hidden rounded-xl bg-components-button-secondary-bg px-3 py-2.5 text-start outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50"
onClick={actions.onImport}
>
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-background-section">
@@ -590,7 +583,7 @@ function SkillCard({
</Menu>
</div>
</div>
<div className="pointer-events-none absolute top-26 right-3 left-3 flex h-6.5 min-w-0 items-start">
<div className="pointer-events-none absolute inset-s-3 inset-e-3 top-26 flex h-6.5 min-w-0 items-start">
<div className="pointer-events-auto w-full min-w-0">
<SkillCardTags
skillId={skill.id}
@@ -1207,8 +1200,15 @@ export default function SkillsPage() {
}
return (
<div className="flex h-0 min-w-0 grow flex-col overflow-hidden bg-background-body">
<div className="shrink-0 bg-background-body px-8 pt-4 pb-2">
<div
role="region"
aria-labelledby={titleId}
ref={listViewportRef}
tabIndex={-1}
className="flex h-0 min-w-0 grow flex-col overflow-x-hidden overflow-y-auto overscroll-contain bg-background-body outline-hidden"
onScroll={handleListScroll}
>
<div className="sticky top-0 z-10 shrink-0 bg-background-body px-8 pt-4 pb-2">
<div className="flex h-6 min-w-0 items-center justify-between gap-4">
<h1
id={titleId}
@@ -1229,24 +1229,8 @@ export default function SkillsPage() {
</div>
</div>
<div className="min-h-0 flex-1">
<ScrollArea className="h-full min-h-0 min-w-0 overflow-hidden">
<ScrollAreaViewport
role="region"
aria-labelledby={titleId}
ref={listViewportRef}
tabIndex={-1}
className="overscroll-contain"
onScroll={handleListScroll}
>
<ScrollAreaContent className="min-h-full px-8 pt-2 pb-8">
<SkillGrid state={skillGridState} />
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div className="flex-1 px-8 pt-2 pb-8">
<SkillGrid state={skillGridState} />
</div>
<Dialog
open={importOpen}