refactor(web): drop headless-ui, migrate overlay to dify-ui (#35963)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-05-09 10:33:25 +00:00
committed by GitHub
co-authored by CodingOnStar autofix-ci[bot] yyh yyh
parent f720a3bed2
commit 8581a68174
187 changed files with 5333 additions and 6395 deletions
@@ -2,10 +2,10 @@
import type { FC } from 'react'
import type { Dependency } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic'
import ReadyToInstall from './ready-to-install'
@@ -50,26 +50,31 @@ const InstallBundle: FC<Props> = ({
}, [step, t])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
<DialogContent className={cn('relative w-full max-w-[480px] overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div>
</div>
<ReadyToInstall
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
allPlugins={fromDSLPayload}
onClose={onClose}
/>
</Modal>
<ReadyToInstall
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
allPlugins={fromDSLPayload}
onClose={onClose}
/>
</DialogContent>
</Dialog>
)
}
@@ -3,11 +3,11 @@
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import type { InstallState } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { InstallStepFromGitHub } from '../../types'
import Installed from '../base/installed'
@@ -160,74 +160,80 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
}
return (
<Modal
isShow={true}
onClose={foldAnimInto}
className={cn(modalClassName, `shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px]
border-components-panel-border bg-components-panel-bg p-0`)}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="flex grow flex-col items-start gap-1">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
<div className="self-stretch system-xs-regular text-text-tertiary">
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
<DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, `shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px]
border-components-panel-border bg-components-panel-bg p-0`))}
>
<DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="flex grow flex-col items-start gap-1">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
<div className="self-stretch system-xs-regular text-text-tertiary">
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
</div>
</div>
</div>
</div>
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
? (
<Installed
payload={manifest}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
errMsg={errorMsg}
onCancel={onClose}
/>
)
: (
<div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
updatePayload={updatePayload!}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
versions={versions}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))}
selectedPackage={state.selectedPackage}
packages={packages}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))}
onUploaded={handleUploaded}
onFailed={handleUploadFail}
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}
onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled}
onFailed={handleFailed}
/>
)}
</div>
)}
</Modal>
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
? (
<Installed
payload={manifest}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
errMsg={errorMsg}
onCancel={onClose}
/>
)
: (
<div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
updatePayload={updatePayload!}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
versions={versions}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))}
selectedPackage={state.selectedPackage}
packages={packages}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))}
onUploaded={handleUploaded}
onFailed={handleUploadFail}
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}
onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled}
onFailed={handleFailed}
/>
)}
</div>
)}
</DialogContent>
</Dialog>
)
}
@@ -2,10 +2,10 @@
import type { Dependency, PluginDeclaration } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic'
@@ -86,52 +86,57 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
}, [])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
<DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div>
</div>
{step === InstallStep.uploading && (
<Uploading
isBundle={isBundle}
file={file}
onCancel={onClose}
onPackageUploaded={handlePackageUploaded}
onBundleUploaded={handleBundleUploaded}
onFailed={handleUploadFail}
/>
)}
{isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies}
/>
)
: (
<ReadyToInstallPackage
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
uniqueIdentifier={uniqueIdentifier}
manifest={manifest}
errorMsg={errorMsg}
onError={setErrorMsg}
/>
)}
</Modal>
{step === InstallStep.uploading && (
<Uploading
isBundle={isBundle}
file={file}
onCancel={onClose}
onPackageUploaded={handlePackageUploaded}
onBundleUploaded={handleBundleUploaded}
onFailed={handleUploadFail}
/>
)}
{isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies}
/>
)
: (
<ReadyToInstallPackage
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
uniqueIdentifier={uniqueIdentifier}
manifest={manifest}
errorMsg={errorMsg}
onError={setErrorMsg}
/>
)}
</DialogContent>
</Dialog>
)
}
@@ -161,11 +161,9 @@ describe('Uploading', () => {
})
})
// NOTE: The uploadFile API has an unconventional contract where it always rejects.
// Success vs failure is determined by whether response.message exists:
// - If response.message exists → treated as failure (calls onFailed)
// - If response.message is absent → treated as success (calls onPackageUploaded/onBundleUploaded)
// This explains why we use mockRejectedValue for "success" scenarios below.
// NOTE: Some upload endpoints have historically returned successful plugin upload
// payloads through rejected XHR objects, so the component accepts both resolved
// responses and rejected responses without an error message.
it('should call onPackageUploaded when upload rejects without error message (success case)', async () => {
const mockResult = {
@@ -193,6 +191,30 @@ describe('Uploading', () => {
})
})
it('should call onPackageUploaded when upload resolves with package response', async () => {
const mockResult = {
unique_identifier: 'test-uid',
manifest: createMockManifest(),
}
mockUploadFile.mockResolvedValue(mockResult)
const onPackageUploaded = vi.fn()
render(
<Uploading
{...defaultProps}
isBundle={false}
onPackageUploaded={onPackageUploaded}
/>,
)
await waitFor(() => {
expect(onPackageUploaded).toHaveBeenCalledWith({
uniqueIdentifier: mockResult.unique_identifier,
manifest: mockResult.manifest,
})
})
})
it('should call onBundleUploaded when upload rejects without error message (success case)', async () => {
const mockDependencies = createMockDependencies()
mockUploadFile.mockRejectedValue({
@@ -212,6 +234,24 @@ describe('Uploading', () => {
expect(onBundleUploaded).toHaveBeenCalledWith(mockDependencies)
})
})
it('should call onBundleUploaded when upload resolves with bundle response', async () => {
const mockDependencies = createMockDependencies()
mockUploadFile.mockResolvedValue(mockDependencies)
const onBundleUploaded = vi.fn()
render(
<Uploading
{...defaultProps}
isBundle
onBundleUploaded={onBundleUploaded}
/>,
)
await waitFor(() => {
expect(onBundleUploaded).toHaveBeenCalledWith(mockDependencies)
})
})
})
// ================================
@@ -260,35 +300,48 @@ describe('Uploading', () => {
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should handle empty response gracefully', async () => {
it('should fail gracefully when upload response is empty', async () => {
mockUploadFile.mockRejectedValue({
response: {},
})
const onPackageUploaded = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)
const onFailed = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} onFailed={onFailed} />)
await waitFor(() => {
expect(onPackageUploaded).toHaveBeenCalledWith({
uniqueIdentifier: undefined,
manifest: undefined,
})
expect(onPackageUploaded).not.toHaveBeenCalled()
expect(onFailed).toHaveBeenCalledWith('plugin.installModal.uploadFailed')
})
})
it('should handle response with only unique_identifier', async () => {
it('should fail gracefully when upload response has no manifest', async () => {
mockUploadFile.mockRejectedValue({
response: { unique_identifier: 'only-uid' },
})
const onPackageUploaded = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} />)
const onFailed = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} onFailed={onFailed} />)
await waitFor(() => {
expect(onPackageUploaded).toHaveBeenCalledWith({
uniqueIdentifier: 'only-uid',
manifest: undefined,
})
expect(onPackageUploaded).not.toHaveBeenCalled()
expect(onFailed).toHaveBeenCalledWith('plugin.installModal.uploadFailed')
})
})
it('should fail gracefully when upload response is null', async () => {
mockUploadFile.mockRejectedValue({
response: null,
})
const onPackageUploaded = vi.fn()
const onFailed = vi.fn()
render(<Uploading {...defaultProps} onPackageUploaded={onPackageUploaded} onFailed={onFailed} />)
await waitFor(() => {
expect(onPackageUploaded).not.toHaveBeenCalled()
expect(onFailed).toHaveBeenCalledWith('plugin.installModal.uploadFailed')
})
})
@@ -1,8 +1,7 @@
'use client'
import type { FC } from 'react'
import type { Dependency, PluginDeclaration } from '../../../types'
import type { Dependency, Plugin, PluginDeclaration } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { uploadFile } from '@/service/plugins'
@@ -10,6 +9,40 @@ import Card from '../../../card'
const i18nPrefix = 'installModal'
type PackageUploadResponse = {
unique_identifier: string
manifest: PluginDeclaration
}
type UploadFailureResponse = {
message?: string
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function isPackageUploadResponse(value: unknown): value is PackageUploadResponse {
if (!isObject(value))
return false
return typeof value.unique_identifier === 'string' && isObject(value.manifest)
}
function getRejectedResponse(error: unknown): unknown {
if (!isObject(error) || !('response' in error))
return undefined
return error.response
}
function getUploadFailureMessage(response: unknown): string | undefined {
if (!isObject(response))
return undefined
return (response as UploadFailureResponse).message
}
type Props = {
isBundle: boolean
file: File
@@ -32,36 +65,50 @@ const Uploading: FC<Props> = ({
}) => {
const { t } = useTranslation()
const fileName = file.name
const handleUpload = async () => {
const handleUploadedResponse = React.useCallback((response: unknown) => {
if (isBundle) {
if (Array.isArray(response)) {
onBundleUploaded(response as Dependency[])
return
}
onFailed(t(`${i18nPrefix}.uploadFailed`, { ns: 'plugin' }))
return
}
if (!isPackageUploadResponse(response)) {
onFailed(t(`${i18nPrefix}.uploadFailed`, { ns: 'plugin' }))
return
}
onPackageUploaded({
uniqueIdentifier: response.unique_identifier,
manifest: response.manifest,
})
}, [isBundle, onBundleUploaded, onFailed, onPackageUploaded, t])
const handleUpload = React.useCallback(async () => {
try {
await uploadFile(file, isBundle)
handleUploadedResponse(await uploadFile(file, isBundle))
}
catch (e: any) {
if (e.response?.message) {
onFailed(e.response?.message)
}
else { // Why it would into this branch?
const res = e.response
if (isBundle) {
onBundleUploaded(res)
return
}
onPackageUploaded({
uniqueIdentifier: res.unique_identifier,
manifest: res.manifest,
})
catch (error: unknown) {
const response = getRejectedResponse(error)
const message = getUploadFailureMessage(response)
if (message) {
onFailed(message)
return
}
handleUploadedResponse(response)
}
}
}, [file, handleUploadedResponse, isBundle, onFailed])
React.useEffect(() => {
handleUpload()
}, [])
}, [handleUpload])
return (
<>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
<div className="flex items-center gap-1 self-stretch">
<RiLoader2Line className="h-4 w-4 animate-spin-slow text-text-accent" />
<span className="i-ri-loader-2-line h-4 w-4 animate-spin-slow text-text-accent" />
<div className="system-md-regular text-text-secondary">
{t(`${i18nPrefix}.uploadingPackage`, {
ns: 'plugin',
@@ -72,7 +119,7 @@ const Uploading: FC<Props> = ({
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
<Card
className="w-full"
payload={{ name: fileName } as any}
payload={{ name: fileName } as Plugin}
isLoading
loadingFileName={fileName}
installed={false}
@@ -2,10 +2,10 @@
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types'
import Installed from '../base/installed'
import useHideLogic from '../hooks/use-hide-logic'
@@ -70,60 +70,64 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
}, [setIsInstalling])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
wrapperClassName="z-9999"
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
<DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div>
</div>
{
isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies!}
isFromMarketPlace
/>
)
: (
<>
{
step === InstallStep.readyToInstall && (
<Install
uniqueIdentifier={uniqueIdentifier}
payload={manifest!}
onCancel={onClose}
onInstalled={handleInstalled}
onFailed={handleFailed}
onStartToInstall={handleStartToInstall}
/>
)
}
{
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
<Installed
payload={manifest!}
isMarketPayload
isFailed={step === InstallStep.installFailed}
errMsg={errorMsg}
onCancel={onSuccess}
/>
)
}
</>
)
}
</Modal>
{
isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies!}
isFromMarketPlace
/>
)
: (
<>
{
step === InstallStep.readyToInstall && (
<Install
uniqueIdentifier={uniqueIdentifier}
payload={manifest!}
onCancel={onClose}
onInstalled={handleInstalled}
onFailed={handleFailed}
onStartToInstall={handleStartToInstall}
/>
)
}
{
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
<Installed
payload={manifest!}
isMarketPayload
isFailed={step === InstallStep.installFailed}
errMsg={errorMsg}
onCancel={onSuccess}
/>
)
}
</>
)
}
</DialogContent>
</Dialog>
)
}
@@ -2,14 +2,11 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SchemaModal from '../schema-modal'
vi.mock('@/app/components/base/modal', () => ({
default: ({
children,
isShow,
}: {
children: React.ReactNode
isShow: boolean
}) => isShow ? <div data-testid="modal">{children}</div> : null,
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
open === false ? null : <>{children}</>,
DialogContent: ({ children }: { children: React.ReactNode }) => <div data-testid="modal">{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor', () => ({
@@ -2,9 +2,9 @@ import type { UseMutationResult } from '@tanstack/react-query'
import type { FC, ReactNode } from 'react'
import type { Plugin } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { memo } from 'react'
import Modal from '@/app/components/base/modal'
import Card from '@/app/components/plugins/card'
type Props = {
@@ -33,45 +33,52 @@ const PluginMutationModal: FC<Props> = ({
modalBottomLeft,
}: Props) => {
return (
<Modal
isShow={true}
onClose={onCancel}
className="min-w-[560px]"
closable
title={modelTitle}
<Dialog
open
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="mt-3 mb-2 system-md-regular text-text-secondary">
{description}
</div>
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
<Card
installed={mutation.isSuccess}
payload={plugin}
className="w-full"
titleLeft={cardTitleLeft}
/>
</div>
<div className="flex items-center gap-2 self-stretch pt-5">
<div>
{modalBottomLeft}
<DialogContent className="w-full min-w-[560px] overflow-hidden! border-none text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{modelTitle}
</DialogTitle>
<div className="mt-3 mb-2 system-md-regular text-text-secondary">
{description}
</div>
<div className="ml-auto flex gap-2">
{!mutation.isPending && (
<Button onClick={onCancel}>
{cancelButtonText}
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
<Card
installed={mutation.isSuccess}
payload={plugin}
className="w-full"
titleLeft={cardTitleLeft}
/>
</div>
<div className="flex items-center gap-2 self-stretch pt-5">
<div>
{modalBottomLeft}
</div>
<div className="ml-auto flex gap-2">
{!mutation.isPending && (
<Button onClick={onCancel}>
{cancelButtonText}
</Button>
)}
<Button
variant="primary"
loading={mutation.isPending}
onClick={mutate}
disabled={mutation.isPending}
>
{confirmButtonText}
</Button>
)}
<Button
variant="primary"
loading={mutation.isPending}
onClick={mutate}
disabled={mutation.isPending}
>
{confirmButtonText}
</Button>
</div>
</div>
</div>
</Modal>
</DialogContent>
</Dialog>
)
}
@@ -2,17 +2,19 @@ import { render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../../base/modal', () => ({
default: ({ children, title, isShow }: { children: React.ReactNode, title: string, isShow: boolean }) => (
isShow
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) => (
open !== false
? (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
</div>
)
: null
),
DialogContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div data-testid="modal-title">{children}</div>,
DialogCloseButton: () => <button type="button">Close</button>,
}))
vi.mock('../../base/key-value-item', () => ({
@@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '../../base/modal'
import KeyValueItem from '../base/key-value-item'
import { convertRepoToUrl } from '../install-plugin/utils'
@@ -23,19 +23,26 @@ const PlugInfo: FC<Props> = ({
const { t } = useTranslation()
const labelWidthClassName = 'w-[96px]'
return (
<Modal
title={t(`${i18nPrefix}.title`, { ns: 'plugin' })}
className="w-[480px]"
isShow
onClose={onHide}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div className="mt-5 space-y-3">
{repository && <KeyValueItem label={t(`${i18nPrefix}.repository`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={`${convertRepoToUrl(repository)}`} valueMaxWidthClassName="max-w-[190px]" />}
{release && <KeyValueItem label={t(`${i18nPrefix}.release`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={release} />}
{packageName && <KeyValueItem label={t(`${i18nPrefix}.packageName`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={packageName} />}
</div>
</Modal>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`${i18nPrefix}.title`, { ns: 'plugin' })}
</DialogTitle>
<div className="mt-5 space-y-3">
{repository && <KeyValueItem label={t(`${i18nPrefix}.repository`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={`${convertRepoToUrl(repository)}`} valueMaxWidthClassName="max-w-[190px]" />}
{release && <KeyValueItem label={t(`${i18nPrefix}.release`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={release} />}
{packageName && <KeyValueItem label={t(`${i18nPrefix}.packageName`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={packageName} />}
</div>
</DialogContent>
</Dialog>
)
}
export default React.memo(PlugInfo)
@@ -14,28 +14,25 @@ const mockSystemFeatures = { enable_marketplace: true }
const render = (ui: ReactElement) =>
renderWithSystemFeatures(ui, { systemFeatures: mockSystemFeatures })
// Mock Modal component
vi.mock('@/app/components/base/modal', () => ({
default: ({ children, isShow, onClose, closable, className }: {
let mockDialogOnOpenChange: ((open: boolean) => void) | undefined
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open, onOpenChange }: {
children: React.ReactNode
isShow: boolean
onClose: () => void
closable?: boolean
className?: string
open?: boolean
onOpenChange?: (open: boolean) => void
}) => {
if (!isShow)
return null
return (
<div data-testid="modal" className={className}>
{closable && (
<button data-testid="modal-close" onClick={onClose}>
Close
</button>
)}
{children}
</div>
)
mockDialogOnOpenChange = onOpenChange
return open === false ? null : <>{children}</>
},
DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
<div data-testid="modal" className={className}>{children}</div>
),
DialogCloseButton: () => (
<button data-testid="modal-close" onClick={() => mockDialogOnOpenChange?.(false)}>
Close
</button>
),
}))
// Mock OptionCard component
@@ -143,7 +143,6 @@ const AutoUpdateSetting: FC<Props> = ({
timezone={timezone}
onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))}
onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))}
popupClassName="z-99"
title={t(`${i18nPrefix}.updateTime`, { ns: 'plugin' })}
minuteFilter={minuteFilter}
renderTrigger={renderTimePickerTrigger}
@@ -3,11 +3,11 @@ import type { FC } from 'react'
import type { AutoUpdateConfig } from './auto-update-setting/types'
import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { PermissionType } from '@/app/components/plugins/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { systemFeaturesQueryOptions } from '@/service/system-features'
@@ -53,59 +53,64 @@ const PluginSettingModal: FC<Props> = ({
}, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
return (
<Modal
isShow
onClose={onHide}
closable
className="w-[620px] max-w-[620px] p-0!"
<Dialog
open
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div className="shadows-shadow-xl flex w-full flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg">
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<span className="self-stretch title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
</div>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
{[
{ title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne },
{ title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
].map(({ title, key, value }) => (
<div key={key} className="flex flex-col items-start gap-1 self-stretch">
<Label label={title} />
<div className="flex w-full items-start justify-between gap-2">
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
<OptionCard
key={option}
title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })}
onSelect={() => handlePrivilegeChange(key)(option)}
selected={value === option}
className="flex-1"
/>
))}
<DialogContent className="w-[620px] max-w-[620px] overflow-hidden! border-none p-0! text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<div className="shadows-shadow-xl flex w-full flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg">
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<span className="self-stretch title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
</div>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
{[
{ title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne },
{ title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
].map(({ title, key, value }) => (
<div key={key} className="flex flex-col items-start gap-1 self-stretch">
<Label label={title} />
<div className="flex w-full items-start justify-between gap-2">
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
<OptionCard
key={option}
title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })}
onSelect={() => handlePrivilegeChange(key)(option)}
selected={value === option}
className="flex-1"
/>
))}
</div>
</div>
</div>
))}
))}
</div>
{
enable_marketplace && (
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
)
}
<div className="flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button
className="min-w-[72px]"
onClick={onHide}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
className="min-w-[72px]"
variant="primary"
onClick={handleSave}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div>
{
enable_marketplace && (
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
)
}
<div className="flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button
className="min-w-[72px]"
onClick={onHide}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
className="min-w-[72px]"
variant="primary"
onClick={handleSave}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div>
</Modal>
</DialogContent>
</Dialog>
)
}