From a9e367e6de0b329176c63aa78c9061ee530be79a Mon Sep 17 00:00:00 2001 From: Yi Date: Mon, 14 Oct 2024 18:43:08 +0800 Subject: [PATCH] feat: use-uploader hook --- .../plugins/install-plugin/uploader.tsx | 97 ------------- .../components/plugins/plugin-page/index.tsx | 42 +++++- .../plugin-page/install-plugin-dropdown.tsx | 131 +++++++++--------- .../plugins/plugin-page/plugins-panel.tsx | 6 - .../plugins/plugin-page/use-uploader.ts | 78 +++++++++++ 5 files changed, 180 insertions(+), 174 deletions(-) delete mode 100644 web/app/components/plugins/install-plugin/uploader.tsx create mode 100644 web/app/components/plugins/plugin-page/use-uploader.ts diff --git a/web/app/components/plugins/install-plugin/uploader.tsx b/web/app/components/plugins/install-plugin/uploader.tsx deleted file mode 100644 index 8e66ed21dd..0000000000 --- a/web/app/components/plugins/install-plugin/uploader.tsx +++ /dev/null @@ -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 = ({ - file, - updateFile, - className, -}) => { - const { notify } = useContext(ToastContext) - const [dragging, setDragging] = useState(false) - const dropRef = useRef(null) - const dragRef = useRef(null) - const fileUploader = useRef(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) => { - 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 ( - <> - - {dragging && ( -
-
- )} - - ) -} - -export default React.memo(Uploader) diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index f79e0af275..dd76880f45 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -1,18 +1,21 @@ 'use client' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiArrowRightUpLine, RiBugLine, RiClipboardLine, + RiDragDropLine, RiEqualizer2Line, } from '@remixicon/react' +import InstallFromLocalPackage from '../install-plugin/install-from-local-package' import { PluginPageContextProvider, usePluginPageContext, } from './context' import InstallPluginDropdown from './install-plugin-dropdown' +import { useUploader } from './use-uploader' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { useModalContext } from '@/context/modal-context' import Button from '@/app/components/base/button' @@ -31,9 +34,15 @@ const PluginPage = ({ }: PluginPageProps) => { const { t } = useTranslation() const { setShowPluginSettingModal } = useModalContext() as any + const [currentFile, setCurrentFile] = useState(null) const containerRef = usePluginPageContext(v => v.containerRef) const scrollDisabled = usePluginPageContext(v => v.scrollDisabled) + const { dragging, fileUploader, fileChangeHandle, removeFile } = useUploader({ + onFileChange: setCurrentFile, + containerRef, + }) + const options = useMemo(() => { return [ { value: 'plugins', text: t('common.menus.plugins') }, @@ -98,7 +107,7 @@ const PluginPage = ({ } popupClassName='flex flex-col items-start w-[256px] px-4 py-3.5 gap-1 border border-components-panel-border - rounded-xl bg-components-tooltip-bg shadows-shadow-lg z-50' + rounded-xl bg-components-tooltip-bg shadows-shadow-lg z-50' asChild={false} position='bottom' > @@ -117,12 +126,35 @@ const PluginPage = ({ - { - activeTab === 'plugins' && plugins - } + {activeTab === 'plugins' && ( + <> + {plugins} + {dragging && ( +
+
+ )} +
+ + Drop plugin package here to install +
+ {currentFile && ( + + )} + + )} { activeTab === 'discover' && marketplace } + ) } diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 111f977289..962349c74a 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' import Button from '@/app/components/base/button' 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 InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package' import cn from '@/utils/classnames' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' const InstallPluginDropdown = () => { const fileInputRef = useRef(null) const [isMenuOpen, setIsMenuOpen] = useState(false) const [selectedAction, setSelectedAction] = useState(null) const [selectedFile, setSelectedFile] = useState(null) - const menuRef = useRef(null) const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] @@ -27,76 +31,71 @@ 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 ( -
- - {isMenuOpen && ( -
- - Install Form - - - {[ - { icon: MagicBox, text: 'Marketplace', action: 'marketplace' }, - { icon: Github, text: 'GitHub', action: 'github' }, - { icon: FileZip, text: 'Local Package File', action: 'local' }, - ].map(({ icon: Icon, text, action }) => ( -
{ - if (action === 'local') { - fileInputRef.current?.click() - } - else { - setSelectedAction(action) - setIsMenuOpen(false) - } - }} - > - - {text} + +
+ setIsMenuOpen(v => !v)}> + + + +
+ + Install Form + + +
+ {[ + { icon: MagicBox, text: 'Marketplace', action: 'marketplace' }, + { icon: Github, text: 'GitHub', action: 'github' }, + { icon: FileZip, text: 'Local Package File', action: 'local' }, + ].map(({ icon: Icon, text, action }) => ( +
{ + if (action === 'local') { + fileInputRef.current?.click() + } + else { + setSelectedAction(action) + setIsMenuOpen(false) + } + }} + > + + {text} +
+ ))}
- ))} -
- )} +
+ +
{selectedAction === 'marketplace' && setSelectedAction(null)} />} {selectedAction === 'github' && setSelectedAction(null)}/>} {selectedAction === 'local' && selectedFile - && ( setSelectedAction(null)}/> - ) + && ( setSelectedAction(null)}/> + ) } -
+ ) } diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 227630f7d8..4d4090f973 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -1,7 +1,5 @@ 'use client' -import { RiDragDropLine } from '@remixicon/react' - const PluginsPanel = () => { return ( <> @@ -14,10 +12,6 @@ const PluginsPanel = () => {
{/* Plugin cards go here */}
-
- - Drop plugin package here to install -
) } diff --git a/web/app/components/plugins/plugin-page/use-uploader.ts b/web/app/components/plugins/plugin-page/use-uploader.ts new file mode 100644 index 0000000000..d49fe012b7 --- /dev/null +++ b/web/app/components/plugins/plugin-page/use-uploader.ts @@ -0,0 +1,78 @@ +import { useEffect, useRef, useState } from 'react' + +type UploaderHookProps = { + onFileChange: (file: File | null) => void + containerRef: React.RefObject +} + +export const useUploader = ({ onFileChange, containerRef }: UploaderHookProps) => { + const [dragging, setDragging] = useState(false) + const fileUploader = useRef(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) => { + 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, + } +}