mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 00:45:53 +08:00
feat: use-uploader hook
This commit is contained in:
parent
684896d100
commit
a9e367e6de
@ -1,97 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useContext } from 'use-context-selector'
|
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
file: File | undefined
|
|
||||||
updateFile: (file?: File) => void
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Uploader: FC<Props> = ({
|
|
||||||
file,
|
|
||||||
updateFile,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const { notify } = useContext(ToastContext)
|
|
||||||
const [dragging, setDragging] = useState(false)
|
|
||||||
const dropRef = useRef<HTMLDivElement>(null)
|
|
||||||
const dragRef = useRef<HTMLDivElement>(null)
|
|
||||||
const fileUploader = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
const handleDragEnter = (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
e.target !== dragRef.current && setDragging(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragOver = (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragLeave = (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
e.target === dragRef.current && setDragging(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
setDragging(false)
|
|
||||||
if (!e.dataTransfer)
|
|
||||||
return
|
|
||||||
const files = [...e.dataTransfer.files]
|
|
||||||
if (files.length > 1) {
|
|
||||||
// notify({ type: 'error', message: })
|
|
||||||
}
|
|
||||||
updateFile(files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectHandle = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const currentFile = e.target.files?.[0]
|
|
||||||
updateFile(currentFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dropRef.current?.addEventListener('dragenter', handleDragEnter)
|
|
||||||
dropRef.current?.addEventListener('dragover', handleDragOver)
|
|
||||||
dropRef.current?.addEventListener('dragleave', handleDragLeave)
|
|
||||||
dropRef.current?.addEventListener('drop', handleDrop)
|
|
||||||
return () => {
|
|
||||||
dropRef.current?.removeEventListener('dragenter', handleDragEnter)
|
|
||||||
dropRef.current?.removeEventListener('dragover', handleDragOver)
|
|
||||||
dropRef.current?.removeEventListener('dragleave', handleDragLeave)
|
|
||||||
dropRef.current?.removeEventListener('drop', handleDrop)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
ref={fileUploader}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
type="file"
|
|
||||||
id="fileUploader"
|
|
||||||
accept='.difypkg'
|
|
||||||
onChange={fileChangeHandle}
|
|
||||||
/>
|
|
||||||
{dragging && (
|
|
||||||
<div
|
|
||||||
ref={dragRef}
|
|
||||||
className='flex w-full h-full p-2 items-start gap-2 absolute left-1 bottom-[3px]
|
|
||||||
rounded-2xl border-2 border-dashed bg-components-dropzone-bg-accent'>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(Uploader)
|
|
@ -1,18 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiArrowRightUpLine,
|
RiArrowRightUpLine,
|
||||||
RiBugLine,
|
RiBugLine,
|
||||||
RiClipboardLine,
|
RiClipboardLine,
|
||||||
|
RiDragDropLine,
|
||||||
RiEqualizer2Line,
|
RiEqualizer2Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import InstallFromLocalPackage from '../install-plugin/install-from-local-package'
|
||||||
import {
|
import {
|
||||||
PluginPageContextProvider,
|
PluginPageContextProvider,
|
||||||
usePluginPageContext,
|
usePluginPageContext,
|
||||||
} from './context'
|
} from './context'
|
||||||
import InstallPluginDropdown from './install-plugin-dropdown'
|
import InstallPluginDropdown from './install-plugin-dropdown'
|
||||||
|
import { useUploader } from './use-uploader'
|
||||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
@ -31,9 +34,15 @@ const PluginPage = ({
|
|||||||
}: PluginPageProps) => {
|
}: PluginPageProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setShowPluginSettingModal } = useModalContext() as any
|
const { setShowPluginSettingModal } = useModalContext() as any
|
||||||
|
const [currentFile, setCurrentFile] = useState<File | null>(null)
|
||||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||||
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
const scrollDisabled = usePluginPageContext(v => v.scrollDisabled)
|
||||||
|
|
||||||
|
const { dragging, fileUploader, fileChangeHandle, removeFile } = useUploader({
|
||||||
|
onFileChange: setCurrentFile,
|
||||||
|
containerRef,
|
||||||
|
})
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ value: 'plugins', text: t('common.menus.plugins') },
|
{ value: 'plugins', text: t('common.menus.plugins') },
|
||||||
@ -117,12 +126,35 @@ const PluginPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
{activeTab === 'plugins' && (
|
||||||
activeTab === 'plugins' && plugins
|
<>
|
||||||
}
|
{plugins}
|
||||||
|
{dragging && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 m-0.5 p-2 rounded-2xl bg-[rgba(21,90,239,0.14)] border-2
|
||||||
|
border-dashed border-components-dropzone-border-accent">
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`flex py-4 justify-center items-center gap-2 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}>
|
||||||
|
<RiDragDropLine className="w-4 h-4" />
|
||||||
|
<span className="system-xs-regular">Drop plugin package here to install</span>
|
||||||
|
</div>
|
||||||
|
{currentFile && (
|
||||||
|
<InstallFromLocalPackage file={currentFile} onClose={removeFile} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
activeTab === 'discover' && marketplace
|
activeTab === 'discover' && marketplace
|
||||||
}
|
}
|
||||||
|
<input
|
||||||
|
ref={fileUploader}
|
||||||
|
className="hidden"
|
||||||
|
type="file"
|
||||||
|
id="fileUploader"
|
||||||
|
accept='.difypkg'
|
||||||
|
onChange={fileChangeHandle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
|
import { RiAddLine, RiArrowDownSLine } from '@remixicon/react'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
@ -10,13 +10,17 @@ import InstallFromMarketplace from '@/app/components/plugins/install-plugin/inst
|
|||||||
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
|
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
|
||||||
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
|
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
|
||||||
const InstallPluginDropdown = () => {
|
const InstallPluginDropdown = () => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
const [selectedAction, setSelectedAction] = useState<string | null>(null)
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0]
|
||||||
@ -27,34 +31,26 @@ const InstallPluginDropdown = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (menuRef.current && !menuRef.current.contains(event.target as Node))
|
|
||||||
setIsMenuOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={menuRef}>
|
<PortalToFollowElem
|
||||||
|
open={isMenuOpen}
|
||||||
|
onOpenChange={setIsMenuOpen}
|
||||||
|
placement='bottom-start'
|
||||||
|
offset={4}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<PortalToFollowElemTrigger onClick={() => setIsMenuOpen(v => !v)}>
|
||||||
<Button
|
<Button
|
||||||
className={cn('w-full h-full p-2 text-components-button-secondary-text', isMenuOpen && 'bg-state-base-hover')}
|
className={cn('w-full h-full p-2 text-components-button-secondary-text', isMenuOpen && 'bg-state-base-hover')}
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
||||||
>
|
>
|
||||||
<RiAddLine className='w-4 h-4' />
|
<RiAddLine className='w-4 h-4' />
|
||||||
<span className='pl-1'>Install plugin</span>
|
<span className='pl-1'>Install plugin</span>
|
||||||
<RiArrowDownSLine className='w-4 h-4 ml-1' />
|
<RiArrowDownSLine className='w-4 h-4 ml-1' />
|
||||||
</Button>
|
</Button>
|
||||||
{isMenuOpen && (
|
</PortalToFollowElemTrigger>
|
||||||
<div className='flex flex-col items-start absolute z-1000 top-full left-0 mt-1 p-1 pb-2
|
<PortalToFollowElemContent className='z-[1002]'>
|
||||||
w-[200px] bg-components-panel-bg-blur border border-components-panel-border rounded-xl
|
<div className='flex flex-col p-1 pb-2 items-start w-[200px] bg-components-panel-bg-blur border border-components-panel-border rounded-xl shadows-shadow-lg'>
|
||||||
shadows-shadow-lg'>
|
<span className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch text-text-tertiary system-xs-medium-uppercase'>
|
||||||
<span className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch text-text-tertiary
|
|
||||||
system-xs-medium-uppercase'>
|
|
||||||
Install Form
|
Install Form
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@ -64,6 +60,7 @@ const InstallPluginDropdown = () => {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
accept='.difypkg'
|
accept='.difypkg'
|
||||||
/>
|
/>
|
||||||
|
<div className='p-1 w-full'>
|
||||||
{[
|
{[
|
||||||
{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' },
|
{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' },
|
||||||
{ icon: Github, text: 'GitHub', action: 'github' },
|
{ icon: Github, text: 'GitHub', action: 'github' },
|
||||||
@ -87,7 +84,9 @@ const InstallPluginDropdown = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</div>
|
||||||
{selectedAction === 'marketplace' && <InstallFromMarketplace onClose={() => setSelectedAction(null)} />}
|
{selectedAction === 'marketplace' && <InstallFromMarketplace onClose={() => setSelectedAction(null)} />}
|
||||||
{selectedAction === 'github' && <InstallFromGitHub onClose={() => setSelectedAction(null)}/>}
|
{selectedAction === 'github' && <InstallFromGitHub onClose={() => setSelectedAction(null)}/>}
|
||||||
{selectedAction === 'local' && selectedFile
|
{selectedAction === 'local' && selectedFile
|
||||||
@ -96,7 +95,7 @@ const InstallPluginDropdown = () => {
|
|||||||
onClose={() => setSelectedAction(null)}/>
|
onClose={() => setSelectedAction(null)}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</PortalToFollowElem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { RiDragDropLine } from '@remixicon/react'
|
|
||||||
|
|
||||||
const PluginsPanel = () => {
|
const PluginsPanel = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -14,10 +12,6 @@ const PluginsPanel = () => {
|
|||||||
<div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'>
|
<div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'>
|
||||||
{/* Plugin cards go here */}
|
{/* Plugin cards go here */}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-center py-4 gap-2 text-text-quaternary'>
|
|
||||||
<RiDragDropLine className='w-4 h-4' />
|
|
||||||
<span className='system-xs-regular'>Drop plugin package here to install</span>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
78
web/app/components/plugins/plugin-page/use-uploader.ts
Normal file
78
web/app/components/plugins/plugin-page/use-uploader.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type UploaderHookProps = {
|
||||||
|
onFileChange: (file: File | null) => void
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUploader = ({ onFileChange, containerRef }: UploaderHookProps) => {
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
const fileUploader = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.dataTransfer?.types.includes('Files'))
|
||||||
|
setDragging(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.relatedTarget === null || !containerRef.current?.contains(e.relatedTarget as Node))
|
||||||
|
setDragging(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDragging(false)
|
||||||
|
if (!e.dataTransfer)
|
||||||
|
return
|
||||||
|
const files = [...e.dataTransfer.files]
|
||||||
|
if (files.length > 0)
|
||||||
|
onFileChange(files[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0] || null
|
||||||
|
onFileChange(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = () => {
|
||||||
|
if (fileUploader.current)
|
||||||
|
fileUploader.current.value = ''
|
||||||
|
|
||||||
|
onFileChange(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = containerRef.current
|
||||||
|
if (current) {
|
||||||
|
current.addEventListener('dragenter', handleDragEnter)
|
||||||
|
current.addEventListener('dragover', handleDragOver)
|
||||||
|
current.addEventListener('dragleave', handleDragLeave)
|
||||||
|
current.addEventListener('drop', handleDrop)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (current) {
|
||||||
|
current.removeEventListener('dragenter', handleDragEnter)
|
||||||
|
current.removeEventListener('dragover', handleDragOver)
|
||||||
|
current.removeEventListener('dragleave', handleDragLeave)
|
||||||
|
current.removeEventListener('drop', handleDrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [containerRef])
|
||||||
|
|
||||||
|
return {
|
||||||
|
dragging,
|
||||||
|
fileUploader,
|
||||||
|
fileChangeHandle,
|
||||||
|
removeFile,
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user