From c9ee1e9ff2220a59a08e7e01a63b40a8c6bd1fa2 Mon Sep 17 00:00:00 2001 From: Yi Date: Tue, 15 Oct 2024 14:56:59 +0800 Subject: [PATCH] feat: install difypkg ui --- .../plugins/card/base/card-icon.tsx | 6 +- .../components/plugins/card/base/org-info.tsx | 33 +++- web/app/components/plugins/card/index.tsx | 43 +++-- .../install-from-github/index.tsx | 92 ++++++++-- .../install-from-local-package/index.tsx | 94 +++++++--- .../install-from-marketplace/index.tsx | 161 +++++++++++------- 6 files changed, 305 insertions(+), 124 deletions(-) diff --git a/web/app/components/plugins/card/base/card-icon.tsx b/web/app/components/plugins/card/base/card-icon.tsx index 60be58007f..0fcc28d997 100644 --- a/web/app/components/plugins/card/base/card-icon.tsx +++ b/web/app/components/plugins/card/base/card-icon.tsx @@ -18,10 +18,8 @@ const Icon = ({ }} > {installed - &&
-
- -
+ &&
+
}
diff --git a/web/app/components/plugins/card/base/org-info.tsx b/web/app/components/plugins/card/base/org-info.tsx index 65cfb1cb68..ed184b8bd8 100644 --- a/web/app/components/plugins/card/base/org-info.tsx +++ b/web/app/components/plugins/card/base/org-info.tsx @@ -4,6 +4,7 @@ type Props = { orgName: string packageName: string packageNameClassName?: string + isLoading?: boolean } const OrgInfo = ({ @@ -11,12 +12,34 @@ const OrgInfo = ({ orgName, packageName, packageNameClassName, + isLoading = false, }: Props) => { - return
- {orgName} - / - {packageName} -
+ const LoadingPlaceholder = ({ width }: { width: string }) => ( +
+ ) + return ( +
+ {isLoading + ? ( + + ) + : ( + {orgName} + )} + + {isLoading ? 'ยท' : '/'} + + {isLoading + ? ( + + ) + : ( + + {packageName} + + )} +
+ ) } export default OrgInfo diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 9b00284fd4..823d9dd025 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import { RiVerifiedBadgeLine } from '@remixicon/react' import type { Plugin } from '../types' import Icon from '../card/base/card-icon' +import { Group } from '../../base/icons/src/vender/other' import CornerMark from './base/corner-mark' import Title from './base/title' import OrgInfo from './base/org-info' @@ -18,6 +19,8 @@ type Props = { descriptionLineRows?: number footer?: React.ReactNode serverLocale?: Locale + isLoading?: boolean + loadingFileName?: string } const Card = ({ @@ -28,33 +31,53 @@ const Card = ({ descriptionLineRows = 2, footer, locale, + isLoading = false, + loadingFileName, }: Props) => { - const { type, name, org, label } = payload + const { type, name, org, label, brief, icon } = payload + + const getLocalizedText = (obj: Record | undefined) => + obj?.[locale] || obj?.['en-US'] || '' + + const LoadingPlaceholder = ({ className }: { className?: string }) => ( +
+ ) return (
- + {!isLoading && } {/* Header */}
- + {isLoading + ? (
+
+ +
+
) + : }
- - <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> + <Title title={loadingFileName || getLocalizedText(label)} /> + {!isLoading && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} {titleLeft} {/* This can be version badge */} </div> <OrgInfo className="mt-0.5" orgName={org} packageName={name} + isLoading={isLoading} /> </div> </div> - <Description - className="mt-3" - text={payload.brief[locale]} - descriptionLineRows={descriptionLineRows} - /> + {isLoading + ? <LoadingPlaceholder className="mt-3 w-[420px]" /> + : <Description + className="mt-3" + text={getLocalizedText(brief)} + descriptionLineRows={descriptionLineRows} + />} {footer && <div>{footer}</div>} </div> ) 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 index 8f7aebebcc..d882bff796 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/index.tsx @@ -10,7 +10,7 @@ type InstallFromGitHubProps = { onClose: () => void } -type InstallStep = 'url' | 'version' | 'package' +type InstallStep = 'url' | 'version' | 'package' | 'installed' const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => { const [step, setStep] = useState<InstallStep>('url') @@ -42,11 +42,38 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => { setStep('package') break case 'package': - // TODO: Handle final submission + // TODO: Handle installation + setStep('installed') break } } + const isInputValid = () => { + switch (step) { + case 'url': + return !!repoUrl.trim() + case 'version': + return !!selectedVersion + case 'package': + return !!selectedPackage + default: + return true + } + } + + const InfoRow = ({ label, value }: { label: string; value: string }) => ( + <div className='flex items-center gap-3'> + <div className='flex w-[72px] items-center gap-2'> + <div className='text-text-tertiary system-sm-medium'> + {label} + </div> + </div> + <div className='flex-grow overflow-hidden text-text-secondary text-ellipsis system-sm-medium'> + {value} + </div> + </div> + ) + return ( <Modal isShow={true} @@ -61,11 +88,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => { 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. + {step !== 'installed' && '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'> + <div className={`flex px-6 py-3 flex-col justify-center items-start self-stretch ${step === 'installed' ? 'gap-2' : 'gap-4'}`}> {step === 'url' && ( <> <label @@ -121,22 +148,51 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => { /> </> )} + {step === 'installed' && ( + <> + <div className='text-text-secondary system-md-regular'>The plugin has been installed successfully.</div> + <div className='flex w-full p-4 flex-col justify-center items-start gap-2 rounded-2xl bg-background-section-burn'> + {[ + { label: 'Repository', value: repoUrl }, + { label: 'Version', value: selectedVersion }, + { label: 'Package', value: selectedPackage }, + ].map(({ label, value }) => ( + <InfoRow key={label} label={label} value={value} /> + ))} + </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]' - onClick={handleNext} - > - {step === 'package' ? 'Install' : 'Next'} - </Button> + {step === 'installed' + ? ( + <Button + variant='primary' + className='min-w-[72px]' + onClick={onClose} + > + Close + </Button> + ) + : ( + <> + <Button + variant='secondary' + className='min-w-[72px]' + onClick={onClose} + > + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + onClick={handleNext} + disabled={!isInputValid()} + > + {step === 'package' ? 'Install' : 'Next'} + </Button> + </> + )} </div> </Modal> ) 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 index c8863faf79..87e470584e 100644 --- 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 @@ -1,9 +1,13 @@ 'use client' -import React from 'react' +import React, { useCallback, useEffect, useState } from 'react' +import { useContext } from 'use-context-selector' import { RiLoader2Line } from '@remixicon/react' +import Card from '../../card' +import { 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 InstallFromLocalPackageProps = { file: File @@ -11,12 +15,48 @@ type InstallFromLocalPackageProps = { } const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ onClose }) => { + const [status, setStatus] = useState<'uploading' | 'ready' | 'installing' | 'installed'>('uploading') + const { locale } = useContext(I18n) + + useEffect(() => { + const timer = setTimeout(() => setStatus('ready'), 1500) + return () => clearTimeout(timer) + }, []) + + const handleInstall = useCallback(async () => { + setStatus('installing') + await new Promise(resolve => setTimeout(resolve, 1000)) + setStatus('installed') + }, []) + + const renderStatusMessage = () => { + switch (status) { + case 'uploading': + return ( + <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> + ) + case 'installed': + return <p className='text-text-secondary system-md-regular'>The plugin has been installed successfully.</p> + default: + return ( + <div className='text-text-secondary system-md-regular'> + <p>About to install the following plugin.</p> + <p>Please make sure that you only install plugins from a <span className='system-md-semibold'>trusted source</span>.</p> + </div> + ) + } + } + 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' + 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'> @@ -25,28 +65,38 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ onClo </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> + {renderStatusMessage()} + <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'> + <Card + className='w-full' + locale={locale} + payload={status === 'uploading' ? { name: 'notion-sync' } as any : toolNotion as any} + isLoading={status === 'uploading'} + loadingFileName='notion-sync.difypkg' + installed={status === 'installed'} + /> </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> + {status === 'installed' + ? ( + <Button variant='primary' onClick={onClose}>Close</Button> + ) + : ( + <> + <Button variant='secondary' className='min-w-[72px]' onClick={onClose}> + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + disabled={status !== 'ready'} + onClick={handleInstall} + > + {status === 'installing' ? 'Installing...' : 'Install'} + </Button> + </> + )} </div> </Modal> ) 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 index 524588132f..96deec4da9 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { useContext } from 'use-context-selector' import { RiInformation2Line } from '@remixicon/react' import Card from '../../card' @@ -17,88 +17,119 @@ type InstallFromMarketplaceProps = { const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({ onClose }) => { const { locale } = useContext(I18n) - - // Mock a plugin list - const plugins = [toolNotion, extensionDallE, modelGPT4] + const plugins = useMemo(() => [toolNotion, extensionDallE, modelGPT4], []) const [selectedPlugins, setSelectedPlugins] = useState<Set<number>>(new Set()) + const [isInstalling, setIsInstalling] = useState(false) + const [nextStep, setNextStep] = useState(false) + + const mockInstall = async () => { + setIsInstalling(true) + await new Promise(resolve => setTimeout(resolve, 1500)) + setIsInstalling(false) + } + + const pluginsToShow = useMemo(() => { + if (plugins.length === 1 || (nextStep && selectedPlugins.size === 1)) + return plugins.length === 1 ? plugins : plugins.filter((_, index) => selectedPlugins.has(index)) + + return nextStep ? plugins.filter((_, index) => selectedPlugins.has(index)) : plugins + }, [plugins, nextStep, selectedPlugins]) + + const renderPluginCard = (plugin: any, index: number) => ( + <Card + key={index} + installed={nextStep && !isInstalling} + payload={plugin} + locale={locale} + className='w-full' + titleLeft={ + plugin.version === plugin.latest_version + ? ( + <Badge className='mx-1' size="s" state={BadgeState.Default}>{plugin.version}</Badge> + ) + : ( + <> + <Badge className='mx-1' size="s" state={BadgeState.Warning}> + {`${plugin.version} -> ${plugin.latest_version}`} + </Badge> + <div className='flex px-0.5 justify-center items-center gap-0.5'> + <div className='text-text-warning system-xs-medium'>Used in 3 apps</div> + <RiInformation2Line className='w-4 h-4 text-text-tertiary' /> + </div> + </> + ) + } + /> + ) 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' + 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 className='self-stretch text-text-primary title-2xl-semi-bold'> + {nextStep ? (isInstalling ? 'Install plugin' : 'Installation successful') : '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 className='text-text-secondary system-md-regular'> + {(nextStep && !isInstalling) + ? `The following ${pluginsToShow.length === 1 ? 'plugin has' : `${pluginsToShow.length} plugins have`} been installed successfully` + : `About to install the following ${pluginsToShow.length === 1 ? 'plugin' : `${pluginsToShow.length} plugins`}`} + </div> </div> - <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap - rounded-2xl bg-background-section-burn'> - {plugins.length === 1 - && <Card - payload={plugins[0] as any} - locale={locale} - className='w-full' - > - </Card> - } - {plugins.length > 1 && plugins.map((plugin, index) => ( - <div className='flex pl-1 items-center gap-2 flex-grow' key={index}> - <Checkbox - checked={selectedPlugins.has(index)} - onCheck={() => { - const newSelectedPlugins = new Set(selectedPlugins) - if (newSelectedPlugins.has(index)) - newSelectedPlugins.delete(index) - else - newSelectedPlugins.add(index) - - setSelectedPlugins(newSelectedPlugins) - }} - /> - <Card - key={index} - payload={plugin as any} - locale={locale} - className='w-full' - titleLeft={plugin.version === plugin.latest_version - ? <Badge className='mx-1' size="s" state={BadgeState.Default}>{plugin.version}</Badge> - : <> - <Badge - className='mx-1' - size="s" - state={BadgeState.Warning}>{`${plugin.version} -> ${plugin.latest_version}`} - </Badge> - <div className='flex px-0.5 justify-center items-center gap-0.5'> - <div className='text-text-warning system-xs-medium'>Used in 3 apps</div> - <RiInformation2Line className='w-4 h-4 text-text-tertiary' /> - </div> - </> - } - /> + <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'> + {pluginsToShow.map((plugin, index) => ( + <div key={index} className={`flex ${(plugins.length > 1 && !nextStep) ? 'pl-1 items-center gap-2' : ''} flex-grow`}> + {(plugins.length > 1 && !nextStep) && ( + <Checkbox + checked={selectedPlugins.has(index)} + onCheck={() => { + const newSelectedPlugins = new Set(selectedPlugins) + newSelectedPlugins.has(index) ? newSelectedPlugins.delete(index) : newSelectedPlugins.add(index) + setSelectedPlugins(newSelectedPlugins) + }} + /> + )} + {renderPluginCard(plugin, index)} </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]' - > - Install - </Button> + {nextStep + ? ( + <Button + variant='primary' + disabled={isInstalling} + loading={isInstalling} + onClick={onClose} + > + {isInstalling ? 'Installing...' : 'Close'} + </Button> + ) + : ( + <> + <Button variant='secondary' className='min-w-[72px]' onClick={onClose}> + Cancel + </Button> + <Button + variant='primary' + className='min-w-[72px]' + disabled={plugins.length > 1 && selectedPlugins.size < 1} + onClick={() => { + setNextStep(true) + mockInstall() + }} + > + Install + </Button> + </> + )} </div> </Modal> )