mirror of
https://github.com/langgenius/dify.git
synced 2026-08-01 09:50:50 +08:00
refactor(ui): simplify scroll area API (#39857)
This commit is contained in:
parent
c62ab071eb
commit
7e30aba12a
@ -19,8 +19,8 @@ import { Field, FieldControl, FieldDescription, FieldError, FieldLabel } from '.
|
||||
import { Form } from '../form'
|
||||
import { Input } from '../input'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -334,7 +334,7 @@ const OutsideScrollingContentDemo = () => {
|
||||
<DialogPortal>
|
||||
<DialogBackdrop className="duration-600 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-350 data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)]" />
|
||||
<DialogViewport className="group/dialog">
|
||||
<ScrollAreaRoot className="h-full overscroll-contain group-data-ending-style/dialog:pointer-events-none">
|
||||
<ScrollArea className="h-full group-data-ending-style/dialog:pointer-events-none">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Scrollable dialog viewport"
|
||||
role="region"
|
||||
@ -359,7 +359,7 @@ const OutsideScrollingContentDemo = () => {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</DialogViewport>
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
@ -418,11 +418,11 @@ const InsideScrollingContentDemo = () => {
|
||||
title="Release notes"
|
||||
description="Highlights from the latest workspace update."
|
||||
/>
|
||||
<ScrollAreaRoot className="relative flex min-h-0 flex-auto overflow-hidden">
|
||||
<ScrollArea className="relative flex min-h-0 flex-auto overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Release note improvements"
|
||||
role="region"
|
||||
className="h-full max-h-full max-w-full overflow-y-auto overscroll-contain"
|
||||
className="h-full max-h-full max-w-full overscroll-contain"
|
||||
>
|
||||
<ScrollAreaContent>
|
||||
<ReleaseNoteSections />
|
||||
@ -431,7 +431,7 @@ const InsideScrollingContentDemo = () => {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
<ReleaseNoteFooter onClose={() => setOpen(false)} />
|
||||
</DialogPopup>
|
||||
</DialogViewport>
|
||||
|
||||
@ -23,8 +23,8 @@ import { Button } from '../button'
|
||||
import { cn } from '../cn'
|
||||
import { Input } from '../input'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -723,16 +723,19 @@ export const MobileNavigation: Story = {
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="fixed" />
|
||||
<DrawerViewport className="group">
|
||||
<ScrollAreaRoot
|
||||
<ScrollArea
|
||||
style={{ position: undefined }}
|
||||
className="h-full overscroll-contain transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] group-data-ending-style:pointer-events-none group-data-starting-style:translate-y-[100dvh] motion-reduce:transition-none"
|
||||
className="h-full transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] group-data-ending-style:pointer-events-none group-data-starting-style:translate-y-[100dvh] motion-reduce:transition-none"
|
||||
>
|
||||
<ScrollAreaViewport
|
||||
className="size-full touch-auto overscroll-contain"
|
||||
role="region"
|
||||
aria-label="Mobile drawer viewport"
|
||||
>
|
||||
<ScrollAreaContent className="flex min-h-full min-w-0 items-end justify-center pt-8 min-[42rem]:px-16 min-[42rem]:py-16">
|
||||
<ScrollAreaContent
|
||||
style={{ minWidth: 0 }}
|
||||
className="flex min-h-full items-end justify-center pt-8 min-[42rem]:px-16 min-[42rem]:py-16"
|
||||
>
|
||||
<DrawerPopup className="relative w-full max-w-2xl touch-auto overflow-visible transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] data-ending-style:duration-350 data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)] data-swiping:select-none data-[swipe-direction=down]:inset-auto data-[swipe-direction=down]:max-h-none data-[swipe-direction=down]:transform-[translateY(var(--drawer-swipe-movement-y,0px))] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(max(100dvh,100%)+2px))] motion-reduce:transition-none min-[42rem]:rounded-2xl min-[42rem]:border-[0.5px] min-[42rem]:border-b-[0.5px]">
|
||||
<nav
|
||||
aria-label="Mobile navigation"
|
||||
@ -789,7 +792,7 @@ export const MobileNavigation: Story = {
|
||||
<ScrollAreaScrollbar orientation="vertical">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</DrawerViewport>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
|
||||
@ -1,188 +0,0 @@
|
||||
// Compile-time public API contracts; included by package typecheck and excluded from package exports.
|
||||
import type {
|
||||
AutocompleteCollection,
|
||||
AutocompleteCollectionProps,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupProps,
|
||||
AutocompleteItemProps,
|
||||
AutocompleteList,
|
||||
AutocompleteListProps,
|
||||
AutocompleteProps,
|
||||
} from './autocomplete'
|
||||
import type {
|
||||
ComboboxCollection,
|
||||
ComboboxCollectionProps,
|
||||
ComboboxGroup,
|
||||
ComboboxGroupProps,
|
||||
ComboboxItemProps,
|
||||
ComboboxList,
|
||||
ComboboxListProps,
|
||||
ComboboxProps,
|
||||
ComboboxValue,
|
||||
ComboboxValueProps,
|
||||
} from './combobox'
|
||||
import type { ContextMenuRadioGroupProps, ContextMenuRadioItemProps } from './context-menu'
|
||||
import type { DialogHandle, DialogTriggerProps } from './dialog'
|
||||
import type { DrawerHandle, DrawerTriggerProps } from './drawer'
|
||||
import type { DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps } from './dropdown-menu'
|
||||
import type { FormProps } from './form'
|
||||
import type { PopoverHandle, PopoverTriggerProps } from './popover'
|
||||
import type { PreviewCardHandle, PreviewCardTriggerProps } from './preview-card'
|
||||
import type { RadioGroupProps, RadioItemProps } from './radio'
|
||||
import type { SelectItemProps, SelectProps, SelectValue, SelectValueProps } from './select'
|
||||
import type { SliderRootProps } from './slider'
|
||||
|
||||
type Equal<Left, Right> =
|
||||
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
|
||||
? true
|
||||
: false
|
||||
type Expect<Condition extends true> = Condition
|
||||
type Arguments<Callback> = Callback extends (...args: infer Values) => unknown ? Values : never
|
||||
type FirstArgument<Callback> = Arguments<Callback>[0]
|
||||
type SecondArgument<Callback> = Arguments<Callback>[1]
|
||||
type IsRequired<Props, Key extends keyof Props> =
|
||||
Record<never, never> extends Pick<Props, Key> ? false : true
|
||||
type Callable<Member> = Member extends (...args: infer Values) => infer Result
|
||||
? (...args: Values) => Result
|
||||
: never
|
||||
|
||||
type DomainValue = { id: string }
|
||||
type FormValues = { enabled: boolean; name: string }
|
||||
type RangeValue = readonly [number, number]
|
||||
type OverlayPayload = { sourceId: string }
|
||||
type SelectSingleValueProps = Parameters<typeof SelectValue<DomainValue>>[0]
|
||||
type SelectMultipleValueProps = Parameters<typeof SelectValue<DomainValue, true>>[0]
|
||||
type SelectDynamicValueProps = Parameters<typeof SelectValue<DomainValue, boolean>>[0]
|
||||
type ComboboxSingleValueProps = Parameters<typeof ComboboxValue<DomainValue>>[0]
|
||||
type ComboboxMultipleValueProps = Parameters<typeof ComboboxValue<DomainValue, true>>[0]
|
||||
type ComboboxDynamicValueProps = Parameters<typeof ComboboxValue<DomainValue, boolean>>[0]
|
||||
type AutocompleteListRuntimeProps = Parameters<typeof AutocompleteList<DomainValue>>[0]
|
||||
type AutocompleteCollectionRuntimeProps = Parameters<typeof AutocompleteCollection<DomainValue>>[0]
|
||||
type AutocompleteGroupRuntimeProps = Parameters<typeof AutocompleteGroup<DomainValue>>[0]
|
||||
type ComboboxListRuntimeProps = Parameters<typeof ComboboxList<DomainValue>>[0]
|
||||
type ComboboxCollectionRuntimeProps = Parameters<typeof ComboboxCollection<DomainValue>>[0]
|
||||
type ComboboxGroupRuntimeProps = Parameters<typeof ComboboxGroup<DomainValue>>[0]
|
||||
|
||||
type PublicTypeAssertions = [
|
||||
Expect<Equal<FirstArgument<NonNullable<FormProps<FormValues>['onFormSubmit']>>, FormValues>>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<SelectProps<DomainValue, false>['onValueChange']>>,
|
||||
DomainValue | null
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<SelectProps<DomainValue, true>['onValueChange']>>,
|
||||
DomainValue[]
|
||||
>
|
||||
>,
|
||||
Expect<Equal<IsRequired<SelectProps<DomainValue, true>, 'multiple'>, true>>,
|
||||
Expect<Equal<IsRequired<SelectProps<DomainValue, boolean>, 'multiple'>, false>>,
|
||||
Expect<Equal<SelectItemProps<DomainValue>['value'], DomainValue | undefined>>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<Callable<SelectValueProps<DomainValue, true>['children']>>,
|
||||
DomainValue[] | null
|
||||
>
|
||||
>,
|
||||
Expect<Equal<FirstArgument<Callable<SelectSingleValueProps['children']>>, DomainValue | null>>,
|
||||
Expect<
|
||||
Equal<FirstArgument<Callable<SelectMultipleValueProps['children']>>, DomainValue[] | null>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<Callable<SelectDynamicValueProps['children']>>,
|
||||
DomainValue | DomainValue[] | null
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<ComboboxProps<DomainValue, true>['onValueChange']>>,
|
||||
DomainValue[]
|
||||
>
|
||||
>,
|
||||
Expect<Equal<IsRequired<ComboboxProps<DomainValue, true>, 'multiple'>, true>>,
|
||||
Expect<Equal<IsRequired<ComboboxProps<DomainValue, boolean>, 'multiple'>, false>>,
|
||||
Expect<Equal<ComboboxItemProps<DomainValue>['value'], DomainValue | undefined>>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<Callable<ComboboxValueProps<DomainValue, true>['children']>>,
|
||||
DomainValue[] | null
|
||||
>
|
||||
>,
|
||||
Expect<Equal<FirstArgument<Callable<ComboboxSingleValueProps['children']>>, DomainValue | null>>,
|
||||
Expect<
|
||||
Equal<FirstArgument<Callable<ComboboxMultipleValueProps['children']>>, DomainValue[] | null>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<Callable<ComboboxDynamicValueProps['children']>>,
|
||||
DomainValue | DomainValue[] | null
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<AutocompleteProps<DomainValue>['onItemHighlighted']>>,
|
||||
DomainValue | undefined
|
||||
>
|
||||
>,
|
||||
Expect<Equal<AutocompleteItemProps<DomainValue>['value'], DomainValue | undefined>>,
|
||||
Expect<
|
||||
Equal<FirstArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, DomainValue>
|
||||
>,
|
||||
Expect<Equal<SecondArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, number>>,
|
||||
Expect<Equal<FirstArgument<Callable<AutocompleteListRuntimeProps['children']>>, DomainValue>>,
|
||||
Expect<Equal<FirstArgument<AutocompleteCollectionProps<DomainValue>['children']>, DomainValue>>,
|
||||
Expect<Equal<SecondArgument<AutocompleteCollectionProps<DomainValue>['children']>, number>>,
|
||||
Expect<Equal<FirstArgument<AutocompleteCollectionRuntimeProps['children']>, DomainValue>>,
|
||||
Expect<Equal<AutocompleteGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
|
||||
Expect<Equal<AutocompleteGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
|
||||
Expect<Equal<FirstArgument<Callable<ComboboxListProps<DomainValue>['children']>>, DomainValue>>,
|
||||
Expect<Equal<SecondArgument<Callable<ComboboxListProps<DomainValue>['children']>>, number>>,
|
||||
Expect<Equal<FirstArgument<Callable<ComboboxListRuntimeProps['children']>>, DomainValue>>,
|
||||
Expect<Equal<FirstArgument<ComboboxCollectionProps<DomainValue>['children']>, DomainValue>>,
|
||||
Expect<Equal<SecondArgument<ComboboxCollectionProps<DomainValue>['children']>, number>>,
|
||||
Expect<Equal<FirstArgument<ComboboxCollectionRuntimeProps['children']>, DomainValue>>,
|
||||
Expect<Equal<ComboboxGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
|
||||
Expect<Equal<ComboboxGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<ContextMenuRadioGroupProps<DomainValue>['onValueChange']>>,
|
||||
DomainValue
|
||||
>
|
||||
>,
|
||||
Expect<Equal<ContextMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
|
||||
Expect<
|
||||
Equal<
|
||||
FirstArgument<NonNullable<DropdownMenuRadioGroupProps<DomainValue>['onValueChange']>>,
|
||||
DomainValue
|
||||
>
|
||||
>,
|
||||
Expect<Equal<DropdownMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
|
||||
Expect<
|
||||
Equal<FirstArgument<NonNullable<RadioGroupProps<DomainValue>['onValueChange']>>, DomainValue>
|
||||
>,
|
||||
Expect<Equal<RadioItemProps<DomainValue>['value'], DomainValue>>,
|
||||
Expect<Equal<SliderRootProps<RangeValue>['value'], RangeValue | undefined>>,
|
||||
Expect<
|
||||
Equal<FirstArgument<NonNullable<SliderRootProps<RangeValue>['onValueChange']>>, RangeValue>
|
||||
>,
|
||||
Expect<
|
||||
Equal<NonNullable<DialogTriggerProps<OverlayPayload>['handle']>, DialogHandle<OverlayPayload>>
|
||||
>,
|
||||
Expect<
|
||||
Equal<NonNullable<DrawerTriggerProps<OverlayPayload>['handle']>, DrawerHandle<OverlayPayload>>
|
||||
>,
|
||||
Expect<
|
||||
Equal<NonNullable<PopoverTriggerProps<OverlayPayload>['handle']>, PopoverHandle<OverlayPayload>>
|
||||
>,
|
||||
Expect<
|
||||
Equal<
|
||||
NonNullable<PreviewCardTriggerProps<OverlayPayload>['handle']>,
|
||||
PreviewCardHandle<OverlayPayload>
|
||||
>
|
||||
>,
|
||||
]
|
||||
|
||||
export type { PublicTypeAssertions }
|
||||
@ -1,10 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import type { CSSProperties, UIEvent } from 'react'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -35,7 +34,9 @@ const stubElementMetric = (
|
||||
const renderScrollArea = (
|
||||
options: {
|
||||
rootClassName?: string
|
||||
contentStyle?: CSSProperties
|
||||
viewportClassName?: string
|
||||
viewportStyle?: CSSProperties
|
||||
verticalScrollbarClassName?: string
|
||||
horizontalScrollbarClassName?: string
|
||||
verticalThumbClassName?: string
|
||||
@ -43,9 +44,13 @@ const renderScrollArea = (
|
||||
} = {},
|
||||
) => {
|
||||
return render(
|
||||
<ScrollAreaRoot className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport data-testid="scroll-area-viewport" className={options.viewportClassName}>
|
||||
<ScrollAreaContent data-testid="scroll-area-content">
|
||||
<ScrollArea className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport
|
||||
data-testid="scroll-area-viewport"
|
||||
style={options.viewportStyle}
|
||||
className={options.viewportClassName}
|
||||
>
|
||||
<ScrollAreaContent data-testid="scroll-area-content" style={options.contentStyle}>
|
||||
<div className="h-48 w-48">Scrollable content</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
@ -70,11 +75,11 @@ const renderScrollArea = (
|
||||
className={options.horizontalThumbClassName}
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>,
|
||||
</ScrollArea>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('scroll-area wrapper', () => {
|
||||
describe('scroll area', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render the compound exports together', async () => {
|
||||
const screen = await renderScrollArea()
|
||||
@ -92,34 +97,90 @@ describe('scroll-area wrapper', () => {
|
||||
await expect.element(screen.getByTestId('scroll-area-horizontal-thumb')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the convenience wrapper and apply slot props', async () => {
|
||||
it('should keep accessible region semantics on the viewport', async () => {
|
||||
const screen = await render(
|
||||
<React.Fragment>
|
||||
<>
|
||||
<p id="installed-apps-label">Installed apps</p>
|
||||
<ScrollArea
|
||||
className="h-40 w-40"
|
||||
slotClassNames={{
|
||||
content: 'custom-content-class',
|
||||
scrollbar: 'custom-scrollbar-class',
|
||||
viewport: 'custom-viewport-class',
|
||||
}}
|
||||
labelledBy="installed-apps-label"
|
||||
data-testid="scroll-area-wrapper-root"
|
||||
>
|
||||
<div className="h-48 w-20">Scrollable content</div>
|
||||
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport
|
||||
aria-labelledby="installed-apps-label"
|
||||
className="custom-viewport-class"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className="custom-content-class">
|
||||
<div className="h-48 w-20">Scrollable content</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="custom-scrollbar-class">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</React.Fragment>,
|
||||
</>,
|
||||
)
|
||||
|
||||
const viewport = screen.getByRole('region', { name: 'Installed apps' })
|
||||
const content = screen.getByText('Scrollable content').element().parentElement
|
||||
|
||||
await expect.element(screen.getByTestId('scroll-area-wrapper-root')).toBeInTheDocument()
|
||||
await expect
|
||||
.element(screen.getByTestId('scroll-area-root'))
|
||||
.not.toHaveAttribute('role', 'region')
|
||||
await expect.element(viewport).toHaveClass('custom-viewport-class')
|
||||
await expect.element(viewport).toHaveAccessibleName('Installed apps')
|
||||
expect(content).toHaveClass('custom-content-class')
|
||||
await expect.element(screen.getByText('Scrollable content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep scrolling, focus, events, and refs on the viewport', async () => {
|
||||
let rootElement: HTMLDivElement | null = null
|
||||
let viewportElement: HTMLDivElement | null = null
|
||||
let scrollOwner: HTMLDivElement | null = null
|
||||
const onScroll = vi.fn((event: UIEvent<HTMLDivElement>) => {
|
||||
scrollOwner = event.currentTarget
|
||||
})
|
||||
|
||||
await render(
|
||||
<ScrollArea
|
||||
ref={(node) => {
|
||||
rootElement = node
|
||||
}}
|
||||
style={{ height: 100, width: 100 }}
|
||||
>
|
||||
<ScrollAreaViewport
|
||||
ref={(node) => {
|
||||
viewportElement = node
|
||||
}}
|
||||
aria-label="Scrollable results"
|
||||
onScroll={onScroll}
|
||||
role="region"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
>
|
||||
<ScrollAreaContent style={{ minWidth: 0 }}>
|
||||
<div style={{ height: 200, width: 100 }}>Scrollable content</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>,
|
||||
)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(viewportElement).not.toBeNull()
|
||||
expect(viewportElement!.scrollHeight).toBeGreaterThan(viewportElement!.clientHeight)
|
||||
expect(viewportElement).toHaveAttribute('tabindex', '0')
|
||||
})
|
||||
|
||||
viewportElement!.focus()
|
||||
expect(document.activeElement).toBe(viewportElement)
|
||||
|
||||
viewportElement!.scrollTop = 40
|
||||
await vi.waitFor(() => expect(onScroll).toHaveBeenCalled())
|
||||
|
||||
expect(viewportElement!.scrollTop).toBe(40)
|
||||
expect(scrollOwner).toBe(viewportElement)
|
||||
expect(rootElement).not.toBe(viewportElement)
|
||||
expect(rootElement).not.toHaveAttribute('tabindex')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Scrollbar', () => {
|
||||
@ -178,6 +239,22 @@ describe('scroll-area wrapper', () => {
|
||||
.element(screen.getByTestId('scroll-area-horizontal-scrollbar'))
|
||||
.toHaveClass('data-[orientation=horizontal]:mx-2', 'data-[orientation=horizontal]:mb-2')
|
||||
})
|
||||
|
||||
it('should let vertical layouts override the content minimum width without important CSS', async () => {
|
||||
const screen = await renderScrollArea({ contentStyle: { minWidth: 0 } })
|
||||
const content = screen.getByTestId('scroll-area-content').element()
|
||||
|
||||
expect(getComputedStyle(content).minWidth).toBe('0px')
|
||||
expect(content.style.getPropertyPriority('min-width')).toBe('')
|
||||
})
|
||||
|
||||
it('should let callers constrain a viewport axis without important CSS', async () => {
|
||||
const screen = await renderScrollArea({ viewportStyle: { overflowX: 'hidden' } })
|
||||
const viewport = screen.getByTestId('scroll-area-viewport').element()
|
||||
|
||||
expect(getComputedStyle(viewport).overflowX).toBe('hidden')
|
||||
expect(viewport.style.getPropertyPriority('overflow-x')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Corner', () => {
|
||||
@ -186,7 +263,7 @@ describe('scroll-area wrapper', () => {
|
||||
|
||||
try {
|
||||
const screen = await render(
|
||||
<ScrollAreaRoot className="h-40 w-40" data-testid="scroll-area-root">
|
||||
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
|
||||
<ScrollAreaViewport
|
||||
data-testid="scroll-area-viewport"
|
||||
ref={(node) => {
|
||||
@ -215,7 +292,7 @@ describe('scroll-area wrapper', () => {
|
||||
<ScrollAreaThumb data-testid="scroll-area-horizontal-thumb" />
|
||||
</ScrollAreaScrollbar>
|
||||
<ScrollAreaCorner data-testid="scroll-area-corner" />
|
||||
</ScrollAreaRoot>,
|
||||
</ScrollArea>,
|
||||
)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type * as React from 'react'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -12,18 +12,18 @@ import { cn } from '../cn'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/ScrollArea',
|
||||
component: ScrollAreaRoot,
|
||||
component: ScrollArea,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Compound scroll container built on Base UI Scroll Area. The examples mirror the upstream anatomy and focus patterns while applying Dify UI tokens and surface treatments. Base UI ScrollArea.Content defaults to min-width: fit-content, so vertical-only regions that should truncate long content must set min-width: 0 on the content slot.',
|
||||
'Compound scroll container built on Base UI Scroll Area. ScrollArea is the structural root; ScrollAreaViewport is the actual scroll and focus owner, so place scroll refs, events, role, accessible-name attributes, and overscroll behavior on the viewport. Add role="region" only when the content is important enough to be a landmark, preferring aria-labelledby when a visible heading exists. Base UI sets Viewport overflow and Content min-width inline: use style={{ overflowX: "hidden" }} for an axis override and style={{ minWidth: 0 }} for vertical content that should truncate; regular overflow-x-hidden and min-w-0 utilities cannot override those inline defaults. This is a targeted override contract for those Base UI defaults, not a general preference for inline styles over className.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof ScrollAreaRoot>
|
||||
} satisfies Meta<typeof ScrollArea>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
@ -54,12 +54,14 @@ const gridCells = Array.from({ length: 100 }, (_, index) => index + 1)
|
||||
function StorySection({
|
||||
eyebrow,
|
||||
title,
|
||||
titleId,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
eyebrow: string
|
||||
title: string
|
||||
titleId?: string
|
||||
description: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
@ -73,7 +75,9 @@ function StorySection({
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{eyebrow}</div>
|
||||
<h3 className="system-md-semibold text-text-primary">{title}</h3>
|
||||
<h3 id={titleId} className="system-md-semibold text-text-primary">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="max-w-[72ch] system-sm-regular text-pretty text-text-secondary">
|
||||
{description}
|
||||
</p>
|
||||
@ -91,10 +95,7 @@ function VerticalContent({
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<ScrollAreaContent
|
||||
style={verticalContentStyle}
|
||||
className={cn('w-full max-w-full min-w-0', className)}
|
||||
>
|
||||
<ScrollAreaContent style={verticalContentStyle} className={cn('w-full max-w-full', className)}>
|
||||
{children}
|
||||
</ScrollAreaContent>
|
||||
)
|
||||
@ -105,11 +106,12 @@ export const Anatomy: Story = {
|
||||
<StorySection
|
||||
eyebrow="Anatomy"
|
||||
title="Base UI compound parts"
|
||||
description="The baseline story mirrors the official Scroll Area anatomy: Root, Viewport, Content, Scrollbar, and Thumb, with keyboard focus drawn by the viewport."
|
||||
titleId="scroll-area-anatomy-title"
|
||||
description="The baseline story uses ScrollArea as the structural root, with Viewport, Content, Scrollbar, and Thumb composed explicitly. Keyboard focus and accessible naming belong to the viewport."
|
||||
>
|
||||
<ScrollAreaRoot className="relative h-75 w-full max-w-105 min-w-0">
|
||||
<ScrollArea className="relative h-75 w-full max-w-105 min-w-0">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Scrollable anatomy example"
|
||||
aria-labelledby="scroll-area-anatomy-title"
|
||||
role="region"
|
||||
className="h-full max-h-full max-w-full rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-bg"
|
||||
>
|
||||
@ -122,7 +124,7 @@ export const Anatomy: Story = {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
@ -134,7 +136,7 @@ export const Vertical: Story = {
|
||||
title="Long form content"
|
||||
description="Vertical overflow keeps the official viewport focus pattern while constraining content width so text never leaks outside the frame."
|
||||
>
|
||||
<ScrollAreaRoot className="relative h-90 w-full max-w-130 min-w-0">
|
||||
<ScrollArea className="relative h-90 w-full max-w-130 min-w-0">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Long form content"
|
||||
role="region"
|
||||
@ -155,7 +157,7 @@ export const Vertical: Story = {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
@ -165,9 +167,9 @@ export const VerticalTruncation: Story = {
|
||||
<StorySection
|
||||
eyebrow="Vertical"
|
||||
title="Constrained content width"
|
||||
description="Use width constraints plus minWidth: 0 on ScrollArea.Content when a vertical-only list should keep vertical scrolling while truncating long labels instead of creating horizontal scroll."
|
||||
description="Pass style={{ minWidth: 0 }} to ScrollAreaContent when a vertical-only list should truncate long labels instead of creating horizontal scroll. A regular min-w-0 utility cannot override Base UI's inline fit-content default."
|
||||
>
|
||||
<ScrollAreaRoot className="relative h-48 w-full max-w-80 min-w-0">
|
||||
<ScrollArea className="relative h-48 w-full max-w-80 min-w-0">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Vertical file list"
|
||||
role="region"
|
||||
@ -190,7 +192,7 @@ export const VerticalTruncation: Story = {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
@ -202,7 +204,7 @@ export const ScrollFade: Story = {
|
||||
title="Viewport mask with root focus"
|
||||
description="This mirrors the Base UI scroll-fade example: the viewport owns the mask and the root owns the focus outline so the indicator is never clipped."
|
||||
>
|
||||
<ScrollAreaRoot
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
'relative h-90 w-full max-w-130 min-w-0',
|
||||
'has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid',
|
||||
@ -227,7 +229,7 @@ export const ScrollFade: Story = {
|
||||
<ScrollAreaScrollbar className="opacity-0 data-hovering:opacity-100 data-scrolling:opacity-100 data-scrolling:duration-0">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
@ -240,7 +242,7 @@ export const Horizontal: Story = {
|
||||
description="Horizontal overflow keeps Base UI's content sizing behavior and uses the same viewport focus treatment on the scrollable element."
|
||||
className="mx-auto max-w-190"
|
||||
>
|
||||
<ScrollAreaRoot className="relative h-46 w-full max-w-130 min-w-0">
|
||||
<ScrollArea className="relative h-46 w-full max-w-130 min-w-0">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Horizontal numbered row"
|
||||
role="region"
|
||||
@ -262,7 +264,7 @@ export const Horizontal: Story = {
|
||||
<ScrollAreaScrollbar orientation="horizontal">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
@ -274,7 +276,7 @@ export const BothAxes: Story = {
|
||||
title="Numbered grid"
|
||||
description="This follows the official two-axis example: both scrollbars are rendered and Corner reserves the intersection."
|
||||
>
|
||||
<ScrollAreaRoot className="relative h-85 w-full max-w-140 min-w-0">
|
||||
<ScrollArea className="relative h-85 w-full max-w-140 min-w-0">
|
||||
<ScrollAreaViewport
|
||||
aria-label="Numbered grid"
|
||||
role="region"
|
||||
@ -300,7 +302,7 @@ export const BothAxes: Story = {
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</StorySection>
|
||||
),
|
||||
}
|
||||
|
||||
@ -1,30 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type * as React from 'react'
|
||||
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
|
||||
import { cn } from '../cn'
|
||||
|
||||
const ScrollAreaRoot = BaseScrollArea.Root
|
||||
type ScrollAreaRootProps = BaseScrollArea.Root.Props
|
||||
const ScrollArea = BaseScrollArea.Root
|
||||
type ScrollAreaProps = BaseScrollArea.Root.Props
|
||||
|
||||
const ScrollAreaContent = BaseScrollArea.Content
|
||||
type ScrollAreaContentProps = BaseScrollArea.Content.Props
|
||||
|
||||
type ScrollAreaSlotClassNames = {
|
||||
viewport?: string
|
||||
content?: string
|
||||
scrollbar?: string
|
||||
}
|
||||
type ScrollAreaOrientation = NonNullable<BaseScrollArea.Scrollbar.Props['orientation']>
|
||||
|
||||
type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
|
||||
children: React.ReactNode
|
||||
orientation?: ScrollAreaOrientation
|
||||
slotClassNames?: ScrollAreaSlotClassNames
|
||||
label?: string
|
||||
labelledBy?: string
|
||||
}
|
||||
|
||||
const scrollAreaScrollbarClassName = cn(
|
||||
'group/scrollbar flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
|
||||
'pointer-events-none data-hovering:pointer-events-auto',
|
||||
@ -89,37 +73,10 @@ function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps) {
|
||||
return <BaseScrollArea.Corner className={cn(scrollAreaCornerClassName, className)} {...props} />
|
||||
}
|
||||
|
||||
function ScrollArea({
|
||||
children,
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
slotClassNames,
|
||||
label,
|
||||
labelledBy,
|
||||
...props
|
||||
}: ScrollAreaProps) {
|
||||
return (
|
||||
<ScrollAreaRoot className={className} {...props}>
|
||||
<ScrollAreaViewport
|
||||
aria-label={label}
|
||||
aria-labelledby={labelledBy}
|
||||
className={slotClassNames?.viewport}
|
||||
role={label || labelledBy ? 'region' : undefined}
|
||||
>
|
||||
<ScrollAreaContent className={slotClassNames?.content}>{children}</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar orientation={orientation} className={slotClassNames?.scrollbar}>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -129,7 +86,6 @@ export type {
|
||||
ScrollAreaContentProps,
|
||||
ScrollAreaCornerProps,
|
||||
ScrollAreaProps,
|
||||
ScrollAreaRootProps,
|
||||
ScrollAreaScrollbarProps,
|
||||
ScrollAreaThumbProps,
|
||||
ScrollAreaViewportProps,
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { ResourceOpenScope } from '@/models/access-control'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@ -89,35 +95,39 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-background-default-subtle"
|
||||
slotClassNames={{ viewport: 'overscroll-contain' }}
|
||||
>
|
||||
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
|
||||
<h1 className="system-xl-semibold text-text-primary">
|
||||
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
|
||||
</h1>
|
||||
<p className="mt-0.5 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
|
||||
</p>
|
||||
</header>
|
||||
<main className="w-full px-6 pt-8 pb-10">
|
||||
<AccessRulesEditor
|
||||
className="w-full max-w-200"
|
||||
rules={appAccessRules}
|
||||
userAccessSettings={appUserAccessSettings}
|
||||
isLoadingRules={isLoadingAppAccessRules}
|
||||
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
|
||||
openScope={openScope}
|
||||
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
|
||||
updatingAccountId={updatingAccountId}
|
||||
maintainerId={maintainerId}
|
||||
onOpenScopeChange={handleOpenScopeChange}
|
||||
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
|
||||
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
|
||||
onAddAccessSubject={handleUserAccessPoliciesChange}
|
||||
/>
|
||||
</main>
|
||||
<ScrollArea className="h-full bg-background-default-subtle">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent>
|
||||
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
|
||||
<h1 className="system-xl-semibold text-text-primary">
|
||||
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
|
||||
</h1>
|
||||
<p className="mt-0.5 system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
|
||||
</p>
|
||||
</header>
|
||||
<main className="w-full px-6 pt-8 pb-10">
|
||||
<AccessRulesEditor
|
||||
className="w-full max-w-200"
|
||||
rules={appAccessRules}
|
||||
userAccessSettings={appUserAccessSettings}
|
||||
isLoadingRules={isLoadingAppAccessRules}
|
||||
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
|
||||
openScope={openScope}
|
||||
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
|
||||
updatingAccountId={updatingAccountId}
|
||||
maintainerId={maintainerId}
|
||||
onOpenScopeChange={handleOpenScopeChange}
|
||||
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
|
||||
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
|
||||
onAddAccessSubject={handleUserAccessPoliciesChange}
|
||||
/>
|
||||
</main>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
@ -8,7 +8,13 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
|
||||
import { Field, FieldControl, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -369,355 +375,367 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
onFormSubmit={handleFormSubmit}
|
||||
>
|
||||
{/* form body */}
|
||||
<ScrollArea
|
||||
className="relative min-h-0"
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'min-w-0 space-y-5 px-6 py-3',
|
||||
}}
|
||||
>
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<Field name="title" className="grow">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<FieldControl
|
||||
value={inputInfo.title}
|
||||
onValueChange={(value) => setInputInfo((item) => ({ ...item, title: value }))}
|
||||
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
|
||||
/>
|
||||
</Field>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => {
|
||||
setShowAppIconPicker(true)
|
||||
}}
|
||||
className="mt-2 cursor-pointer"
|
||||
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
|
||||
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
|
||||
/>
|
||||
</div>
|
||||
{/* description */}
|
||||
<Field name="description">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
value={inputInfo.desc}
|
||||
onValueChange={onDesChange}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
<FieldDescription>
|
||||
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
<Field name="use_icon_as_answer_icon" className="w-full">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
{t(($) => $['answerIcon.description'], { ns: 'app' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={handleLanguageChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
|
||||
size="medium"
|
||||
className="w-50"
|
||||
>
|
||||
{selectedLanguage?.name ?? t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">
|
||||
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
</div>
|
||||
<Field name="chat_color_theme" className="w-50 shrink-0">
|
||||
<FieldControl
|
||||
className="mb-1"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onValueChange={(value) =>
|
||||
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
|
||||
}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
|
||||
<span>
|
||||
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</span>
|
||||
<Switch
|
||||
checked={inputInfo.chatColorThemeInverted}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
|
||||
}
|
||||
></Switch>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<Field name="show_workflow_steps" className="w-full">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
disabled={
|
||||
!(
|
||||
appInfo.mode === AppModeEnum.WORKFLOW ||
|
||||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
|
||||
)
|
||||
}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={(v) => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Divider className="my-0 h-px" />
|
||||
<div className="space-y-5">
|
||||
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div
|
||||
id={inputPlaceholderLabelId}
|
||||
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
|
||||
>
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</div>
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-4.5 select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
|
||||
/>
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadgeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
id={inputPlaceholderDescriptionId}
|
||||
className="pb-0.5 body-xs-regular text-text-tertiary"
|
||||
>
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</p>
|
||||
{isCloudSandboxPlan ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={inputPlaceholderField} />
|
||||
<TooltipContent className="w-45">
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
inputPlaceholderField
|
||||
)}
|
||||
{!isCloudSandboxPlan && (
|
||||
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
|
||||
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
{/* upgrade button */}
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-4.5 select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
|
||||
/>
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadgeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{webappCopyrightEnabled ? (
|
||||
<Switch
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
|
||||
<ScrollArea className="relative min-h-0">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent style={{ minWidth: 0 }} className="space-y-5 px-6 py-3">
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<Field name="title" className="grow">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<FieldControl
|
||||
value={inputInfo.title}
|
||||
onValueChange={(value) =>
|
||||
setInputInfo((item) => ({ ...item, title: value }))
|
||||
}
|
||||
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div>
|
||||
<Switch
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
disabled
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="w-45">
|
||||
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Field>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => {
|
||||
setShowAppIconPicker(true)
|
||||
}}
|
||||
className="mt-2 cursor-pointer"
|
||||
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
|
||||
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
||||
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
|
||||
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">
|
||||
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
|
||||
</p>
|
||||
{copyrightSwitchValue && (
|
||||
<Input
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
{/* description */}
|
||||
<Field name="description">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
value={inputInfo.desc}
|
||||
onValueChange={onDesChange}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
|
||||
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
<FieldDescription>
|
||||
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
<Field name="use_icon_as_answer_icon" className="w-full">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
{t(($) => $['answerIcon.description'], { ns: 'app' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={handleLanguageChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t(($) => $[`${prefixSettings}.language`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
size="medium"
|
||||
className="w-50"
|
||||
>
|
||||
{selectedLanguage?.name ??
|
||||
t(($) => $['placeholder.select'], { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
|
||||
ns="appOverview"
|
||||
components={{
|
||||
privacyPolicyLink: (
|
||||
<Link
|
||||
href="https://dify.ai/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">
|
||||
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Field name="chat_color_theme" className="w-50 shrink-0">
|
||||
<FieldControl
|
||||
className="mb-1"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onValueChange={(value) =>
|
||||
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
|
||||
}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
|
||||
<span>
|
||||
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</span>
|
||||
<Switch
|
||||
checked={inputInfo.chatColorThemeInverted}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
|
||||
}
|
||||
></Switch>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<Field name="show_workflow_steps" className="w-full">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<FieldLabel>
|
||||
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
disabled={
|
||||
!(
|
||||
appInfo.mode === AppModeEnum.WORKFLOW ||
|
||||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
|
||||
)
|
||||
}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, show_workflow_steps: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Divider className="my-0 h-px" />
|
||||
<div className="space-y-5">
|
||||
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div
|
||||
id={inputPlaceholderLabelId}
|
||||
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
|
||||
>
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</div>
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-4.5 select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
|
||||
/>
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadgeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
id={inputPlaceholderDescriptionId}
|
||||
className="pb-0.5 body-xs-regular text-text-tertiary"
|
||||
>
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</p>
|
||||
{isCloudSandboxPlan ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={inputPlaceholderField} />
|
||||
<TooltipContent className="w-45">
|
||||
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
inputPlaceholderField
|
||||
)}
|
||||
{!isCloudSandboxPlan && (
|
||||
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
|
||||
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
{/* upgrade button */}
|
||||
{isCloudSandboxPlan && (
|
||||
<div className="h-4.5 select-none">
|
||||
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
|
||||
/>
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadgeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{webappCopyrightEnabled ? (
|
||||
<Switch
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], { ns: 'appOverview' })}
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div>
|
||||
<Switch
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
disabled
|
||||
checked={copyrightSwitchValue}
|
||||
onCheckedChange={(v) =>
|
||||
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="w-45">
|
||||
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">
|
||||
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
|
||||
</p>
|
||||
{copyrightSwitchValue && (
|
||||
<Input
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
|
||||
</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
|
||||
ns="appOverview"
|
||||
components={{
|
||||
privacyPolicyLink: (
|
||||
<Link
|
||||
href="https://dify.ai/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</p>
|
||||
<Textarea
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onValueChange={(value) =>
|
||||
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
|
||||
}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
</p>
|
||||
<Textarea
|
||||
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
|
||||
ns: 'appOverview',
|
||||
})}
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onValueChange={(value) =>
|
||||
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
|
||||
}
|
||||
placeholder={
|
||||
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
|
||||
ns: 'appOverview',
|
||||
}) as string
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
{/* footer */}
|
||||
<div className="flex shrink-0 justify-end p-6 pt-5">
|
||||
|
||||
@ -3,9 +3,9 @@ import type { FC } from 'react'
|
||||
import type { Category } from './types'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -51,7 +51,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
|
||||
}}
|
||||
>
|
||||
<DialogContent className="inset-0 size-full max-h-none max-w-none translate-0 overflow-hidden rounded-none border-none bg-saas-background p-0 shadow-none">
|
||||
<ScrollAreaRoot className="relative h-full w-full overflow-hidden">
|
||||
<ScrollArea className="relative h-full w-full overflow-hidden">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent className="min-h-full min-w-300">
|
||||
<div className="relative grid min-h-full grid-rows-[1fr_auto_auto_1fr] overflow-hidden">
|
||||
@ -85,7 +85,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
|
||||
<ScrollAreaThumb className="rounded-full" />
|
||||
</ScrollAreaScrollbar>
|
||||
<ScrollAreaCorner className="bg-saas-background" />
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { ResourceOpenScope } from '@/models/access-control'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@ -103,35 +109,39 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
|
||||
if (!canAccessConfig) return <Loading type="app" />
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
className="h-full bg-background-default-subtle"
|
||||
slotClassNames={{ viewport: 'overscroll-contain' }}
|
||||
>
|
||||
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
|
||||
<h1 className="system-sm-semibold text-text-primary">
|
||||
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
|
||||
</h1>
|
||||
<p className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
|
||||
</p>
|
||||
</header>
|
||||
<main className="w-full px-6 pt-8 pb-10">
|
||||
<AccessRulesEditor
|
||||
className="w-full max-w-200"
|
||||
rules={datasetAccessRules}
|
||||
userAccessSettings={datasetUserAccessSettings}
|
||||
isLoadingRules={isLoadingDatasetAccessRules}
|
||||
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
|
||||
openScope={openScope}
|
||||
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
|
||||
updatingAccountId={updatingAccountId}
|
||||
maintainerId={dataset?.maintainer}
|
||||
onOpenScopeChange={handleOpenScopeChange}
|
||||
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
|
||||
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
|
||||
onAddAccessSubject={handleUserAccessPoliciesChange}
|
||||
/>
|
||||
</main>
|
||||
<ScrollArea className="h-full bg-background-default-subtle">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent>
|
||||
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
|
||||
<h1 className="system-sm-semibold text-text-primary">
|
||||
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
|
||||
</h1>
|
||||
<p className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
|
||||
</p>
|
||||
</header>
|
||||
<main className="w-full px-6 pt-8 pb-10">
|
||||
<AccessRulesEditor
|
||||
className="w-full max-w-200"
|
||||
rules={datasetAccessRules}
|
||||
userAccessSettings={datasetUserAccessSettings}
|
||||
isLoadingRules={isLoadingDatasetAccessRules}
|
||||
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
|
||||
openScope={openScope}
|
||||
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
|
||||
updatingAccountId={updatingAccountId}
|
||||
maintainerId={dataset?.maintainer}
|
||||
onOpenScopeChange={handleOpenScopeChange}
|
||||
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
|
||||
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
|
||||
onAddAccessSubject={handleUserAccessPoliciesChange}
|
||||
/>
|
||||
</main>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -193,7 +193,7 @@ const SideBar = () => {
|
||||
)}
|
||||
{!isFold ? (
|
||||
<div className="min-h-0 flex-1">
|
||||
<ScrollAreaRoot className="h-full">
|
||||
<ScrollArea className="h-full">
|
||||
<ScrollAreaViewport
|
||||
ref={scrollRef}
|
||||
aria-busy={installedAppsQuery.isFetchingNextPage}
|
||||
@ -219,7 +219,7 @@ const SideBar = () => {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
||||
@ -24,8 +24,8 @@ import {
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -443,11 +443,11 @@ function GotoAnythingDialog() {
|
||||
|
||||
<AutocompleteStatus className="sr-only">{autocompleteStatus}</AutocompleteStatus>
|
||||
|
||||
<ScrollAreaRoot
|
||||
aria-busy={isLoading || undefined}
|
||||
className="relative h-60 min-h-0 overflow-hidden"
|
||||
>
|
||||
<ScrollAreaViewport className="scroll-py-1 overscroll-contain">
|
||||
<ScrollArea className="relative h-60 min-h-0 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-busy={isLoading || undefined}
|
||||
className="scroll-py-1 overscroll-contain"
|
||||
>
|
||||
<ScrollAreaContent
|
||||
className="min-h-full w-full max-w-full"
|
||||
style={{ minWidth: '100%' }}
|
||||
@ -576,7 +576,7 @@ function GotoAnythingDialog() {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
|
||||
<Footer
|
||||
resultCount={autocompleteResultCount}
|
||||
|
||||
@ -2,7 +2,13 @@
|
||||
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useRef } from 'react'
|
||||
@ -225,37 +231,44 @@ export default function AccountSetting({
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-206 min-w-0">
|
||||
<ScrollArea
|
||||
ref={scrollContainerRef}
|
||||
className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg"
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain overflow-x-hidden',
|
||||
content: 'min-h-full min-w-0 w-full max-w-full pb-4',
|
||||
}}
|
||||
>
|
||||
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
|
||||
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
|
||||
{activeItem?.title ?? activeItem?.name}
|
||||
{activeItem?.description && (
|
||||
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
|
||||
{activeItem?.description}
|
||||
<ScrollArea className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg">
|
||||
<ScrollAreaViewport
|
||||
ref={scrollContainerRef}
|
||||
style={{ overflowX: 'hidden' }}
|
||||
className="overscroll-contain"
|
||||
>
|
||||
<ScrollAreaContent
|
||||
style={{ minWidth: 0 }}
|
||||
className="min-h-full w-full max-w-full pb-4"
|
||||
>
|
||||
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
|
||||
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
|
||||
{activeItem?.title ?? activeItem?.name}
|
||||
{activeItem?.description && (
|
||||
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
|
||||
{activeItem?.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
|
||||
<PermissionsPage containerRef={scrollContainerRef} />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
|
||||
<WorkflowLogArchivesPage />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
|
||||
<PermissionsPage containerRef={scrollContainerRef} />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
|
||||
<WorkflowLogArchivesPage />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
|
||||
</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ComboboxInput, ComboboxInputGroup } from '@langgenius/dify-ui/combobox'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -75,18 +75,19 @@ type ModelSelectorScrollBodyProps = {
|
||||
|
||||
export function ModelSelectorScrollBody({ children, label }: ModelSelectorScrollBodyProps) {
|
||||
return (
|
||||
<ScrollAreaRoot className="relative min-h-0 overflow-hidden overscroll-contain">
|
||||
<ScrollArea className="relative min-h-0 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={label}
|
||||
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overflow-x-hidden overscroll-contain"
|
||||
style={{ overflowX: 'hidden' }}
|
||||
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overscroll-contain"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className="min-w-0 overflow-x-hidden">{children}</ScrollAreaContent>
|
||||
<ScrollAreaContent style={{ minWidth: 0 }}>{children}</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="z-2">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,13 @@
|
||||
import type { PermissionGroup } from '@/models/access-control'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -75,113 +81,114 @@ const PermissionGroupList = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ScrollArea
|
||||
className="h-full overflow-hidden rounded-xl"
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: groups.length === 0 ? 'h-full' : undefined,
|
||||
}}
|
||||
>
|
||||
{groups.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{groups.map((group) => {
|
||||
const expanded = expandedGroupKeysMerged.has(group.group_key)
|
||||
const selectedCount = group.permissions.reduce(
|
||||
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
|
||||
0,
|
||||
)
|
||||
const totalCount = group.permissions.length
|
||||
const allSelected = totalCount > 0 && selectedCount === totalCount
|
||||
<ScrollArea className="h-full overflow-hidden rounded-xl">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent className={groups.length === 0 ? 'h-full' : undefined}>
|
||||
{groups.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{groups.map((group) => {
|
||||
const expanded = expandedGroupKeysMerged.has(group.group_key)
|
||||
const selectedCount = group.permissions.reduce(
|
||||
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
|
||||
0,
|
||||
)
|
||||
const totalCount = group.permissions.length
|
||||
const allSelected = totalCount > 0 && selectedCount === totalCount
|
||||
|
||||
return (
|
||||
<div key={group.group_key} className="border-b border-b-divider-subtle">
|
||||
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => toggleGroupExpanded(group.group_key)}
|
||||
>
|
||||
{group.group_name}
|
||||
</button>
|
||||
{!readonly && totalCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
onClick={() => toggleGroupSelection(group, selectedCount)}
|
||||
>
|
||||
{allSelected
|
||||
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
|
||||
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
|
||||
</button>
|
||||
)}
|
||||
{selectedCount > 0 && (
|
||||
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
|
||||
{selectedCount}/{totalCount}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
aria-label={
|
||||
expanded
|
||||
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
|
||||
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
|
||||
}
|
||||
onClick={() => toggleGroupExpanded(group.group_key)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
|
||||
expanded && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="flex flex-col">
|
||||
{group.permissions.map((permission) => {
|
||||
const checked = selectedSet.has(permission.key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={permission.key}
|
||||
className={cn(
|
||||
'flex min-h-9 items-center gap-3 px-3 py-1.5',
|
||||
readonly
|
||||
? 'cursor-default'
|
||||
: 'cursor-pointer hover:bg-state-base-hover',
|
||||
checked && 'bg-state-accent-hover',
|
||||
checked && !readonly && 'hover:bg-state-accent-hover',
|
||||
)}
|
||||
onClick={() => togglePermission(permission.key)}
|
||||
return (
|
||||
<div key={group.group_key} className="border-b border-b-divider-subtle">
|
||||
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => toggleGroupExpanded(group.group_key)}
|
||||
>
|
||||
{group.group_name}
|
||||
</button>
|
||||
{!readonly && totalCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
onClick={() => toggleGroupSelection(group, selectedCount)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={readonly}
|
||||
className="shrink-0"
|
||||
onCheckedChange={() => {
|
||||
togglePermission(permission.key)
|
||||
}}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
|
||||
{permission.name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{allSelected
|
||||
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
|
||||
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
|
||||
</button>
|
||||
)}
|
||||
{selectedCount > 0 && (
|
||||
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
|
||||
{selectedCount}/{totalCount}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
|
||||
aria-label={
|
||||
expanded
|
||||
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
|
||||
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
|
||||
}
|
||||
onClick={() => toggleGroupExpanded(group.group_key)}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
|
||||
expanded && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="flex flex-col">
|
||||
{group.permissions.map((permission) => {
|
||||
const checked = selectedSet.has(permission.key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={permission.key}
|
||||
className={cn(
|
||||
'flex min-h-9 items-center gap-3 px-3 py-1.5',
|
||||
readonly
|
||||
? 'cursor-default'
|
||||
: 'cursor-pointer hover:bg-state-base-hover',
|
||||
checked && 'bg-state-accent-hover',
|
||||
checked && !readonly && 'hover:bg-state-accent-hover',
|
||||
)}
|
||||
onClick={() => togglePermission(permission.key)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={readonly}
|
||||
className="shrink-0"
|
||||
onCheckedChange={() => {
|
||||
togglePermission(permission.key)
|
||||
}}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
|
||||
{permission.name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -5,7 +5,13 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
@ -227,99 +233,102 @@ const WorkspaceRoleCheckboxList = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
ref={containerRef}
|
||||
className="min-h-0 flex-1"
|
||||
slotClassNames={{ viewport: 'px-3 overscroll-contain' }}
|
||||
>
|
||||
{rolesLoading ? (
|
||||
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['role.loading'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : filteredRoles.length === 0 ? (
|
||||
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{allowMultipleRoles ? (
|
||||
<ul className="flex flex-col gap-0.5 pb-2">
|
||||
{filteredRoles.map((role) => {
|
||||
const checked = selectedRoleIdSet.has(role.id)
|
||||
const disabled = disabledRoleIdSet.has(role.id)
|
||||
const handleToggle = () => toggleRole(role)
|
||||
|
||||
return (
|
||||
<li key={role.id}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
|
||||
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
|
||||
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleToggle()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
className="pointer-events-none mt-0.5"
|
||||
/>
|
||||
{renderRoleText(role)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ScrollAreaViewport ref={containerRef} className="overscroll-contain px-3">
|
||||
<ScrollAreaContent>
|
||||
{rolesLoading ? (
|
||||
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['role.loading'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : filteredRoles.length === 0 ? (
|
||||
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
|
||||
</div>
|
||||
) : (
|
||||
<RadioGroup
|
||||
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
|
||||
value={selectedRoleIds[0] ?? ''}
|
||||
onValueChange={handleRadioValueChange}
|
||||
className="flex-col items-stretch gap-0.5 pb-2"
|
||||
render={<ul />}
|
||||
>
|
||||
{filteredRoles.map((role) => {
|
||||
const checked = selectedRoleIdSet.has(role.id)
|
||||
const disabled = disabledRoleIdSet.has(role.id)
|
||||
<>
|
||||
{allowMultipleRoles ? (
|
||||
<ul className="flex flex-col gap-0.5 pb-2">
|
||||
{filteredRoles.map((role) => {
|
||||
const checked = selectedRoleIdSet.has(role.id)
|
||||
const disabled = disabledRoleIdSet.has(role.id)
|
||||
const handleToggle = () => toggleRole(role)
|
||||
|
||||
return (
|
||||
<li key={role.id}>
|
||||
<RadioItem
|
||||
value={role.id}
|
||||
disabled={disabled}
|
||||
nativeButton
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
return (
|
||||
<li key={role.id}>
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
|
||||
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
|
||||
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
|
||||
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RadioControl className="pointer-events-none mt-0.5" />
|
||||
{renderRoleText(role)}
|
||||
</RadioItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</RadioGroup>
|
||||
onClick={handleToggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleToggle()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
className="pointer-events-none mt-0.5"
|
||||
/>
|
||||
{renderRoleText(role)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<RadioGroup
|
||||
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
|
||||
value={selectedRoleIds[0] ?? ''}
|
||||
onValueChange={handleRadioValueChange}
|
||||
className="flex-col items-stretch gap-0.5 pb-2"
|
||||
render={<ul />}
|
||||
>
|
||||
{filteredRoles.map((role) => {
|
||||
const checked = selectedRoleIdSet.has(role.id)
|
||||
const disabled = disabledRoleIdSet.has(role.id)
|
||||
|
||||
return (
|
||||
<li key={role.id}>
|
||||
<RadioItem
|
||||
value={role.id}
|
||||
disabled={disabled}
|
||||
nativeButton
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
|
||||
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
|
||||
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RadioControl className="pointer-events-none mt-0.5" />
|
||||
{renderRoleText(role)}
|
||||
</RadioItem>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</RadioGroup>
|
||||
)}
|
||||
<div ref={anchorRef} className="h-0" />
|
||||
</>
|
||||
)}
|
||||
<div ref={anchorRef} className="h-0" />
|
||||
</>
|
||||
)}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -5,7 +5,13 @@ import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import type { DocPathWithoutLang } from '@/types/doc-paths'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import UpdateSettingDialog from '@/app/components/header/account-setting/update-setting-dialog'
|
||||
@ -339,28 +345,32 @@ export default function IntegrationsPage({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
label={scrollAreaLabel}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'min-h-full',
|
||||
}}
|
||||
>
|
||||
<IntegrationSectionRenderer
|
||||
key={section}
|
||||
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}
|
||||
/>
|
||||
<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">
|
||||
<IntegrationSectionRenderer
|
||||
key={section}
|
||||
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}
|
||||
/>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
|
||||
type IntegrationSectionLayoutProps = {
|
||||
bodyClassName?: string
|
||||
@ -15,15 +21,19 @@ export function IntegrationSectionLayout({
|
||||
label,
|
||||
}: IntegrationSectionLayoutProps) {
|
||||
return (
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
label={label}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'min-h-full',
|
||||
}}
|
||||
>
|
||||
<div className={bodyClassName}>{children}</div>
|
||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,8 +5,8 @@ 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,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -203,7 +203,7 @@ 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">
|
||||
<ScrollAreaRoot className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
|
||||
<ScrollArea className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
|
||||
<ScrollAreaViewport
|
||||
ref={containerRef}
|
||||
aria-label={t(($) => $['menus.tools'], { ns: 'common' })}
|
||||
@ -261,7 +261,7 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
{currentProvider && !currentProvider.plugin_id && (
|
||||
<ProviderDetail
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -213,15 +213,16 @@ const WebAppsSectionContent = () => {
|
||||
</div>
|
||||
)}
|
||||
{appsExpanded && (
|
||||
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden overscroll-contain">
|
||||
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
ref={scrollRef}
|
||||
aria-busy={installedAppsQuery.isPending || installedAppsQuery.isFetchingNextPage}
|
||||
aria-label={t(($) => $['sidebar.webApps'], { ns: 'explore' })}
|
||||
className="overflow-x-hidden"
|
||||
style={{ overflowX: 'hidden' }}
|
||||
className="overscroll-contain"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className="w-full max-w-full min-w-0! px-2">
|
||||
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full px-2">
|
||||
{installedAppsQuery.isPending && <WebAppsSkeleton />}
|
||||
{!installedAppsQuery.isPending && installedAppsQuery.isError && (
|
||||
<div
|
||||
@ -277,7 +278,7 @@ const WebAppsSectionContent = () => {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<AlertDialog
|
||||
open={uninstallDialogAppId !== null}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import PluginItem from './plugin-item'
|
||||
|
||||
type PluginSectionProps = {
|
||||
@ -36,36 +42,36 @@ function PluginSection({
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-0 flex items-start justify-between pt-1 pr-1 pl-2 system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="flex h-6 items-center">
|
||||
<h3 className="flex h-6 items-center">
|
||||
{title} ({count})
|
||||
</span>
|
||||
</h3>
|
||||
{headerAction}
|
||||
</div>
|
||||
<ScrollArea
|
||||
className="overflow-hidden"
|
||||
label={title}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'min-w-0',
|
||||
}}
|
||||
>
|
||||
{plugins.map((plugin) => (
|
||||
<PluginItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={statusIcon}
|
||||
statusText={plugin.message || defaultStatusText}
|
||||
statusClassName={statusClassName}
|
||||
action={renderItemAction?.(plugin)}
|
||||
onClear={
|
||||
onClearSingle
|
||||
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<ScrollArea className="overflow-hidden">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent style={{ minWidth: 0 }}>
|
||||
{plugins.map((plugin) => (
|
||||
<PluginItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={statusIcon}
|
||||
statusText={plugin.message || defaultStatusText}
|
||||
statusClassName={statusClassName}
|
||||
action={renderItemAction?.(plugin)}
|
||||
onClear={
|
||||
onClearSingle
|
||||
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import type { PluginStatus } from '@/app/components/plugins/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import ErrorPluginItem from './error-plugin-item'
|
||||
@ -36,84 +42,89 @@ function PluginTaskList({
|
||||
className="w-90 max-w-[calc(100vw-32px)] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
|
||||
data-testid="plugin-task-list"
|
||||
>
|
||||
<ScrollArea
|
||||
className="max-h-105 overflow-hidden"
|
||||
label={t(($) => $['task.installing'], { ns: 'plugin' })}
|
||||
slotClassNames={{
|
||||
viewport: 'max-h-[420px] overscroll-contain',
|
||||
content: 'w-full! max-w-full! min-w-0! overflow-x-hidden! pr-3',
|
||||
}}
|
||||
>
|
||||
{runningPlugins.length > 0 && (
|
||||
<PluginSection
|
||||
title={runningSectionTitle}
|
||||
count={runningPlugins.length}
|
||||
plugins={runningPlugins}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={
|
||||
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
|
||||
}
|
||||
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
<ScrollArea className="max-h-105 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t(($) => $['task.installing'], { ns: 'plugin' })}
|
||||
style={{ overflowX: 'hidden' }}
|
||||
className="max-h-[420px] overscroll-contain"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full pr-3">
|
||||
{runningPlugins.length > 0 && (
|
||||
<PluginSection
|
||||
title={runningSectionTitle}
|
||||
count={runningPlugins.length}
|
||||
plugins={runningPlugins}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={
|
||||
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
|
||||
}
|
||||
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{successPlugins.length > 0 && (
|
||||
<PluginSection
|
||||
title={successSectionTitle}
|
||||
count={successPlugins.length}
|
||||
plugins={successPlugins}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />}
|
||||
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
|
||||
statusClassName="text-text-success"
|
||||
headerAction={
|
||||
<Button
|
||||
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearAll}
|
||||
>
|
||||
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
}
|
||||
onClearSingle={onClearSingle}
|
||||
/>
|
||||
)}
|
||||
{successPlugins.length > 0 && (
|
||||
<PluginSection
|
||||
title={successSectionTitle}
|
||||
count={successPlugins.length}
|
||||
plugins={successPlugins}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
statusIcon={
|
||||
<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />
|
||||
}
|
||||
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
|
||||
statusClassName="text-text-success"
|
||||
headerAction={
|
||||
<Button
|
||||
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearAll}
|
||||
>
|
||||
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
}
|
||||
onClearSingle={onClearSingle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{errorPlugins.length > 0 && (
|
||||
<>
|
||||
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
|
||||
{errorSectionTitle} ({errorPlugins.length})
|
||||
<Button
|
||||
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearErrors}
|
||||
>
|
||||
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
aria-label={errorSectionTitle}
|
||||
className="w-full max-w-full min-w-0 overflow-hidden"
|
||||
role="region"
|
||||
>
|
||||
{errorPlugins.map((plugin) => (
|
||||
<ErrorPluginItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{errorPlugins.length > 0 && (
|
||||
<>
|
||||
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
|
||||
<h3>
|
||||
{errorSectionTitle} ({errorPlugins.length})
|
||||
</h3>
|
||||
<Button
|
||||
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
|
||||
className="shrink-0"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={onClearErrors}
|
||||
>
|
||||
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full max-w-full min-w-0 overflow-hidden">
|
||||
{errorPlugins.map((plugin) => (
|
||||
<ErrorPluginItem
|
||||
key={plugin.plugin_unique_identifier}
|
||||
plugin={plugin}
|
||||
getIconUrl={getIconUrl}
|
||||
language={language}
|
||||
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -5,8 +5,8 @@ import type { Collection } from '@/app/components/tools/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -103,7 +103,7 @@ const PluginsPanelResults = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<ScrollAreaRoot
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
'min-h-0 grow self-stretch overflow-hidden bg-components-panel-bg',
|
||||
contentFrameClassName,
|
||||
@ -169,7 +169,7 @@ const PluginsPanelResults = ({
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,13 @@ import { Field, FieldItem, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
@ -112,185 +118,203 @@ export default function ConfigCredential({ positionCenter, credential, onChange,
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'space-y-4 pt-2 pr-8 pl-6',
|
||||
}}
|
||||
>
|
||||
<Field name="auth_type" className="contents">
|
||||
<Fieldset
|
||||
render={
|
||||
<RadioGroup<AuthType>
|
||||
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
|
||||
value={tempCredential.auth_type}
|
||||
onValueChange={handleAuthTypeChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
|
||||
</FieldsetLegend>
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
|
||||
value={AuthType.none}
|
||||
isChecked={tempCredential.auth_type === AuthType.none}
|
||||
/>
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthType.apiKeyHeader}
|
||||
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
|
||||
/>
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthType.apiKeyQuery}
|
||||
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Field>
|
||||
{tempCredential.auth_type === AuthType.apiKeyHeader && (
|
||||
<>
|
||||
<Field name="api_key_header_prefix" className="contents">
|
||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent className="space-y-4 pt-2 pr-8 pl-6">
|
||||
<Field name="auth_type" className="contents">
|
||||
<Fieldset
|
||||
render={
|
||||
<RadioGroup<AuthHeaderPrefix>
|
||||
<RadioGroup<AuthType>
|
||||
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
|
||||
value={tempCredential.api_key_header_prefix}
|
||||
onValueChange={(value) =>
|
||||
setTempCredential({ ...tempCredential, api_key_header_prefix: value })
|
||||
}
|
||||
value={tempCredential.auth_type}
|
||||
onValueChange={handleAuthTypeChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
|
||||
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
|
||||
</FieldsetLegend>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.basic}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
|
||||
}
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
|
||||
value={AuthType.none}
|
||||
isChecked={tempCredential.auth_type === AuthType.none}
|
||||
/>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.bearer}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
|
||||
}
|
||||
value={AuthType.apiKeyHeader}
|
||||
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
|
||||
/>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
|
||||
<SelectItem<AuthType>
|
||||
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.custom}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
|
||||
}
|
||||
value={AuthType.apiKeyQuery}
|
||||
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Field>
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
|
||||
<Infotip
|
||||
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
className="ml-0.5 size-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_header}
|
||||
onChange={(e) =>
|
||||
setTempCredential({ ...tempCredential, api_key_header: e.target.value })
|
||||
}
|
||||
placeholder={t(($) => $['createTool.authMethod.types.apiKeyPlaceholder'], {
|
||||
ns: 'tools',
|
||||
})!}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_value}
|
||||
onChange={(e) =>
|
||||
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{tempCredential.auth_type === AuthType.apiKeyQuery && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
|
||||
<Infotip
|
||||
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
className="ml-0.5 size-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t(($) => $['createTool.authMethod.queryParamTooltip'], { ns: 'tools' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_query_param}
|
||||
onChange={(e) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_query_param: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_value}
|
||||
onChange={(e) =>
|
||||
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{tempCredential.auth_type === AuthType.apiKeyHeader && (
|
||||
<>
|
||||
<Field name="api_key_header_prefix" className="contents">
|
||||
<Fieldset
|
||||
render={
|
||||
<RadioGroup<AuthHeaderPrefix>
|
||||
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
|
||||
value={tempCredential.api_key_header_prefix}
|
||||
onValueChange={(value) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_header_prefix: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
|
||||
</FieldsetLegend>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.basic}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
|
||||
}
|
||||
/>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.bearer}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
|
||||
}
|
||||
/>
|
||||
<SelectItem<AuthHeaderPrefix>
|
||||
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
value={AuthHeaderPrefix.custom}
|
||||
isChecked={
|
||||
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
|
||||
}
|
||||
/>
|
||||
</Fieldset>
|
||||
</Field>
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
|
||||
<Infotip
|
||||
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
className="ml-0.5 size-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_header}
|
||||
onChange={(e) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_header: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.apiKeyPlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_value}
|
||||
onChange={(e) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{tempCredential.auth_type === AuthType.apiKeyQuery && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
|
||||
<Infotip
|
||||
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
className="ml-0.5 size-4"
|
||||
popupClassName="w-[261px] text-text-tertiary"
|
||||
>
|
||||
{t(($) => $['createTool.authMethod.queryParamTooltip'], {
|
||||
ns: 'tools',
|
||||
})}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_query_param}
|
||||
onChange={(e) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_query_param: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="py-2 system-sm-medium text-text-primary">
|
||||
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
|
||||
</div>
|
||||
<Input
|
||||
value={tempCredential.api_key_value}
|
||||
onChange={(e) =>
|
||||
setTempCredential({
|
||||
...tempCredential,
|
||||
api_key_value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
|
||||
{
|
||||
ns: 'tools',
|
||||
},
|
||||
)!}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
<div className="mt-4 flex shrink-0 justify-end space-x-2 px-6 py-4">
|
||||
<Button onClick={onHide}>
|
||||
|
||||
@ -8,8 +8,8 @@ import {
|
||||
PreviewCardTrigger,
|
||||
} from '@langgenius/dify-ui/preview-card'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -154,7 +154,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
|
||||
) : !snippets.length ? (
|
||||
<SnippetEmptyState />
|
||||
) : (
|
||||
<ScrollAreaRoot className="relative max-h-120 max-w-125 overflow-hidden">
|
||||
<ScrollArea className="relative max-h-120 max-w-125 overflow-hidden">
|
||||
<ScrollAreaViewport ref={viewportRef}>
|
||||
<ScrollAreaContent className="p-1">
|
||||
{snippets.map((item) => {
|
||||
@ -183,7 +183,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
|
||||
<ScrollAreaScrollbar orientation="vertical">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@ -7,8 +7,8 @@ import type {
|
||||
} from '@/app/components/workflow/block-selector/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -171,7 +171,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
</BlockSelectorRow>
|
||||
|
||||
{!notShowProvider && hasAction && !isFold && (
|
||||
<ScrollAreaRoot className="relative max-h-60 overflow-hidden overscroll-contain">
|
||||
<ScrollArea className="relative max-h-60 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t(($) => $['tabs.allTriggers'], { ns: 'workflow' })}
|
||||
className="max-h-60 overscroll-contain"
|
||||
@ -194,7 +194,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
@ -49,35 +55,40 @@ export function AgentAccessPage({ agentId }: AgentAccessPageProps) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
slotClassNames={{
|
||||
content: 'px-6 pt-2 pb-8',
|
||||
}}
|
||||
>
|
||||
<div className="w-full min-w-0 space-y-6">
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<WebAppAccessCard
|
||||
agent={agentQuery.data}
|
||||
agentId={agentId}
|
||||
isLoading={agentQuery.isPending}
|
||||
/>
|
||||
<ServiceApiAccessCard agentId={agentId} />
|
||||
</div>
|
||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport>
|
||||
<ScrollAreaContent className="px-6 pt-2 pb-8">
|
||||
<div className="w-full min-w-0 space-y-6">
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<WebAppAccessCard
|
||||
agent={agentQuery.data}
|
||||
agentId={agentId}
|
||||
isLoading={agentQuery.isPending}
|
||||
/>
|
||||
<ServiceApiAccessCard agentId={agentId} />
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="agent-workflow-access-title">
|
||||
<div className="mb-3">
|
||||
<h3 id="agent-workflow-access-title" className="system-md-semibold text-text-primary">
|
||||
{t(($) => $['agentDetail.access.workflow.title'])}
|
||||
</h3>
|
||||
<p className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.access.workflow.description'])}
|
||||
</p>
|
||||
<section aria-labelledby="agent-workflow-access-title">
|
||||
<div className="mb-3">
|
||||
<h3
|
||||
id="agent-workflow-access-title"
|
||||
className="system-md-semibold text-text-primary"
|
||||
>
|
||||
{t(($) => $['agentDetail.access.workflow.title'])}
|
||||
</h3>
|
||||
<p className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['agentDetail.access.workflow.description'])}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
|
||||
</section>
|
||||
</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</AgentDetailSectionSurface>
|
||||
)
|
||||
|
||||
@ -687,7 +687,7 @@ describe('AgentConfigurePage', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should expose a single configure workspace region after loading', () => {
|
||||
it('should leave the configure landmark to the orchestrate panel after loading', () => {
|
||||
const queryClient = new QueryClient()
|
||||
mocks.queryState.composer = {
|
||||
data: {},
|
||||
@ -705,11 +705,8 @@ describe('AgentConfigurePage', () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getAllByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
screen.getByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
|
||||
).toBeVisible()
|
||||
screen.queryByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -13,7 +13,13 @@ import {
|
||||
FileTreeLabel,
|
||||
FileTreeList,
|
||||
} from '@langgenius/dify-ui/file-tree'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
type AgentFileTreeFolderOpenStrategy = (context: { file: AgentFileNode; depth: number }) => boolean
|
||||
@ -133,8 +139,7 @@ export function AgentFileTree({
|
||||
selectedFileId,
|
||||
id,
|
||||
treeLabel,
|
||||
label,
|
||||
labelledBy,
|
||||
treeLabelledBy,
|
||||
header,
|
||||
className,
|
||||
scrollAreaClassName,
|
||||
@ -153,8 +158,7 @@ export function AgentFileTree({
|
||||
selectedFileId?: string
|
||||
id?: string
|
||||
treeLabel?: string
|
||||
label?: string
|
||||
labelledBy?: string
|
||||
treeLabelledBy?: string
|
||||
header?: ReactNode
|
||||
className?: string
|
||||
scrollAreaClassName?: string
|
||||
@ -174,35 +178,36 @@ export function AgentFileTree({
|
||||
{header}
|
||||
<ScrollArea
|
||||
className={cn('min-h-0 w-full max-w-full flex-1 overflow-hidden', scrollAreaClassName)}
|
||||
label={label}
|
||||
labelledBy={labelledBy}
|
||||
slotClassNames={{
|
||||
viewport: 'max-h-[inherit]',
|
||||
content: 'w-full max-w-full min-w-0!',
|
||||
scrollbar: 'hidden',
|
||||
}}
|
||||
>
|
||||
<FileTree
|
||||
id={id}
|
||||
<ScrollAreaViewport
|
||||
aria-label={treeLabel}
|
||||
className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}
|
||||
aria-labelledby={treeLabelledBy}
|
||||
className="max-h-[inherit]"
|
||||
role={treeLabel || treeLabelledBy ? 'region' : undefined}
|
||||
>
|
||||
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
|
||||
<AgentFileTreeRows
|
||||
files={files}
|
||||
selectedFileId={selectedFileId}
|
||||
depth={1}
|
||||
folderOpenStrategy={folderOpenStrategy}
|
||||
folderOpenState={folderOpenState}
|
||||
onFolderOpenChange={onFolderOpenChange}
|
||||
onFolderDoubleClick={onFolderDoubleClick}
|
||||
onFolderOpen={onFolderOpen}
|
||||
renderFile={renderFile}
|
||||
renderFolderSuffix={renderFolderSuffix}
|
||||
renderFolderPanel={renderFolderPanel}
|
||||
/>
|
||||
</FileTreeList>
|
||||
</FileTree>
|
||||
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full">
|
||||
<FileTree id={id} className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}>
|
||||
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
|
||||
<AgentFileTreeRows
|
||||
files={files}
|
||||
selectedFileId={selectedFileId}
|
||||
depth={1}
|
||||
folderOpenStrategy={folderOpenStrategy}
|
||||
folderOpenState={folderOpenState}
|
||||
onFolderOpenChange={onFolderOpenChange}
|
||||
onFolderDoubleClick={onFolderDoubleClick}
|
||||
onFolderOpen={onFolderOpen}
|
||||
renderFile={renderFile}
|
||||
renderFolderSuffix={renderFolderSuffix}
|
||||
renderFolderPanel={renderFolderPanel}
|
||||
/>
|
||||
</FileTreeList>
|
||||
</FileTree>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="hidden">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -9,7 +9,13 @@ import type { AgentBuildDraftChangedKey } from './build-draft-changes-context'
|
||||
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { AgentComposerModel } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AgentOrchestrateAddActionsProvider } from './add-actions'
|
||||
@ -134,36 +140,40 @@ export function AgentOrchestratePanel({
|
||||
|
||||
<AgentOrchestrateReadOnlyContext value={readOnly}>
|
||||
<div aria-readonly={readOnly} className="flex min-h-0 flex-1 flex-col">
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
label={showHeader ? undefined : orchestrateLabel}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20'),
|
||||
scrollbar: hasBottomAction ? 'z-20' : undefined,
|
||||
}}
|
||||
>
|
||||
<AgentConfigApiContextProvider value={configApiContext}>
|
||||
<AgentOrchestrateAddActionsProvider>
|
||||
<AgentBuildDraftChangedKeysProvider
|
||||
changedKeys={
|
||||
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
|
||||
}
|
||||
>
|
||||
<AgentModelField
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
onSelect={onSelectModel}
|
||||
/>
|
||||
<AgentPromptEditor />
|
||||
<AgentSkills />
|
||||
<AgentFiles />
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
</AgentBuildDraftChangedKeysProvider>
|
||||
</AgentOrchestrateAddActionsProvider>
|
||||
</AgentConfigApiContextProvider>
|
||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={showHeader ? undefined : orchestrateLabel}
|
||||
aria-labelledby={showHeader ? orchestrateHeadingId : undefined}
|
||||
className="overscroll-contain"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent className={cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20')}>
|
||||
<AgentConfigApiContextProvider value={configApiContext}>
|
||||
<AgentOrchestrateAddActionsProvider>
|
||||
<AgentBuildDraftChangedKeysProvider
|
||||
changedKeys={
|
||||
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
|
||||
}
|
||||
>
|
||||
<AgentModelField
|
||||
currentModel={currentModel}
|
||||
textGenerationModelList={textGenerationModelList}
|
||||
onSelect={onSelectModel}
|
||||
/>
|
||||
<AgentPromptEditor />
|
||||
<AgentSkills />
|
||||
<AgentFiles />
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
</AgentBuildDraftChangedKeysProvider>
|
||||
</AgentOrchestrateAddActionsProvider>
|
||||
</AgentConfigApiContextProvider>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className={hasBottomAction ? 'z-20' : undefined}>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</AgentOrchestrateReadOnlyContext>
|
||||
|
||||
@ -10,8 +10,14 @@ import {
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { FileTreeFile } from '@langgenius/dify-ui/file-tree'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useCallback, useId } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AgentFileTree } from '../files/tree'
|
||||
@ -113,7 +119,7 @@ function AgentSkillFileList({
|
||||
<AgentFileTree
|
||||
files={files}
|
||||
selectedFileId={selectedFileId}
|
||||
labelledBy="agent-skill-detail-files-heading"
|
||||
treeLabelledBy="agent-skill-detail-files-heading"
|
||||
className={cn('h-full bg-background-section p-1', fileListTreeClassName)}
|
||||
listClassName={fileListTreeListClassName}
|
||||
scrollAreaClassName="flex-1"
|
||||
@ -337,8 +343,8 @@ export function AgentSkillDetailDialog({
|
||||
skillName: string
|
||||
detail: AgentSkillDetail
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const dialogTitleId = useId()
|
||||
const previewTitle = detail.filePreview?.fileName
|
||||
const isHeaderDownloadLoading = detail.filePreview?.downloadActionLoadingTarget === 'header'
|
||||
|
||||
@ -355,7 +361,9 @@ export function AgentSkillDetailDialog({
|
||||
)}
|
||||
>
|
||||
<DialogDescription className="sr-only">{detail.description}</DialogDescription>
|
||||
<DialogTitle className="sr-only">{previewTitle || skillName}</DialogTitle>
|
||||
<DialogTitle id={dialogTitleId} className="sr-only">
|
||||
{previewTitle || skillName}
|
||||
</DialogTitle>
|
||||
<div className="min-h-0 w-full">
|
||||
<AgentSkillFileList
|
||||
fileListHeader={detail.fileListHeader}
|
||||
@ -408,35 +416,42 @@ export function AgentSkillDetailDialog({
|
||||
<DialogCloseButton className="static size-7 shrink-0 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea
|
||||
className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid"
|
||||
label={t(($) => $['agentDetail.configure.skills.detail.contentRegion'])}
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain outline-none focus-visible:outline-none',
|
||||
content: 'flex min-h-full w-full max-w-full min-w-0 flex-col gap-2',
|
||||
}}
|
||||
>
|
||||
{detail.filePreview && (
|
||||
<AgentFilePreviewContent
|
||||
binary={detail.filePreview.binary}
|
||||
content={detail.filePreview.content}
|
||||
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
|
||||
downloadUrl={detail.filePreview.downloadUrl}
|
||||
fileName={detail.filePreview.fileName}
|
||||
imageData={detail.filePreview.imageData}
|
||||
isDownloadError={detail.filePreview.isDownloadError}
|
||||
isDownloadLoading={detail.filePreview.isDownloadLoading}
|
||||
isError={detail.filePreview.isError}
|
||||
isImage={detail.filePreview.isImage}
|
||||
isLoading={detail.filePreview.isLoading}
|
||||
onDownloadFile={detail.onDownloadFile}
|
||||
/>
|
||||
)}
|
||||
{detail.sections.map((section) => (
|
||||
<div key={section.id} className="px-4">
|
||||
<AgentSkillDetailSectionBlock section={section} />
|
||||
</div>
|
||||
))}
|
||||
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid">
|
||||
<ScrollAreaViewport
|
||||
aria-labelledby={dialogTitleId}
|
||||
className="overscroll-contain outline-none focus-visible:outline-none"
|
||||
role="region"
|
||||
>
|
||||
<ScrollAreaContent
|
||||
style={{ minWidth: 0 }}
|
||||
className="flex min-h-full w-full max-w-full flex-col gap-2"
|
||||
>
|
||||
{detail.filePreview && (
|
||||
<AgentFilePreviewContent
|
||||
binary={detail.filePreview.binary}
|
||||
content={detail.filePreview.content}
|
||||
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
|
||||
downloadUrl={detail.filePreview.downloadUrl}
|
||||
fileName={detail.filePreview.fileName}
|
||||
imageData={detail.filePreview.imageData}
|
||||
isDownloadError={detail.filePreview.isDownloadError}
|
||||
isDownloadLoading={detail.filePreview.isDownloadLoading}
|
||||
isError={detail.filePreview.isError}
|
||||
isImage={detail.filePreview.isImage}
|
||||
isLoading={detail.filePreview.isLoading}
|
||||
onDownloadFile={detail.onDownloadFile}
|
||||
/>
|
||||
)}
|
||||
{detail.sections.map((section) => (
|
||||
<div key={section.id} className="px-4">
|
||||
<AgentSkillDetailSectionBlock section={section} />
|
||||
</div>
|
||||
))}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type AgentConfigureWorkspaceProps = {
|
||||
'aria-busy'?: boolean
|
||||
@ -19,11 +18,8 @@ export function AgentConfigureWorkspace({
|
||||
rightPanel,
|
||||
sidePanels,
|
||||
}: AgentConfigureWorkspaceProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={t(($) => $['agentDetail.sections.configure'])}
|
||||
aria-busy={ariaBusy}
|
||||
className={cn(
|
||||
'flex h-full min-w-0 flex-1 gap-1 overflow-hidden bg-background-body p-1',
|
||||
|
||||
@ -3,8 +3,8 @@ import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -52,7 +52,7 @@ export function AgentLogsTable({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t(($) => $['agentDetail.logs.title'])}
|
||||
role="region"
|
||||
@ -78,7 +78,7 @@ export function AgentLogsTable({
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,7 +3,13 @@
|
||||
import type { AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -129,46 +135,48 @@ export function AgentMonitoringPage({ agentId }: AgentMonitoringPageProps) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1 overflow-hidden"
|
||||
slotClassNames={{
|
||||
content: 'px-6 pt-2 pb-3',
|
||||
}}
|
||||
>
|
||||
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
|
||||
{shouldShowError && (
|
||||
<AgentMonitoringState>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
void statisticsQuery.refetch()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
</AgentMonitoringState>
|
||||
)}
|
||||
{!shouldShowInitialSkeleton && !shouldShowError && (
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
{metrics.map((metric) => (
|
||||
<AgentMonitoringChart
|
||||
key={metric.id}
|
||||
titleKey={metric.titleKey}
|
||||
explanationKey={metric.explanationKey}
|
||||
summaryValue={metric.summaryValue}
|
||||
rows={metric.rows}
|
||||
chartType={metric.chartType}
|
||||
valueKey={metric.valueKey}
|
||||
unitKey={metric.unitKey}
|
||||
yMaxWhenEmpty={metric.yMaxWhenEmpty}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport>
|
||||
<ScrollAreaContent className="px-6 pt-2 pb-3">
|
||||
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
|
||||
{shouldShowError && (
|
||||
<AgentMonitoringState>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
void statisticsQuery.refetch()
|
||||
}}
|
||||
>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
</AgentMonitoringState>
|
||||
)}
|
||||
{!shouldShowInitialSkeleton && !shouldShowError && (
|
||||
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
{metrics.map((metric) => (
|
||||
<AgentMonitoringChart
|
||||
key={metric.id}
|
||||
titleKey={metric.titleKey}
|
||||
explanationKey={metric.explanationKey}
|
||||
summaryValue={metric.summaryValue}
|
||||
rows={metric.rows}
|
||||
chartType={metric.chartType}
|
||||
valueKey={metric.valueKey}
|
||||
unitKey={metric.unitKey}
|
||||
yMaxWhenEmpty={metric.yMaxWhenEmpty}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</AgentDetailSectionSurface>
|
||||
)
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { RosterFilterValue } from './components/roster-filter'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
@ -108,7 +108,7 @@ export default function RosterPage() {
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1">
|
||||
<ScrollAreaRoot className="relative h-full min-h-0 min-w-0 overflow-hidden">
|
||||
<ScrollArea className="relative h-full min-h-0 min-w-0 overflow-hidden">
|
||||
<ScrollAreaViewport tabIndex={-1} className="overscroll-contain">
|
||||
<ScrollAreaContent className="min-h-full px-8 pt-2 pb-8">
|
||||
<AgentRosterList
|
||||
@ -127,7 +127,7 @@ export default function RosterPage() {
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,7 +3,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { GuideStep } from '@/features/deployments/create-guide/state/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import {
|
||||
ScrollArea,
|
||||
ScrollAreaContent,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TitleTooltip } from '@/features/deployments/shared/components/title-tooltip'
|
||||
|
||||
@ -161,14 +167,13 @@ export function GuideCard({
|
||||
return (
|
||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col">
|
||||
{contentScrollable ? (
|
||||
<ScrollArea
|
||||
className="min-h-0 flex-1"
|
||||
slotClassNames={{
|
||||
viewport: 'overscroll-contain',
|
||||
content: 'min-h-full pt-0.5 pb-6',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ScrollAreaViewport className="overscroll-contain">
|
||||
<ScrollAreaContent className="min-h-full pt-0.5 pb-6">{children}</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="min-h-0 flex-1 overflow-hidden pt-0.5 pb-6">{children}</div>
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "يشغل Preview الوكيل المكتمل كما سيراه المستخدمون، مع ردود واضحة وميزات الدردشة.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "عاين وكيلك",
|
||||
"agentDetail.configure.skills.add": "إضافة مهارة",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "محتوى تفاصيل المهارة",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ملفات",
|
||||
"agentDetail.configure.skills.detail.files": "الملفات",
|
||||
"agentDetail.configure.skills.empty.description": "تمنح المهارات الوكيل خبرة قابلة لإعادة الاستخدام يمكنه استدعاؤها أثناء العمل",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview führt den fertigen Agenten so aus, wie deine Benutzer ihn sehen, mit klaren Antworten und Chat-Funktionen.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Agenten vorschauen",
|
||||
"agentDetail.configure.skills.add": "Skill hinzufügen",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill-Detailinhalt",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATEIEN",
|
||||
"agentDetail.configure.skills.detail.files": "Dateien",
|
||||
"agentDetail.configure.skills.empty.description": "Skills geben dem Agenten wiederverwendbare Fachkenntnisse, die er bei der Arbeit nutzen kann",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview runs the finished agent the way your users will see it, with clean replies and chat features.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Preview your agent",
|
||||
"agentDetail.configure.skills.add": "Add skill",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill detail content",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILES",
|
||||
"agentDetail.configure.skills.detail.files": "Files",
|
||||
"agentDetail.configure.skills.empty.description": "Skills give the agent reusable expertise it can call while working",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview ejecuta el agente terminado como lo verán tus usuarios, con respuestas claras y funciones de chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Previsualiza tu agente",
|
||||
"agentDetail.configure.skills.add": "Agregar habilidad",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Contenido de los detalles de la habilidad",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARCHIVOS",
|
||||
"agentDetail.configure.skills.detail.files": "Archivos",
|
||||
"agentDetail.configure.skills.empty.description": "Las habilidades le dan al agente experiencia reutilizable que puede invocar mientras trabaja",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview عامل تکمیلشده را همانطور که کاربران میبینند اجرا میکند، با پاسخهای تمیز و قابلیتهای گفتگو.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "پیشنمایش عامل",
|
||||
"agentDetail.configure.skills.add": "افزودن مهارت",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "محتوای جزئیات مهارت",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} فایل",
|
||||
"agentDetail.configure.skills.detail.files": "فایلها",
|
||||
"agentDetail.configure.skills.empty.description": "مهارتها به عامل تخصص قابل استفاده مجدد میدهند که هنگام کار میتواند فراخوانی کند",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview exécute l’agent final comme vos utilisateurs le verront, avec des réponses claires et les fonctions de chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Prévisualiser votre agent",
|
||||
"agentDetail.configure.skills.add": "Ajouter une compétence",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Contenu des détails de la compétence",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FICHIERS",
|
||||
"agentDetail.configure.skills.detail.files": "Fichiers",
|
||||
"agentDetail.configure.skills.empty.description": "Les compétences offrent à l’agent une expertise réutilisable qu’il peut invoquer pendant son travail",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview तैयार agent को वैसे चलाता है जैसे आपके users उसे देखेंगे, साफ replies और chat features के साथ।",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "अपने agent का preview करें",
|
||||
"agentDetail.configure.skills.add": "कौशल जोड़ें",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "कौशल विवरण सामग्री",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} फ़ाइलें",
|
||||
"agentDetail.configure.skills.detail.files": "फ़ाइलें",
|
||||
"agentDetail.configure.skills.empty.description": "कौशल एजेंट को पुनः उपयोग योग्य विशेषज्ञता देते हैं जिसे वह काम करते समय कॉल कर सकता है",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview menjalankan agent yang sudah selesai seperti yang akan dilihat pengguna, dengan balasan rapi dan fitur chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Pratinjau agen Anda",
|
||||
"agentDetail.configure.skills.add": "Tambahkan keterampilan",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Konten detail keterampilan",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
|
||||
"agentDetail.configure.skills.detail.files": "File",
|
||||
"agentDetail.configure.skills.empty.description": "Keterampilan memberi agen keahlian yang dapat digunakan kembali yang bisa dipanggil saat bekerja",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview esegue l’agente completato come lo vedranno gli utenti, con risposte pulite e funzionalità di chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Anteprima del tuo agente",
|
||||
"agentDetail.configure.skills.add": "Aggiungi abilità",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Contenuto dei dettagli dell’abilità",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
|
||||
"agentDetail.configure.skills.detail.files": "File",
|
||||
"agentDetail.configure.skills.empty.description": "Le abilità forniscono all’agente competenze riutilizzabili che può richiamare durante il lavoro",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview は完成したエージェントをユーザーに見える形で実行し、整った返信とチャット機能を確認できます。",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "エージェントをプレビュー",
|
||||
"agentDetail.configure.skills.add": "スキルを追加",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "スキル詳細コンテンツ",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個のファイル",
|
||||
"agentDetail.configure.skills.detail.files": "ファイル",
|
||||
"agentDetail.configure.skills.empty.description": "スキルはエージェントが作業中に呼び出せる再利用可能な専門知識を提供します",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview는 사용자가 보게 될 방식으로 완성된 에이전트를 실행하며, 깔끔한 답변과 채팅 기능을 보여줍니다.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "에이전트 미리보기",
|
||||
"agentDetail.configure.skills.add": "스킬 추가",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "스킬 상세 내용",
|
||||
"agentDetail.configure.skills.detail.fileCount": "파일 {{count}}개",
|
||||
"agentDetail.configure.skills.detail.files": "파일",
|
||||
"agentDetail.configure.skills.empty.description": "스킬은 에이전트가 작업하면서 호출할 수 있는 재사용 가능한 전문 능력을 제공합니다",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview voert de voltooide agent uit zoals je gebruikers die zien, met duidelijke antwoorden en chatfuncties.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Voorbeeld van je agent",
|
||||
"agentDetail.configure.skills.add": "Skill toevoegen",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill-detailinhoud",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} BESTANDEN",
|
||||
"agentDetail.configure.skills.detail.files": "Bestanden",
|
||||
"agentDetail.configure.skills.empty.description": "Skills geven de agent herbruikbare expertise die hij tijdens het werken kan inzetten",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview uruchamia gotowego agenta tak, jak zobaczą go użytkownicy, z przejrzystymi odpowiedziami i funkcjami czatu.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Podgląd agenta",
|
||||
"agentDetail.configure.skills.add": "Dodaj umiejętność",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Zawartość szczegółów umiejętności",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} PLIKÓW",
|
||||
"agentDetail.configure.skills.detail.files": "Pliki",
|
||||
"agentDetail.configure.skills.empty.description": "Umiejętności dają agentowi reużywalną wiedzę, którą może wywołać podczas pracy",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview executa o agente finalizado como seus usuários o verão, com respostas claras e recursos de chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Pré-visualize seu agente",
|
||||
"agentDetail.configure.skills.add": "Adicionar habilidade",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Conteúdo dos detalhes da habilidade",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARQUIVOS",
|
||||
"agentDetail.configure.skills.detail.files": "Arquivos",
|
||||
"agentDetail.configure.skills.empty.description": "As habilidades dão ao agente expertise reutilizável que ele pode invocar enquanto trabalha",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview rulează agentul finalizat așa cum îl vor vedea utilizatorii, cu răspunsuri clare și funcții de chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Previzualizează agentul",
|
||||
"agentDetail.configure.skills.add": "Adaugă abilitate",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Conținutul detaliilor abilității",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} FIȘIERE",
|
||||
"agentDetail.configure.skills.detail.files": "Fișiere",
|
||||
"agentDetail.configure.skills.empty.description": "Abilitățile oferă agentului expertiză reutilizabilă pe care o poate apela în timpul lucrului",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускает готового агента так, как его увидят пользователи, с чистыми ответами и функциями чата.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Предпросмотр агента",
|
||||
"agentDetail.configure.skills.add": "Добавить навык",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Содержимое деталей навыка",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛОВ",
|
||||
"agentDetail.configure.skills.detail.files": "Файлы",
|
||||
"agentDetail.configure.skills.empty.description": "Навыки дают агенту переиспользуемую экспертизу, которую он может вызывать в работе",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview zažene dokončanega agenta tako, kot ga bodo videli uporabniki, z jasnimi odgovori in funkcijami klepeta.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Predogled agenta",
|
||||
"agentDetail.configure.skills.add": "Dodaj veščino",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Vsebina podrobnosti veščine",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATOTEK",
|
||||
"agentDetail.configure.skills.detail.files": "Datoteke",
|
||||
"agentDetail.configure.skills.empty.description": "Veščine agentu dajejo ponovno uporabno strokovnost, ki jo lahko kliče med delom",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview เรียกใช้เอเจนต์ที่เสร็จแล้วในแบบที่ผู้ใช้จะเห็น พร้อมคำตอบที่ชัดเจนและฟีเจอร์แชท",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "ดูตัวอย่างเอเจนต์",
|
||||
"agentDetail.configure.skills.add": "เพิ่มทักษะ",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "เนื้อหารายละเอียดทักษะ",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ไฟล์",
|
||||
"agentDetail.configure.skills.detail.files": "ไฟล์",
|
||||
"agentDetail.configure.skills.empty.description": "ทักษะให้ตัวแทนมีความเชี่ยวชาญที่นำกลับมาใช้ใหม่ได้ซึ่งสามารถเรียกใช้ได้ขณะทำงาน",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview, tamamlanan ajanı kullanıcılarınızın göreceği şekilde, temiz yanıtlar ve sohbet özellikleriyle çalıştırır.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Ajanınızı önizleyin",
|
||||
"agentDetail.configure.skills.add": "Beceri ekle",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Beceri detay içeriği",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} DOSYA",
|
||||
"agentDetail.configure.skills.detail.files": "Dosyalar",
|
||||
"agentDetail.configure.skills.empty.description": "Beceriler, ajana çalışırken çağırabileceği yeniden kullanılabilir uzmanlık sağlar",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускає готового агента так, як його побачать користувачі, з чистими відповідями та функціями чату.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Попередній перегляд агента",
|
||||
"agentDetail.configure.skills.add": "Додати навичку",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Вміст деталей навички",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛІВ",
|
||||
"agentDetail.configure.skills.detail.files": "Файли",
|
||||
"agentDetail.configure.skills.empty.description": "Навички дають агенту перевикористовну експертизу, яку він може викликати під час роботи",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "Preview chạy agent hoàn chỉnh theo cách người dùng sẽ thấy, với câu trả lời rõ ràng và các tính năng chat.",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "Xem trước agent",
|
||||
"agentDetail.configure.skills.add": "Thêm kỹ năng",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Nội dung chi tiết kỹ năng",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} TỆP",
|
||||
"agentDetail.configure.skills.detail.files": "Tệp",
|
||||
"agentDetail.configure.skills.empty.description": "Kỹ năng mang đến cho tác nhân chuyên môn có thể tái sử dụng mà nó có thể gọi khi làm việc",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "像用户一样测试它。你的交互不会影响 Agent 后续的行为。",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "预览你的 Agent",
|
||||
"agentDetail.configure.skills.add": "添加 Skill",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill 详情内容",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} 个文件",
|
||||
"agentDetail.configure.skills.detail.files": "文件",
|
||||
"agentDetail.configure.skills.empty.description": "Skill 为 Agent 提供工作时可调用的可复用专业能力",
|
||||
|
||||
@ -203,7 +203,6 @@
|
||||
"agentDetail.configure.rightPanel.previewTipBody": "像使用者一樣測試它。你的互動不會影響 Agent 後續的行為。",
|
||||
"agentDetail.configure.rightPanel.previewTipTitle": "預覽你的 Agent",
|
||||
"agentDetail.configure.skills.add": "新增 Skill",
|
||||
"agentDetail.configure.skills.detail.contentRegion": "Skill 詳情內容",
|
||||
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個檔案",
|
||||
"agentDetail.configure.skills.detail.files": "檔案",
|
||||
"agentDetail.configure.skills.empty.description": "Skill 為 Agent 提供工作時可呼叫的可複用專業能力",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user