### What problem does this PR solve? fix: Optimized popups and the search page #3221 - Added a new PortalModal component - Refactored the Modal component, adding show and hide methods to support popups - Updated the search page, adding a new query function and optimizing the search card style - Localized, added search-related translations ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.20.1
| @@ -0,0 +1,102 @@ | |||
| import { ReactNode, useEffect, useState } from 'react'; | |||
| import { createPortal } from 'react-dom'; | |||
| import { createRoot } from 'react-dom/client'; | |||
| import { Modal, ModalProps } from './modal'; | |||
| type PortalModalProps = Omit<ModalProps, 'open' | 'onOpenChange'> & { | |||
| visible: boolean; | |||
| onVisibleChange: (visible: boolean) => void; | |||
| container?: HTMLElement; | |||
| children: ReactNode; | |||
| [key: string]: any; | |||
| }; | |||
| const PortalModal = ({ | |||
| visible, | |||
| onVisibleChange, | |||
| container, | |||
| children, | |||
| ...restProps | |||
| }: PortalModalProps) => { | |||
| const [mounted, setMounted] = useState(false); | |||
| useEffect(() => { | |||
| setMounted(true); | |||
| return () => setMounted(false); | |||
| }, []); | |||
| if (!mounted || !visible) return null; | |||
| console.log('PortalModal:', visible); | |||
| return createPortal( | |||
| <Modal open={visible} onOpenChange={onVisibleChange} {...restProps}> | |||
| {children} | |||
| </Modal>, | |||
| container || document.body, | |||
| ); | |||
| }; | |||
| export const createPortalModal = () => { | |||
| let container = document.createElement('div'); | |||
| document.body.appendChild(container); | |||
| let currentProps: any = {}; | |||
| let isVisible = false; | |||
| let root: ReturnType<typeof createRoot> | null = null; | |||
| root = createRoot(container); | |||
| const destroy = () => { | |||
| if (root && container) { | |||
| root.unmount(); | |||
| if (container.parentNode) { | |||
| container.parentNode.removeChild(container); | |||
| } | |||
| root = null; | |||
| } | |||
| isVisible = false; | |||
| currentProps = {}; | |||
| }; | |||
| const render = () => { | |||
| const { onVisibleChange, ...props } = currentProps; | |||
| const modalParam = { | |||
| visible: isVisible, | |||
| onVisibleChange: (visible: boolean) => { | |||
| isVisible = visible; | |||
| if (onVisibleChange) { | |||
| onVisibleChange(visible); | |||
| } | |||
| if (!visible) { | |||
| render(); | |||
| } | |||
| }, | |||
| ...props, | |||
| }; | |||
| root?.render(isVisible ? <PortalModal {...modalParam} /> : null); | |||
| }; | |||
| const show = (props: PortalModalProps) => { | |||
| if (!container) { | |||
| container = document.createElement('div'); | |||
| document.body.appendChild(container); | |||
| } | |||
| if (!root) { | |||
| root = createRoot(container); | |||
| } | |||
| currentProps = { ...currentProps, ...props }; | |||
| isVisible = true; | |||
| render(); | |||
| }; | |||
| const hide = () => { | |||
| isVisible = false; | |||
| render(); | |||
| }; | |||
| const update = (props = {}) => { | |||
| currentProps = { ...currentProps, ...props }; | |||
| render(); | |||
| }; | |||
| return { show, hide, update, destroy }; | |||
| }; | |||
| @@ -1,15 +1,19 @@ | |||
| // src/components/ui/modal.tsx | |||
| import { cn } from '@/lib/utils'; | |||
| import * as DialogPrimitive from '@radix-ui/react-dialog'; | |||
| import { Loader, X } from 'lucide-react'; | |||
| import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { createPortalModal } from './modal-manage'; | |||
| interface ModalProps { | |||
| export interface ModalProps { | |||
| open: boolean; | |||
| onOpenChange?: (open: boolean) => void; | |||
| title?: ReactNode; | |||
| titleClassName?: string; | |||
| children: ReactNode; | |||
| footer?: ReactNode; | |||
| footerClassName?: string; | |||
| showfooter?: boolean; | |||
| className?: string; | |||
| size?: 'small' | 'default' | 'large'; | |||
| @@ -24,13 +28,19 @@ interface ModalProps { | |||
| onOk?: () => void; | |||
| onCancel?: () => void; | |||
| } | |||
| export interface ModalType extends FC<ModalProps> { | |||
| show: typeof modalIns.show; | |||
| hide: typeof modalIns.hide; | |||
| } | |||
| export const Modal: FC<ModalProps> = ({ | |||
| const Modal: ModalType = ({ | |||
| open, | |||
| onOpenChange, | |||
| title, | |||
| titleClassName, | |||
| children, | |||
| footer, | |||
| footerClassName, | |||
| showfooter = true, | |||
| className = '', | |||
| size = 'default', | |||
| @@ -74,6 +84,7 @@ export const Modal: FC<ModalProps> = ({ | |||
| }, [onOpenChange, onOk]); | |||
| const handleChange = (open: boolean) => { | |||
| onOpenChange?.(open); | |||
| console.log('open', open, onOpenChange); | |||
| if (open) { | |||
| handleOk(); | |||
| } | |||
| @@ -113,7 +124,12 @@ export const Modal: FC<ModalProps> = ({ | |||
| ); | |||
| } | |||
| return ( | |||
| <div className="flex items-center justify-end border-t border-border px-6 py-4"> | |||
| <div | |||
| className={cn( | |||
| 'flex items-center justify-end px-6 py-4', | |||
| footerClassName, | |||
| )} | |||
| > | |||
| {footerTemp} | |||
| </div> | |||
| ); | |||
| @@ -126,6 +142,7 @@ export const Modal: FC<ModalProps> = ({ | |||
| handleCancel, | |||
| handleOk, | |||
| showfooter, | |||
| footerClassName, | |||
| ]); | |||
| return ( | |||
| <DialogPrimitive.Root open={open} onOpenChange={handleChange}> | |||
| @@ -139,11 +156,23 @@ export const Modal: FC<ModalProps> = ({ | |||
| onClick={(e) => e.stopPropagation()} | |||
| > | |||
| {/* title */} | |||
| {title && ( | |||
| <div className="flex items-center justify-between border-b border-border px-6 py-4"> | |||
| <DialogPrimitive.Title className="text-lg font-medium text-foreground"> | |||
| {title} | |||
| </DialogPrimitive.Title> | |||
| {(title || closable) && ( | |||
| <div | |||
| className={cn( | |||
| 'flex items-center px-6 py-4', | |||
| { | |||
| 'justify-end': closable && !title, | |||
| 'justify-between': closable && title, | |||
| 'justify-start': !closable, | |||
| }, | |||
| titleClassName, | |||
| )} | |||
| > | |||
| {title && ( | |||
| <DialogPrimitive.Title className="text-lg font-medium text-foreground"> | |||
| {title} | |||
| </DialogPrimitive.Title> | |||
| )} | |||
| {closable && ( | |||
| <DialogPrimitive.Close asChild> | |||
| <button | |||
| @@ -156,13 +185,9 @@ export const Modal: FC<ModalProps> = ({ | |||
| )} | |||
| </div> | |||
| )} | |||
| {/* title */} | |||
| {!title && ( | |||
| <DialogPrimitive.Title className="text-lg font-medium text-foreground"></DialogPrimitive.Title> | |||
| )} | |||
| {/* content */} | |||
| <div className="p-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none"> | |||
| <div className="py-2 px-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none"> | |||
| {destroyOnClose && !open ? null : children} | |||
| </div> | |||
| @@ -175,43 +200,13 @@ export const Modal: FC<ModalProps> = ({ | |||
| ); | |||
| }; | |||
| // example usage | |||
| /* | |||
| import { Modal } from '@/components/ui/modal'; | |||
| function Demo() { | |||
| const [open, setOpen] = useState(false); | |||
| let modalIns = createPortalModal(); | |||
| Modal.show = modalIns | |||
| ? modalIns.show | |||
| : () => { | |||
| modalIns = createPortalModal(); | |||
| return modalIns.show; | |||
| }; | |||
| Modal.hide = modalIns.hide; | |||
| return ( | |||
| <div> | |||
| <button onClick={() => setOpen(true)}>open modal</button> | |||
| <Modal | |||
| open={open} | |||
| onOpenChange={setOpen} | |||
| title="title" | |||
| footer={ | |||
| <div className="flex gap-2"> | |||
| <button onClick={() => setOpen(false)} className="px-4 py-2 border rounded-md"> | |||
| cancel | |||
| </button> | |||
| <button onClick={() => setOpen(false)} className="px-4 py-2 bg-primary text-white rounded-md"> | |||
| ok | |||
| </button> | |||
| </div> | |||
| } | |||
| > | |||
| <div className="py-4">弹窗内容区域</div> | |||
| </Modal> | |||
| <Modal | |||
| title={'modal-title'} | |||
| onOk={handleOk} | |||
| confirmLoading={loading} | |||
| destroyOnClose | |||
| > | |||
| <div className="py-4">弹窗内容区域</div> | |||
| </Modal> | |||
| </div> | |||
| ); | |||
| } | |||
| */ | |||
| export { Modal }; | |||
| @@ -1376,5 +1376,8 @@ This delimiter is used to split the input text into several text pieces echo of | |||
| addMCP: 'Add MCP', | |||
| editMCP: 'Edit MCP', | |||
| }, | |||
| search: { | |||
| createSearch: 'Create Search', | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -1310,4 +1310,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| }, | |||
| search: { | |||
| createSearch: '新建查询', | |||
| }, | |||
| }; | |||
| @@ -1,5 +1,5 @@ | |||
| import MessageItem from '@/components/next-message-item'; | |||
| import { Modal } from '@/components/ui/modal'; | |||
| import { Modal } from '@/components/ui/modal/modal'; | |||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| import { IAgentLogMessage } from '@/interfaces/database/agent'; | |||
| @@ -13,7 +13,7 @@ import { | |||
| HoverCardContent, | |||
| HoverCardTrigger, | |||
| } from '@/components/ui/hover-card'; | |||
| import { Modal } from '@/components/ui/modal'; | |||
| import { Modal } from '@/components/ui/modal/modal'; | |||
| import Space from '@/components/ui/space'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| @@ -61,7 +61,7 @@ export default ({ | |||
| }; | |||
| return ( | |||
| <div className="flex pr-[25px]"> | |||
| <div className="flex items-center gap-4 bg-background-card text-muted w-fit h-[35px] rounded-md px-4 py-2 text-base"> | |||
| <div className="flex items-center gap-4 bg-background-card text-muted-foreground w-fit h-[35px] rounded-md px-4 py-2"> | |||
| {textSelectOptions.map((option) => ( | |||
| <div | |||
| key={option.value} | |||
| @@ -98,13 +98,13 @@ export default function DatasetSettings() { | |||
| setCurrentTab(val); | |||
| }} | |||
| > | |||
| <TabsList className="grid bg-background grid-cols-2 rounded-none bg-[#161618]"> | |||
| <TabsList className="grid bg-transparent grid-cols-2 rounded-none text-foreground"> | |||
| <TabsTrigger | |||
| value="generalForm" | |||
| className="group bg-transparent p-0 !border-transparent" | |||
| > | |||
| <div className="flex w-full h-full justify-center items-center bg-[#161618]"> | |||
| <span className="h-full group-data-[state=active]:border-b-2 border-white "> | |||
| <div className="flex w-full h-full justify-center items-center"> | |||
| <span className="h-full group-data-[state=active]:border-b-2 border-foreground "> | |||
| General | |||
| </span> | |||
| </div> | |||
| @@ -113,8 +113,8 @@ export default function DatasetSettings() { | |||
| value="chunkMethodForm" | |||
| className="group bg-transparent p-0 !border-transparent" | |||
| > | |||
| <div className="flex w-full h-full justify-center items-center bg-[#161618]"> | |||
| <span className="h-full group-data-[state=active]:border-b-2 border-white "> | |||
| <div className="flex w-full h-full justify-center items-center"> | |||
| <span className="h-full group-data-[state=active]:border-b-2 border-foreground "> | |||
| Chunk Method | |||
| </span> | |||
| </div> | |||
| @@ -1,4 +1,4 @@ | |||
| import { Modal } from '@/components/ui/modal'; | |||
| import { Modal } from '@/components/ui/modal/modal'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import DebugContent from '@/pages/agent/debug-content'; | |||
| import { buildBeginInputListFromObject } from '@/pages/agent/form/begin-form/utils'; | |||
| @@ -1,23 +1,73 @@ | |||
| import ListFilterBar from '@/components/list-filter-bar'; | |||
| import { Input } from '@/components/originui/input'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Modal } from '@/components/ui/modal/modal'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFetchFlowList } from '@/hooks/flow-hooks'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { Plus, Search } from 'lucide-react'; | |||
| import { useState } from 'react'; | |||
| import { SearchCard } from './search-card'; | |||
| export default function SearchList() { | |||
| const { data } = useFetchFlowList(); | |||
| const { t } = useTranslate('search'); | |||
| const [searchName, setSearchName] = useState(''); | |||
| const handleSearchChange = (value: string) => { | |||
| console.log(value); | |||
| }; | |||
| return ( | |||
| <section> | |||
| <div className="px-8 pt-8"> | |||
| <ListFilterBar title="Search apps"> | |||
| <Button variant={'tertiary'} size={'sm'}> | |||
| <ListFilterBar | |||
| icon={ | |||
| <div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center"> | |||
| <Search size={14} className="font-bold m-auto" /> | |||
| </div> | |||
| } | |||
| title="Search apps" | |||
| showFilter={false} | |||
| onSearchChange={(e) => handleSearchChange(e.target.value)} | |||
| > | |||
| <Button | |||
| variant={'default'} | |||
| onClick={() => { | |||
| Modal.show({ | |||
| title: ( | |||
| <div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center"> | |||
| <Search size={14} className="font-bold m-auto" /> | |||
| </div> | |||
| ), | |||
| titleClassName: 'border-none', | |||
| footerClassName: 'border-none', | |||
| visible: true, | |||
| children: ( | |||
| <div> | |||
| <div>{t('createSearch')}</div> | |||
| <div>name:</div> | |||
| <Input | |||
| defaultValue={searchName} | |||
| onChange={(e) => { | |||
| console.log(e.target.value, e); | |||
| setSearchName(e.target.value); | |||
| }} | |||
| /> | |||
| </div> | |||
| ), | |||
| onOk: () => { | |||
| console.log('ok', searchName); | |||
| }, | |||
| onVisibleChange: (e) => { | |||
| Modal.hide(); | |||
| }, | |||
| }); | |||
| }} | |||
| > | |||
| <Plus className="mr-2 h-4 w-4" /> | |||
| Create app | |||
| {t('createSearch')} | |||
| </Button> | |||
| </ListFilterBar> | |||
| </div> | |||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8"> | |||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8"> | |||
| {data.map((x) => { | |||
| return <SearchCard key={x.id} data={x}></SearchCard>; | |||
| })} | |||
| @@ -1,4 +1,5 @@ | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { MoreButton } from '@/components/more-button'; | |||
| import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| @@ -15,38 +16,40 @@ export function SearchCard({ data }: IProps) { | |||
| return ( | |||
| <Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard"> | |||
| <CardContent className="p-4"> | |||
| <CardContent className="p-4 flex gap-2 items-start group"> | |||
| <div className="flex justify-between mb-4"> | |||
| {data.avatar ? ( | |||
| <div | |||
| className="w-[70px] h-[70px] rounded-xl bg-cover" | |||
| style={{ backgroundImage: `url(${data.avatar})` }} | |||
| /> | |||
| ) : ( | |||
| <Avatar className="w-[70px] h-[70px]"> | |||
| <AvatarImage src="https://github.com/shadcn.png" /> | |||
| <AvatarFallback>CN</AvatarFallback> | |||
| </Avatar> | |||
| )} | |||
| <RAGFlowAvatar | |||
| className="w-[70px] h-[70px]" | |||
| avatar={data.avatar} | |||
| name={data.title} | |||
| /> | |||
| </div> | |||
| <div className="flex flex-col gap-1"> | |||
| <section className="flex justify-between"> | |||
| <div className="text-[20px] font-bold size-7 leading-5"> | |||
| {data.title} | |||
| </div> | |||
| <MoreButton></MoreButton> | |||
| </section> | |||
| <div>An app that does things An app that does things</div> | |||
| <section className="flex justify-between"> | |||
| <div> | |||
| Search app | |||
| <p className="text-sm opacity-80"> | |||
| {formatPureDate(data.update_time)} | |||
| </p> | |||
| </div> | |||
| <div className="space-x-2 invisible group-hover:visible"> | |||
| <Button variant="icon" size="icon" onClick={navigateToSearch}> | |||
| <ChevronRight className="h-6 w-6" /> | |||
| </Button> | |||
| <Button variant="icon" size="icon"> | |||
| <Trash2 /> | |||
| </Button> | |||
| </div> | |||
| </section> | |||
| </div> | |||
| <h3 className="text-xl font-bold mb-2">{data.title}</h3> | |||
| <p>An app that does things An app that does things</p> | |||
| <section className="flex justify-between pt-3"> | |||
| <div> | |||
| Search app | |||
| <p className="text-sm opacity-80"> | |||
| {formatPureDate(data.update_time)} | |||
| </p> | |||
| </div> | |||
| <div className="space-x-2"> | |||
| <Button variant="icon" size="icon" onClick={navigateToSearch}> | |||
| <ChevronRight className="h-6 w-6" /> | |||
| </Button> | |||
| <Button variant="icon" size="icon"> | |||
| <Trash2 /> | |||
| </Button> | |||
| </div> | |||
| </section> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||