mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 05:41:09 +08:00
refactor(web): use native scrolling for top-level page content (#45596)
This commit is contained in:
10 files changed
+298
-429
No files matched your search
Generated
-20
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user