From c990bc61dbaf6ac9f7bfdf144b51089d23a3a9b3 Mon Sep 17 00:00:00 2001 From: Yi Date: Fri, 11 Oct 2024 12:39:27 +0800 Subject: [PATCH] feat: install plugins (partial) --- web/app/(commonLayout)/plugins/Container.tsx | 2 +- .../plugins/InstallPluginDropdown.tsx | 39 ++++- web/app/components/plugins/card/index.tsx | 17 +- .../install-from-github/index.tsx | 145 ++++++++++++++++++ .../install-from-local-package/index.tsx | 55 +++++++ .../install-from-marketplace/index.tsx | 66 ++++++++ .../plugins/install-plugin/uploader.tsx | 97 ++++++++++++ 7 files changed, 414 insertions(+), 7 deletions(-) create mode 100644 web/app/components/plugins/install-plugin/install-from-github/index.tsx create mode 100644 web/app/components/plugins/install-plugin/install-from-local-package/index.tsx create mode 100644 web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx create mode 100644 web/app/components/plugins/install-plugin/uploader.tsx diff --git a/web/app/(commonLayout)/plugins/Container.tsx b/web/app/(commonLayout)/plugins/Container.tsx index f6d9c9a700..3e8642c947 100644 --- a/web/app/(commonLayout)/plugins/Container.tsx +++ b/web/app/(commonLayout)/plugins/Container.tsx @@ -26,7 +26,7 @@ const Container = () => { const options = useMemo(() => { return [ { value: 'plugins', text: t('common.menus.plugins') }, - { value: 'discover', text: 'Discover in Marketplace' }, + { value: 'discover', text: 'Explore Marketplace' }, ] }, [t]) diff --git a/web/app/(commonLayout)/plugins/InstallPluginDropdown.tsx b/web/app/(commonLayout)/plugins/InstallPluginDropdown.tsx index c94ddfe75b..111f977289 100644 --- a/web/app/(commonLayout)/plugins/InstallPluginDropdown.tsx +++ b/web/app/(commonLayout)/plugins/InstallPluginDropdown.tsx @@ -6,12 +6,27 @@ import Button from '@/app/components/base/button' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' import { Github } from '@/app/components/base/icons/src/vender/solid/general' +import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' +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' 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] + if (file) { + setSelectedFile(file) + setSelectedAction('local') + setIsMenuOpen(false) + } + } + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) @@ -42,6 +57,13 @@ const InstallPluginDropdown = () => { system-xs-medium-uppercase'> Install Form + {[ { icon: MagicBox, text: 'Marketplace', action: 'marketplace' }, { icon: Github, text: 'GitHub', action: 'github' }, @@ -51,8 +73,13 @@ const InstallPluginDropdown = () => { key={action} className='flex items-center w-full px-2 py-1.5 gap-1 rounded-lg hover:bg-state-base-hover cursor-pointer' onClick={() => { - console.log(action) - setIsMenuOpen(false) + if (action === 'local') { + fileInputRef.current?.click() + } + else { + setSelectedAction(action) + setIsMenuOpen(false) + } }} > @@ -61,6 +88,14 @@ const InstallPluginDropdown = () => { ))} )} + {selectedAction === 'marketplace' && setSelectedAction(null)} />} + {selectedAction === 'github' && setSelectedAction(null)}/>} + {selectedAction === 'local' && selectedFile + && ( setSelectedAction(null)}/> + ) + } ) } diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 64fe273ef3..f0a6666450 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -3,12 +3,12 @@ import { RiVerifiedBadgeLine } from '@remixicon/react' import type { Plugin } from '../types' import Badge from '../../base/badge' import CornerMark from './base/corner-mark' -import Icon from './base/icon' import Title from './base/title' import OrgInfo from './base/org-info' import Description from './base/description' import cn from '@/utils/classnames' -import { getLocaleOnServer } from '@/i18n/server' +// import { getLocaleOnServer } from '@/i18n/server' +import type { Locale } from '@/i18n' type Props = { className?: string @@ -17,6 +17,8 @@ type Props = { installed?: boolean descriptionLineRows?: number footer?: React.ReactNode + clientLocale?: Locale + serverLocale?: Locale } const Card = ({ @@ -26,8 +28,10 @@ const Card = ({ installed, descriptionLineRows = 2, footer, + clientLocale, + serverLocale, }: Props) => { - const locale = getLocaleOnServer() + const locale = clientLocale || serverLocale || 'en' const { type, name, org, label } = payload return ( @@ -35,7 +39,7 @@ const Card = ({ {/* Header */}
- + {/* */}
@@ -62,3 +66,8 @@ const Card = ({ } export default Card + +// export function ServerCard(props: Omit<Props, 'serverLocale'>) { +// const serverLocale = getLocaleOnServer() +// return <Card {...props} serverLocale={serverLocale} /> +// } diff --git a/web/app/components/plugins/install-plugin/install-from-github/index.tsx b/web/app/components/plugins/install-plugin/install-from-github/index.tsx new file mode 100644 index 0000000000..8f7aebebcc --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-github/index.tsx @@ -0,0 +1,145 @@ +'use client' + +import React, { useState } from 'react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import type { Item } from '@/app/components/base/select' +import { PortalSelect } from '@/app/components/base/select' + +type InstallFromGitHubProps = { + onClose: () => void +} + +type InstallStep = 'url' | 'version' | 'package' + +const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => { + const [step, setStep] = useState<InstallStep>('url') + const [repoUrl, setRepoUrl] = useState('') + const [selectedVersion, setSelectedVersion] = useState('') + const [selectedPackage, setSelectedPackage] = useState('') + + // Mock data - replace with actual data fetched from the backend + const versions: Item[] = [ + { value: '1.0.1', name: '1.0.1' }, + { value: '1.2.0', name: '1.2.0' }, + { value: '1.2.1', name: '1.2.1' }, + { value: '1.3.2', name: '1.3.2' }, + ] + const packages: Item[] = [ + { value: 'package1', name: 'Package 1' }, + { value: 'package2', name: 'Package 2' }, + { value: 'package3', name: 'Package 3' }, + ] + + const handleNext = () => { + switch (step) { + case 'url': + // TODO: Validate URL and fetch versions + setStep('version') + break + case 'version': + // TODO: Validate version and fetch packages + setStep('package') + break + case 'package': + // TODO: Handle final submission + break + } + } + + return ( + <Modal + isShow={true} + onClose={onClose} + className='flex min-w-[480px] p-0 flex-col items-start rounded-2xl border-[0.5px] + border-components-panel-border bg-components-panel-bg shadows-shadow-xl' + closable + > + <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> + <div className='flex flex-col items-start gap-1 flex-grow'> + <div className='self-stretch text-text-primary title-2xl-semi-bold'> + Install plugin from GitHub + </div> + <div className='self-stretch text-text-tertiary system-xs-regular'> + Please make sure that you only install plugins from a trusted source. + </div> + </div> + </div> + <div className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'> + {step === 'url' && ( + <> + <label + htmlFor='repoUrl' + className='flex flex-col justify-center items-start self-stretch text-text-secondary' + > + <span className='system-sm-semibold'>GitHub repository</span> + </label> + <input + type='url' + id='repoUrl' + name='repoUrl' + value={repoUrl} + onChange={e => setRepoUrl(e.target.value)} // TODO: needs to verify the url + className='flex items-center self-stretch rounded-lg border border-components-input-border-active + bg-components-input-bg-active shadows-shadow-xs p-2 gap-[2px] flex-grow overflow-hidden + text-components-input-text-filled text-ellipsis system-sm-regular' + placeholder='Please enter GitHub repo URL' + /> + </> + )} + {step === 'version' && ( + <> + <label + htmlFor='version' + className='flex flex-col justify-center items-start self-stretch text-text-secondary' + > + <span className='system-sm-semibold'>Select version</span> + </label> + <PortalSelect + value={selectedVersion} + onSelect={item => setSelectedVersion(item.value as string)} + items={versions} + placeholder="Please select a version" + popupClassName='w-[432px] z-[1001]' + /> + </> + )} + {step === 'package' && ( + <> + <label + htmlFor='package' + className='flex flex-col justify-center items-start self-stretch text-text-secondary' + > + <span className='system-sm-semibold'>Select package</span> + </label> + <PortalSelect + value={selectedPackage} + onSelect={item => setSelectedPackage(item.value as string)} + items={packages} + placeholder="Please select a package" + popupClassName='w-[432px] z-[1001]' + /> + </> + )} + </div> + <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> + <Button + variant='secondary' + className='min-w-[72px]' + onClick={onClose} + > + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + onClick={handleNext} + > + {step === 'package' ? 'Install' : 'Next'} + </Button> + </div> + </Modal> + ) +} + +export default InstallFromGitHub diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx new file mode 100644 index 0000000000..c8863faf79 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -0,0 +1,55 @@ +'use client' + +import React from 'react' +import { RiLoader2Line } from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' + +type InstallFromLocalPackageProps = { + file: File + onClose: () => void +} + +const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ onClose }) => { + return ( + <Modal + isShow={true} + onClose={onClose} + className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] + border-components-panel-border bg-components-panel-bg shadows-shadow-xl' + closable + > + <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> + <div className='self-stretch text-text-primary title-2xl-semi-bold'> + Install plugin + </div> + </div> + <div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'> + <div className='flex items-center gap-1 self-stretch'> + <RiLoader2Line className='text-text-accent w-4 h-4' /> + <div className='text-text-secondary system-md-regular'> + Uploading notion-sync.difypkg ... + </div> + </div> + </div> + <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> + <Button + variant='secondary' + className='min-w-[72px]' + onClick={onClose} + > + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + disabled + > + Install + </Button> + </div> + </Modal> + ) +} + +export default InstallFromLocalPackage diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx new file mode 100644 index 0000000000..dd80fcce55 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -0,0 +1,66 @@ +'use client' + +import React from 'react' +import { useContext } from 'use-context-selector' +import Card from '../../card' +import { extensionDallE, modelGPT4, toolNotion } from '../../card/card-mock' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import I18n from '@/context/i18n' + +type InstallFromMarketplaceProps = { + onClose: () => void +} + +const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({ onClose }) => { + const { locale } = useContext(I18n) + + // Mock a plugin list + const plugins = [toolNotion, extensionDallE, modelGPT4] + + return ( + <Modal + isShow={true} + onClose={onClose} + className='flex min-w-[560px] flex-col items-start p-0 rounded-2xl border-[0.5px] + border-components-panel-border bg-components-panel-bg shadows-shadow-xl' + closable + > + <div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'> + <div className='self-stretch text-text-primary title-2xl-semi-bold'>Install plugin</div> + </div> + <div className='flex px-6 py-3 flex-col justify-center items-start gap-4 self-stretch'> + <div className='flex flex-col items-start gap-2 self-stretch'> + <div className='text-text-secondary system-md-regular'>About to install the following plugin</div> + </div> + <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap + rounded-2xl bg-background-section-burn'> + {plugins.map((plugin, index) => ( + <Card + key={index} + payload={plugin as any} + clientLocale={locale} + /> + ))} + </div> + </div> + <div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'> + <Button + variant='secondary' + className='min-w-[72px]' + onClick={onClose} + > + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + > + Install + </Button> + </div> + </Modal> + ) +} + +export default InstallFromMarketplace diff --git a/web/app/components/plugins/install-plugin/uploader.tsx b/web/app/components/plugins/install-plugin/uploader.tsx new file mode 100644 index 0000000000..8e66ed21dd --- /dev/null +++ b/web/app/components/plugins/install-plugin/uploader.tsx @@ -0,0 +1,97 @@ +'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)