refactor(auth): update OAuth button and settings modal for improved state management and UI consistency (#35702)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, {{defaultContext}}:api, Dockerfile, DIFY_API_IMAGE_NAME, linux/amd64, depot-ubuntu-24.04-4, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, {{defaultContext}}:api, Dockerfile, DIFY_API_IMAGE_NAME, linux/arm64, depot-ubuntu-24.04-4, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, {{defaultContext}}, web/Dockerfile, DIFY_WEB_IMAGE_NAME, linux/amd64, depot-ubuntu-24.04-4, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, {{defaultContext}}, web/Dockerfile, DIFY_WEB_IMAGE_NAME, linux/arm64, depot-ubuntu-24.04-4, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / fork-build-validate ({{defaultContext}}, web/Dockerfile, validate-web-amd64) (push) Has been cancelled
Build and Push API & Web / fork-build-validate ({{defaultContext}}:api, Dockerfile, validate-api-amd64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Main CI Pipeline / Skip Duplicate Checks (push) Has been cancelled
Main CI Pipeline / Check Changed Files (push) Has been cancelled
Main CI Pipeline / Run API Tests (push) Has been cancelled
Main CI Pipeline / Skip API Tests (push) Has been cancelled
Main CI Pipeline / API Tests (push) Has been cancelled
Main CI Pipeline / Run Web Tests (push) Has been cancelled
Main CI Pipeline / Skip Web Tests (push) Has been cancelled
Main CI Pipeline / Web Tests (push) Has been cancelled
Main CI Pipeline / Run Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Skip Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Web Full-Stack E2E (push) Has been cancelled
Main CI Pipeline / Style Check (push) Has been cancelled
Main CI Pipeline / Run VDB Tests (push) Has been cancelled
Main CI Pipeline / Skip VDB Tests (push) Has been cancelled
Main CI Pipeline / VDB Tests (push) Has been cancelled
Main CI Pipeline / Run DB Migration Test (push) Has been cancelled
Main CI Pipeline / Skip DB Migration Test (push) Has been cancelled
Main CI Pipeline / DB Migration Test (push) Has been cancelled

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-04-29 23:54:15 +08:00
committed by GitHub
parent e751ec323e
commit 9f47317032
7 changed files with 517 additions and 112 deletions

View File

@@ -3182,24 +3182,11 @@
"count": 1
}
},
"web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx": {
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/plugins/plugin-auth/authorize/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/plugins/plugin-auth/authorized-in-node.tsx": {
"ts/no-explicit-any": {
"count": 1

View File

@@ -1,10 +1,14 @@
import { fireEvent, render, screen } from '@testing-library/react'
import type { OAuthClientSettingsProps } from '../oauth-client-settings'
import type { FormSchema } from '@/app/components/base/form/types'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
const mockGetPluginOAuthUrl = vi.fn().mockResolvedValue({ authorization_url: 'https://auth.example.com' })
const mockOpenOAuthPopup = vi.fn()
const mockWriteText = vi.fn()
const mockOAuthClientSettingsProps: OAuthClientSettingsProps[] = []
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (obj: Record<string, string> | string) => typeof obj === 'string' ? obj : obj.en_US || '',
@@ -31,11 +35,37 @@ vi.mock('../../hooks/use-credential', () => ({
}))
vi.mock('../oauth-client-settings', () => ({
default: ({ onClose }: { onClose: () => void }) => (
<div data-testid="oauth-settings-modal">
<button data-testid="oauth-settings-close" onClick={onClose}>Close</button>
</div>
),
default: (props: OAuthClientSettingsProps) => {
mockOAuthClientSettingsProps.push(props)
const {
open = true,
onClose,
onOpenChange,
schemas,
} = props
if (!open)
return null
const handleClose = () => {
onOpenChange?.(false)
onClose?.()
}
return (
<div data-testid="oauth-settings-modal">
<button data-testid="oauth-settings-close" onClick={handleClose}>Close</button>
{schemas.map(schema => (
<div key={schema.name} data-testid={`oauth-schema-${schema.name}`}>
<div data-testid={`oauth-schema-label-${schema.name}`}>
{React.isValidElement(schema.label) ? schema.label : String(schema.label || '')}
</div>
{String(schema.default || '')}
</div>
))}
</div>
)
},
}))
vi.mock('@/app/components/base/form/types', () => ({
@@ -56,6 +86,11 @@ describe('AddOAuthButton', () => {
beforeEach(async () => {
vi.clearAllMocks()
mockOAuthClientSettingsProps.length = 0
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: mockWriteText },
})
const mod = await import('../add-oauth-button')
AddOAuthButton = mod.default
})
@@ -72,6 +107,7 @@ describe('AddOAuthButton', () => {
fireEvent.click(screen.getByTestId('oauth-settings-button'))
expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument()
expect(mockOAuthClientSettingsProps.at(-1)?.open).toBe(true)
})
it('should close OAuth settings modal', () => {
@@ -84,13 +120,37 @@ describe('AddOAuthButton', () => {
})
it('should trigger OAuth flow on main button click', async () => {
const mockOnUpdate = vi.fn()
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" onUpdate={mockOnUpdate} />)
const button = screen.getByText('Use OAuth').closest('button')
if (button)
fireEvent.click(button)
await waitFor(() => {
expect(mockOpenOAuthPopup).toHaveBeenCalledWith('https://auth.example.com', expect.any(Function))
})
const handleOAuthSuccess = mockOpenOAuthPopup.mock.calls[0]?.[1]
expect(handleOAuthSuccess).toBeTypeOf('function')
if (typeof handleOAuthSuccess === 'function')
handleOAuthSuccess()
expect(mockOnUpdate).toHaveBeenCalled()
})
it('should not open OAuth popup when authorization URL is missing', async () => {
mockGetPluginOAuthUrl.mockResolvedValueOnce({})
render(<AddOAuthButton pluginPayload={basePayload} buttonText="Use OAuth" />)
const button = screen.getByText('Use OAuth').closest('button')
if (button)
fireEvent.click(button)
expect(mockGetPluginOAuthUrl).toHaveBeenCalled()
await waitFor(() => {
expect(mockGetPluginOAuthUrl).toHaveBeenCalled()
})
expect(mockOpenOAuthPopup).not.toHaveBeenCalled()
})
it('should be disabled when disabled prop is true', () => {
@@ -99,4 +159,96 @@ describe('AddOAuthButton', () => {
const button = screen.getByText('Use OAuth').closest('button')
expect(button).toBeDisabled()
})
it('should open OAuth settings from setup entry when OAuth is not configured', () => {
render(
<AddOAuthButton
pluginPayload={basePayload}
oAuthData={{
schema: [],
is_oauth_custom_client_enabled: false,
is_system_oauth_params_exists: false,
client_params: {},
}}
/>,
)
fireEvent.click(screen.getByText('plugin.auth.setupOAuth'))
expect(screen.getByTestId('oauth-settings-modal')).toBeInTheDocument()
expect(mockOAuthClientSettingsProps.at(-1)?.editValues).toMatchObject({
__oauth_client__: 'custom',
})
})
it('should show custom badge when OAuth custom client is enabled', () => {
render(
<AddOAuthButton
pluginPayload={basePayload}
buttonText="Use OAuth"
oAuthData={{
schema: [],
is_oauth_custom_client_enabled: true,
is_system_oauth_params_exists: true,
client_params: {},
}}
/>,
)
expect(screen.getByText('plugin.auth.custom')).toBeInTheDocument()
})
it('should build custom OAuth schema and edit values for settings modal', () => {
const schema = [
{
name: 'client_id',
label: { en_US: 'Client ID' },
type: 'text-input',
required: true,
default: 'schema-client-id',
},
] as FormSchema[]
render(
<AddOAuthButton
pluginPayload={basePayload}
buttonText="Use OAuth"
oAuthData={{
schema,
is_oauth_custom_client_enabled: true,
is_system_oauth_params_exists: true,
client_params: { client_id: 'stored-client-id' },
redirect_uri: 'https://redirect.example.com',
}}
/>,
)
fireEvent.click(screen.getByTestId('oauth-settings-button'))
const settingsProps = mockOAuthClientSettingsProps.at(-1)
expect(settingsProps?.editValues).toMatchObject({
__oauth_client__: 'custom',
client_id: 'stored-client-id',
})
expect(settingsProps?.hasOriginalClientParams).toBe(true)
expect(settingsProps?.schemas[0]).toMatchObject({
name: '__oauth_client__',
default: 'custom',
})
expect(settingsProps?.schemas[1]).toMatchObject({
name: 'client_id',
default: 'stored-client-id',
show_on: [
{
variable: '__oauth_client__',
value: 'custom',
},
],
})
expect(screen.getByText('https://redirect.example.com')).toBeInTheDocument()
fireEvent.click(within(screen.getByTestId('oauth-schema-label-client_id')).getByRole('button'))
expect(mockWriteText).toHaveBeenCalledWith('https://redirect.example.com')
})
})

View File

@@ -1,5 +1,6 @@
import type { ApiKeyModalProps } from '../api-key-modal'
import type { FormSchema } from '@/app/components/base/form/types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -384,6 +385,29 @@ describe('ApiKeyModal', () => {
expect(mockOnClose).toHaveBeenCalled()
})
it('should close on backdrop click when nested inside another dialog', async () => {
const mockOnClose = vi.fn()
render(
<Dialog open>
<DialogContent backdropClassName="bg-transparent">
<ControlledModalHarness ApiKeyModal={ApiKeyModal} onClose={mockOnClose} />
</DialogContent>
</Dialog>,
)
const backdrop = document.querySelector('.bg-background-overlay')
expect(backdrop).toBeInTheDocument()
fireEvent.pointerDown(backdrop!)
fireEvent.mouseDown(backdrop!)
fireEvent.click(backdrop!)
await waitFor(() => {
expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false')
})
expect(mockOnClose).toHaveBeenCalled()
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(<ApiKeyModal pluginPayload={payload} />)

View File

@@ -1,4 +1,8 @@
import type { OAuthClientSettingsProps } from '../oauth-client-settings'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthCategory } from '../../types'
@@ -20,7 +24,8 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
const mockSetPluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockDeletePluginOAuthCustomClient = vi.fn().mockResolvedValue({})
const mockInvalidPluginOAuthClientSchema = vi.fn()
const mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
let mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
let mockAuthFormProps: Record<string, unknown> | undefined
vi.mock('../../hooks/use-credential', () => ({
useSetPluginOAuthCustomClientHook: () => ({
@@ -40,36 +45,19 @@ vi.mock('../../../readme-panel/store', () => ({
ReadmeShowType: { modal: 'modal' },
}))
vi.mock('@/app/components/base/modal/modal', () => ({
default: ({ children, title, onClose: _onClose, onConfirm, onCancel, onExtraButtonClick, footerSlot }: {
children: React.ReactNode
title: string
onClose?: () => void
onConfirm?: () => void
onCancel?: () => void
onExtraButtonClick?: () => void
footerSlot?: React.ReactNode
[key: string]: unknown
}) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
<button data-testid="modal-confirm" onClick={onConfirm}>Save And Auth</button>
<button data-testid="modal-cancel" onClick={onCancel}>Save Only</button>
<button data-testid="modal-close" onClick={onExtraButtonClick}>Cancel</button>
{!!footerSlot && <div data-testid="footer-slot">{footerSlot}</div>}
</div>
),
}))
vi.mock('@/app/components/base/form/form-scenarios/auth', () => ({
default: React.forwardRef((_props: Record<string, unknown>, ref: React.Ref<unknown>) => {
vi.mock('@/app/components/base/form/form-scenarios/auth', () => {
const MockAuthForm = ({ ref, ...props }: { ref?: React.Ref<unknown> } & Record<string, unknown>) => {
mockAuthFormProps = props
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
return <div data-testid="auth-form" />
}),
}))
}
return {
default: MockAuthForm,
}
})
vi.mock('@tanstack/react-form', () => ({
useForm: (config: Record<string, unknown>) => ({
@@ -89,11 +77,72 @@ const defaultSchemas = [
{ name: 'client_id', label: 'Client ID', type: 'text-input', required: true },
] as never
const PopoverSettingsHarness = ({
OAuthClientSettings,
onClose,
onPopoverClose,
}: {
OAuthClientSettings: React.FC<OAuthClientSettingsProps>
onClose: () => void
onPopoverClose: () => void
}) => {
const [open, setOpen] = React.useState(true)
return (
<Popover
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen)
if (!nextOpen)
onPopoverClose()
}}
>
<PopoverTrigger render={<button type="button">OAuth</button>} />
<PopoverContent>
<div data-testid="oauth-popover">
<OAuthClientSettings
open={open}
onOpenChange={setOpen}
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={onClose}
/>
</div>
</PopoverContent>
</Popover>
)
}
const ControlledSettingsHarness = ({
OAuthClientSettings,
onClose,
}: {
OAuthClientSettings: React.FC<OAuthClientSettingsProps>
onClose: () => void
}) => {
const [open, setOpen] = React.useState(true)
return (
<>
<div data-testid="modal-open-state">{String(open)}</div>
<OAuthClientSettings
open={open}
onOpenChange={setOpen}
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={onClose}
/>
</>
)
}
describe('OAuthClientSettings', () => {
let OAuthClientSettings: (typeof import('../oauth-client-settings'))['default']
let OAuthClientSettings: React.FC<OAuthClientSettingsProps>
beforeEach(async () => {
vi.clearAllMocks()
mockFormValues = { isCheckValidated: true, values: { __oauth_client__: 'custom', client_id: 'test-id' } }
mockAuthFormProps = undefined
const mod = await import('../oauth-client-settings')
OAuthClientSettings = mod.default
})
@@ -120,6 +169,36 @@ describe('OAuthClientSettings', () => {
expect(screen.getByTestId('auth-form')).toBeInTheDocument()
})
it('should render backdrop when nested inside another dialog', () => {
render(
<Dialog open>
<DialogContent backdropClassName="bg-transparent">
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
/>
</DialogContent>
</Dialog>,
)
expect(document.querySelector('.bg-background-overlay')).toBeInTheDocument()
})
it('should pass schema defaults to auth form', () => {
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={[
{ name: 'client_id', label: 'Client ID', type: 'text-input', required: true, default: 'default-client-id' },
] as never}
/>,
)
expect(mockAuthFormProps?.defaultValues).toMatchObject({
client_id: 'default-client-id',
})
})
it('should call onClose when cancel clicked', () => {
const mockOnClose = vi.fn()
render(
@@ -134,6 +213,33 @@ describe('OAuthClientSettings', () => {
expect(mockOnClose).toHaveBeenCalled()
})
it('should close through controlled open state when cancel clicked', async () => {
const mockOnClose = vi.fn()
render(<ControlledSettingsHarness OAuthClientSettings={OAuthClientSettings} onClose={mockOnClose} />)
fireEvent.click(screen.getByTestId('modal-close'))
await waitFor(() => {
expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false')
})
expect(mockOnClose).toHaveBeenCalled()
})
it('should close when backdrop is clicked', async () => {
const mockOnClose = vi.fn()
render(<ControlledSettingsHarness OAuthClientSettings={OAuthClientSettings} onClose={mockOnClose} />)
const backdrop = document.querySelector('.bg-background-overlay')
expect(backdrop).toBeInTheDocument()
fireEvent.click(backdrop!)
await waitFor(() => {
expect(screen.getByTestId('modal-open-state')).toHaveTextContent('false')
})
expect(mockOnClose).toHaveBeenCalled()
})
it('should save settings on save only button click', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
@@ -155,6 +261,38 @@ describe('OAuthClientSettings', () => {
})
})
it('should ignore duplicate save clicks while action is pending', async () => {
const mockOnClose = vi.fn()
let resolveSave: (value: object) => void = () => {}
mockSetPluginOAuthCustomClient.mockImplementationOnce(() => new Promise((resolve) => {
resolveSave = resolve
}))
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
onClose={mockOnClose}
/>,
)
fireEvent.click(screen.getByTestId('modal-cancel'))
await waitFor(() => {
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledTimes(1)
})
fireEvent.click(screen.getByTestId('modal-cancel'))
expect(mockSetPluginOAuthCustomClient).toHaveBeenCalledTimes(1)
resolveSave({})
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalled()
})
})
it('should save and authorize on confirm button click', async () => {
const mockOnAuth = vi.fn().mockResolvedValue(undefined)
render(
@@ -172,6 +310,34 @@ describe('OAuthClientSettings', () => {
})
})
it('should remove custom client settings', async () => {
const mockOnClose = vi.fn()
const mockOnUpdate = vi.fn()
render(
<OAuthClientSettings
pluginPayload={basePayload}
schemas={defaultSchemas}
editValues={{ client_id: 'test-id' }}
hasOriginalClientParams
onClose={mockOnClose}
onUpdate={mockOnUpdate}
/>,
)
fireEvent.click(screen.getByTestId('modal-extra'))
await waitFor(() => {
expect(mockDeletePluginOAuthCustomClient).toHaveBeenCalled()
})
expect(mockOnClose).toHaveBeenCalled()
expect(mockOnUpdate).toHaveBeenCalled()
expect(mockInvalidPluginOAuthClientSchema).toHaveBeenCalled()
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
message: 'common.api.actionSuccess',
type: 'success',
}))
})
it('should render readme entrance when detail is provided', () => {
const payload = { ...basePayload, detail: { name: 'Test' } as never }
render(
@@ -183,4 +349,26 @@ describe('OAuthClientSettings', () => {
expect(screen.getByTestId('readme-entrance')).toBeInTheDocument()
})
it('should stay open when clicking inside the modal from a popover', async () => {
const user = userEvent.setup()
const mockOnClose = vi.fn()
const mockOnPopoverClose = vi.fn()
render(
<PopoverSettingsHarness
OAuthClientSettings={OAuthClientSettings}
onClose={mockOnClose}
onPopoverClose={mockOnPopoverClose}
/>,
)
const form = await screen.findByTestId('auth-form')
await user.click(form)
expect(mockOnClose).not.toHaveBeenCalled()
expect(mockOnPopoverClose).not.toHaveBeenCalled()
expect(screen.getByTestId('modal')).toBeInTheDocument()
})
})

View File

@@ -3,11 +3,6 @@ import type { PluginPayload } from '../types'
import type { FormSchema } from '@/app/components/base/form/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiClipboardLine,
RiEqualizer2Line,
RiInformation2Fill,
} from '@remixicon/react'
import {
memo,
useCallback,
@@ -40,10 +35,12 @@ export type AddOAuthButtonProps = {
schema?: FormSchema[]
is_oauth_custom_client_enabled?: boolean
is_system_oauth_params_exists?: boolean
client_params?: Record<string, any>
client_params?: Record<string, unknown>
redirect_uri?: string
}
}
type OAuthData = NonNullable<AddOAuthButtonProps['oAuthData']>
const AddOAuthButton = ({
pluginPayload,
buttonVariant = 'primary',
@@ -59,22 +56,27 @@ const AddOAuthButton = ({
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
const [isOAuthSettingsMounted, setIsOAuthSettingsMounted] = useState(false)
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload)
const mergedOAuthData = useMemo(() => {
const mergedOAuthData = useMemo<OAuthData>(() => {
if (oAuthData)
return oAuthData
return data
return data || {}
}, [oAuthData, data])
const {
schema = [],
is_oauth_custom_client_enabled,
is_system_oauth_params_exists,
client_params,
client_params = {},
redirect_uri,
} = mergedOAuthData as any || {}
} = mergedOAuthData
const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled
const openOAuthSettings = useCallback(() => {
setIsOAuthSettingsMounted(true)
setIsOAuthSettingsOpen(true)
}, [])
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl()
@@ -91,7 +93,7 @@ const AddOAuthButton = ({
<div className="w-full">
<div className="mb-4 flex rounded-xl bg-background-section-burn p-4">
<div className="mr-3 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg">
<RiInformation2Fill className="h-5 w-5 text-text-accent" />
<span className="i-ri-information-2-fill h-5 w-5 text-text-accent" />
</div>
<div className="w-0 grow">
<div className="mb-1.5 system-sm-regular">
@@ -107,7 +109,7 @@ const AddOAuthButton = ({
navigator.clipboard.writeText(redirect_uri || '')
}}
>
<RiClipboardLine className="h-4 w-4" />
<span className="i-ri-clipboard-line h-4 w-4" />
</ActionButton>
</div>
)
@@ -232,10 +234,10 @@ const AddOAuthButton = ({
)}
onClick={(e) => {
e.stopPropagation()
setIsOAuthSettingsOpen(true)
openOAuthSettings()
}}
>
<RiEqualizer2Line className="h-4 w-4" />
<span className="i-ri-equalizer-2-line h-4 w-4" />
</div>
</Button>
)
@@ -244,18 +246,20 @@ const AddOAuthButton = ({
!isConfigured && (
<Button
variant={buttonVariant}
onClick={() => setIsOAuthSettingsOpen(true)}
onClick={openOAuthSettings}
disabled={disabled}
className="w-full"
>
<RiEqualizer2Line className="mr-0.5 h-4 w-4" />
<span className="mr-0.5 i-ri-equalizer-2-line h-4 w-4" />
{t('auth.setupOAuth', { ns: 'plugin' })}
</Button>
)
}
{
isOAuthSettingsOpen && (
isOAuthSettingsMounted && (
<OAuthClientSettings
open={isOAuthSettingsOpen}
onOpenChange={setIsOAuthSettingsOpen}
pluginPayload={pluginPayload}
onClose={() => setIsOAuthSettingsOpen(false)}
disabled={disabled || isLoading}

View File

@@ -140,7 +140,10 @@ const ApiKeyModal = ({
open={open}
onOpenChange={handleOpenChange}
>
<DialogContent className="w-[640px]! max-w-[calc(100vw-2rem)]! p-0!">
<DialogContent
backdropProps={{ forceRender: true }}
className="w-[640px]! max-w-[calc(100vw-2rem)]! p-0!"
>
<div data-testid="modal" className="flex max-h-[80dvh] flex-col">
<div className="relative shrink-0 p-6 pr-14 pb-3">
<DialogTitle data-testid="modal-title" className="title-2xl-semi-bold text-text-primary">

View File

@@ -4,6 +4,7 @@ import type {
FormSchema,
} from '@/app/components/base/form/types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import {
useForm,
@@ -17,7 +18,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import Modal from '@/app/components/base/modal/modal'
import { ReadmeEntrance } from '../../readme-panel/entrance'
import { ReadmeShowType } from '../../readme-panel/store'
import {
@@ -26,10 +26,12 @@ import {
useSetPluginOAuthCustomClientHook,
} from '../hooks/use-credential'
type OAuthClientSettingsProps = {
export type OAuthClientSettingsProps = {
pluginPayload: PluginPayload
open?: boolean
onOpenChange?: (open: boolean) => void
onClose?: () => void
editValues?: Record<string, any>
editValues?: Record<string, unknown>
disabled?: boolean
schemas: FormSchema[]
onAuth?: () => Promise<void>
@@ -38,6 +40,8 @@ type OAuthClientSettingsProps = {
}
const OAuthClientSettings = ({
pluginPayload,
open = true,
onOpenChange,
onClose,
editValues,
disabled,
@@ -53,11 +57,16 @@ const OAuthClientSettings = ({
doingActionRef.current = value
setDoingAction(value)
}, [])
const handleOpenChange = useCallback((nextOpen: boolean) => {
onOpenChange?.(nextOpen)
if (!nextOpen)
onClose?.()
}, [onClose, onOpenChange])
const defaultValues = schemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
}, {} as Record<string, unknown>)
const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload)
const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload)
const formRef = useRef<FormRefObject>(null)
@@ -87,6 +96,7 @@ const OAuthClientSettings = ({
})
toast.success(t('api.actionSuccess', { ns: 'common' }))
onOpenChange?.(false)
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
@@ -94,7 +104,7 @@ const OAuthClientSettings = ({
finally {
handleSetDoingAction(false)
}
}, [onClose, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, t, handleSetDoingAction])
}, [onClose, onOpenChange, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, t, handleSetDoingAction])
const handleConfirmAndAuthorize = useCallback(async () => {
await handleConfirm()
@@ -110,6 +120,7 @@ const OAuthClientSettings = ({
handleSetDoingAction(true)
await deletePluginOAuthCustomClient()
toast.success(t('api.actionSuccess', { ns: 'common' }))
onOpenChange?.(false)
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
@@ -117,53 +128,89 @@ const OAuthClientSettings = ({
finally {
handleSetDoingAction(false)
}
}, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, t, handleSetDoingAction, onClose])
}, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, t, handleSetDoingAction, onClose, onOpenChange])
const form = useForm({
defaultValues: editValues || defaultValues,
})
const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__)
const isDisabled = disabled || doingAction
return (
<Modal
title={t('auth.oauthClientSettings', { ns: 'plugin' })}
confirmButtonText={t('auth.saveAndAuth', { ns: 'plugin' })}
cancelButtonText={t('auth.saveOnly', { ns: 'plugin' })}
extraButtonText={t('operation.cancel', { ns: 'common' })}
showExtraButton
extraButtonVariant="secondary"
onExtraButtonClick={onClose}
onClose={onClose}
onCancel={handleConfirm}
onConfirm={handleConfirmAndAuthorize}
disabled={disabled || doingAction}
footerSlot={
__oauth_client__ === 'custom' && hasOriginalClientParams && (
<div className="grow">
<Button
variant="secondary"
className="text-components-button-destructive-secondary-text"
disabled={disabled || doingAction || !editValues}
onClick={handleRemove}
>
{t('operation.remove', { ns: 'common' })}
</Button>
</div>
)
}
containerClassName="pt-0"
wrapperClassName="z-1002!"
clickOutsideNotClose={true}
<Dialog
open={open}
onOpenChange={handleOpenChange}
>
{pluginPayload.detail && (
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
)}
<AuthForm
formFromProps={form}
ref={formRef}
formSchemas={schemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
</Modal>
<DialogContent
backdropProps={{ forceRender: true }}
className="w-[480px]! max-w-[calc(100vw-2rem)]! p-0!"
>
<div data-testid="modal" className="flex max-h-[80dvh] flex-col">
<div className="relative shrink-0 p-6 pr-14 pb-3">
<DialogTitle data-testid="modal-title" className="title-2xl-semi-bold text-text-primary">
{t('auth.oauthClientSettings', { ns: 'plugin' })}
</DialogTitle>
<DialogCloseButton
data-testid="modal-x-close"
className="top-5 right-5 h-8 w-8 rounded-lg"
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-3 pt-0">
{pluginPayload.detail && (
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
)}
<AuthForm
formFromProps={form}
ref={formRef}
formSchemas={schemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
</div>
<div className="flex shrink-0 justify-between p-6 pt-5">
<div>
{__oauth_client__ === 'custom' && hasOriginalClientParams && (
<Button
data-testid="modal-extra"
variant="secondary"
className="text-components-button-destructive-secondary-text"
disabled={isDisabled || !editValues}
onClick={handleRemove}
>
{t('operation.remove', { ns: 'common' })}
</Button>
)}
</div>
<div className="flex items-center">
<Button
data-testid="modal-close"
variant="secondary"
onClick={() => handleOpenChange(false)}
disabled={isDisabled}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<div className="mx-3 h-4 w-px bg-divider-regular"></div>
<Button
data-testid="modal-cancel"
onClick={handleConfirm}
disabled={isDisabled}
>
{t('auth.saveOnly', { ns: 'plugin' })}
</Button>
<Button
data-testid="modal-confirm"
className="ml-2"
variant="primary"
onClick={handleConfirmAndAuthorize}
disabled={isDisabled}
>
{t('auth.saveAndAuth', { ns: 'plugin' })}
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}