### What problem does this PR solve? Feat: Modify the dataset list page style #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| <Form {...form}> | <Form {...form}> | ||||
| <form | <form | ||||
| onSubmit={form.handleSubmit(onSubmit)} | onSubmit={form.handleSubmit(onSubmit)} | ||||
| className="space-y-8" | |||||
| className="space-y-8 px-5 py-2.5" | |||||
| onReset={() => form.reset()} | onReset={() => form.reset()} | ||||
| > | > | ||||
| {filters.map((x) => ( | {filters.map((x) => ( | ||||
| control={form.control} | control={form.control} | ||||
| name={x.field} | name={x.field} | ||||
| render={() => ( | render={() => ( | ||||
| <FormItem> | |||||
| <div className="mb-4"> | |||||
| <FormLabel className="text-base">{x.label}</FormLabel> | |||||
| <FormItem className="space-y-4"> | |||||
| <div> | |||||
| <FormLabel className="text-base text-text-sub-title-invert"> | |||||
| {x.label} | |||||
| </FormLabel> | |||||
| </div> | </div> | ||||
| {x.list.map((item) => ( | {x.list.map((item) => ( | ||||
| <FormField | <FormField | ||||
| name={x.field} | name={x.field} | ||||
| render={({ field }) => { | render={({ field }) => { | ||||
| return ( | return ( | ||||
| <div className="flex items-center justify-between"> | |||||
| <div className="flex items-center justify-between text-text-title text-xs"> | |||||
| <FormItem | <FormItem | ||||
| key={item.id} | key={item.id} | ||||
| className="flex flex-row space-x-3 space-y-0 items-center" | |||||
| className="flex flex-row space-x-3 space-y-0 items-center " | |||||
| > | > | ||||
| <FormControl> | <FormControl> | ||||
| <Checkbox | <Checkbox | ||||
| }} | }} | ||||
| /> | /> | ||||
| </FormControl> | </FormControl> | ||||
| <FormLabel className="text-lg"> | |||||
| {item.label} | |||||
| </FormLabel> | |||||
| <FormLabel>{item.label}</FormLabel> | |||||
| </FormItem> | </FormItem> | ||||
| <span className=" text-sm">{item.count}</span> | <span className=" text-sm">{item.count}</span> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| /> | /> | ||||
| ))} | ))} | ||||
| <div className="flex justify-between"> | |||||
| <div className="flex justify-end gap-5"> | |||||
| <Button | <Button | ||||
| type="button" | type="button" | ||||
| variant={'outline'} | variant={'outline'} | ||||
| return ( | return ( | ||||
| <Popover open={open} onOpenChange={setOpen}> | <Popover open={open} onOpenChange={setOpen}> | ||||
| <PopoverTrigger asChild>{children}</PopoverTrigger> | <PopoverTrigger asChild>{children}</PopoverTrigger> | ||||
| <PopoverContent> | |||||
| <PopoverContent className="p-0"> | |||||
| <CheckboxFormMultiple | <CheckboxFormMultiple | ||||
| onChange={onChange} | onChange={onChange} | ||||
| value={value} | value={value} |
| ReactNode, | ReactNode, | ||||
| useMemo, | useMemo, | ||||
| } from 'react'; | } from 'react'; | ||||
| import { IconFont } from '../icon-font'; | |||||
| import { Button, ButtonProps } from '../ui/button'; | import { Button, ButtonProps } from '../ui/button'; | ||||
| import { SearchInput } from '../ui/input'; | import { SearchInput } from '../ui/input'; | ||||
| import { CheckboxFormMultipleProps, FilterPopover } from './filter-popover'; | import { CheckboxFormMultipleProps, FilterPopover } from './filter-popover'; | ||||
| interface IProps { | interface IProps { | ||||
| title?: string; | |||||
| title?: ReactNode; | |||||
| searchString?: string; | searchString?: string; | ||||
| onSearchChange?: ChangeEventHandler<HTMLInputElement>; | onSearchChange?: ChangeEventHandler<HTMLInputElement>; | ||||
| showFilter?: boolean; | showFilter?: boolean; | ||||
| ButtonProps & { count?: number } | ButtonProps & { count?: number } | ||||
| >(({ count = 0, ...props }, ref) => { | >(({ count = 0, ...props }, ref) => { | ||||
| return ( | return ( | ||||
| <Button variant="outline" size={'sm'} {...props} ref={ref}> | |||||
| Filter <span>{count}</span> <ChevronDown /> | |||||
| <Button variant="secondary" {...props} ref={ref}> | |||||
| <span | |||||
| className={cn({ | |||||
| 'text-text-title': count > 0, | |||||
| 'text-text-sub-title-invert': count === 0, | |||||
| })} | |||||
| > | |||||
| Filter | |||||
| </span> | |||||
| {count > 0 && ( | |||||
| <span className="rounded-full bg-text-badge px-1 text-xs "> | |||||
| {count} | |||||
| </span> | |||||
| )} | |||||
| <ChevronDown /> | |||||
| </Button> | </Button> | ||||
| ); | ); | ||||
| }); | }); | ||||
| onChange, | onChange, | ||||
| filters, | filters, | ||||
| className, | className, | ||||
| icon, | |||||
| }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>> & { | }: PropsWithChildren<IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>> & { | ||||
| className?: string; | className?: string; | ||||
| icon?: ReactNode; | |||||
| }) { | }) { | ||||
| const filterCount = useMemo(() => { | const filterCount = useMemo(() => { | ||||
| return typeof value === 'object' && value !== null | return typeof value === 'object' && value !== null | ||||
| }, [value]); | }, [value]); | ||||
| return ( | return ( | ||||
| <div className={cn('flex justify-between mb-6 items-center', className)}> | |||||
| <span className="text-3xl font-bold ">{leftPanel || title}</span> | |||||
| <div className="flex gap-4 items-center"> | |||||
| <div className={cn('flex justify-between mb-5 items-center', className)}> | |||||
| <div className="text-2xl font-semibold flex items-center gap-2.5"> | |||||
| {typeof icon === 'string' ? ( | |||||
| <IconFont name={icon} className="size-6"></IconFont> | |||||
| ) : ( | |||||
| icon | |||||
| )} | |||||
| {leftPanel || title} | |||||
| </div> | |||||
| <div className="flex gap-5 items-center"> | |||||
| {showFilter && ( | {showFilter && ( | ||||
| <FilterPopover value={value} onChange={onChange} filters={filters}> | <FilterPopover value={value} onChange={onChange} filters={filters}> | ||||
| <FilterButton count={filterCount}></FilterButton> | <FilterButton count={filterCount}></FilterButton> | ||||
| <SearchInput | <SearchInput | ||||
| value={searchString} | value={searchString} | ||||
| onChange={onSearchChange} | onChange={onSearchChange} | ||||
| className="w-32" | |||||
| ></SearchInput> | ></SearchInput> | ||||
| {children} | {children} | ||||
| </div> | </div> |
| destructive: | destructive: | ||||
| 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', | ||||
| outline: | outline: | ||||
| 'border border-colors-outline-sentiment-primary bg-background hover:bg-accent hover:text-accent-foreground', | |||||
| 'border border-text-sub-title-invert bg-transparent hover:bg-accent hover:text-accent-foreground', | |||||
| secondary: | secondary: | ||||
| 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | ||||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | ghost: 'hover:bg-accent hover:text-accent-foreground', | ||||
| icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80', | icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80', | ||||
| }, | }, | ||||
| size: { | size: { | ||||
| default: 'h-10 px-4 py-2', | |||||
| sm: 'h-9 rounded-md px-3', | |||||
| default: 'h-8 px-2.5 py-1.5 ', | |||||
| sm: 'h-6 rounded-sm px-2', | |||||
| lg: 'h-11 rounded-md px-8', | lg: 'h-11 rounded-md px-8', | ||||
| icon: 'h-10 w-10', | icon: 'h-10 w-10', | ||||
| auto: 'h-full px-1', | auto: 'h-full px-1', |
| <input | <input | ||||
| type={type} | type={type} | ||||
| className={cn( | className={cn( | ||||
| 'flex h-10 w-full rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', | |||||
| 'flex h-8 w-full rounded-md border border-input bg-colors-background-inverse-weak px-2 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', | |||||
| className, | className, | ||||
| )} | )} | ||||
| ref={ref} | ref={ref} | ||||
| suffix?: React.ReactNode; | suffix?: React.ReactNode; | ||||
| } | } | ||||
| const ExpandedInput = ({ suffix, prefix, ...props }: ExpandedInputProps) => { | |||||
| const ExpandedInput = ({ | |||||
| suffix, | |||||
| prefix, | |||||
| className, | |||||
| ...props | |||||
| }: ExpandedInputProps) => { | |||||
| return ( | return ( | ||||
| <div className="relative"> | <div className="relative"> | ||||
| <span | <span | ||||
| {prefix} | {prefix} | ||||
| </span> | </span> | ||||
| <Input | <Input | ||||
| className={cn({ 'pr-10': suffix, 'pl-10': prefix })} | |||||
| className={cn({ 'pr-8': !!suffix, 'pl-8': !!prefix }, className)} | |||||
| {...props} | {...props} | ||||
| ></Input> | ></Input> | ||||
| <span | <span | ||||
| }; | }; | ||||
| const SearchInput = (props: InputProps) => { | const SearchInput = (props: InputProps) => { | ||||
| return <ExpandedInput suffix={<Search />} {...props}></ExpandedInput>; | |||||
| return ( | |||||
| <ExpandedInput | |||||
| prefix={<Search className="size-3.5" />} | |||||
| {...props} | |||||
| ></ExpandedInput> | |||||
| ); | |||||
| }; | }; | ||||
| export { ExpandedInput, Input, SearchInput }; | export { ExpandedInput, Input, SearchInput }; |
| <a | <a | ||||
| aria-current={isActive ? 'page' : undefined} | aria-current={isActive ? 'page' : undefined} | ||||
| className={cn( | className={cn( | ||||
| 'size-8', | |||||
| buttonVariants({ | buttonVariants({ | ||||
| variant: isActive ? 'outline' : 'ghost', | variant: isActive ? 'outline' : 'ghost', | ||||
| size, | size, | ||||
| {...props} | {...props} | ||||
| > | > | ||||
| <ChevronLeft className="h-4 w-4" /> | <ChevronLeft className="h-4 w-4" /> | ||||
| <span>Previous</span> | |||||
| </PaginationLink> | </PaginationLink> | ||||
| ); | ); | ||||
| PaginationPrevious.displayName = 'PaginationPrevious'; | PaginationPrevious.displayName = 'PaginationPrevious'; | ||||
| className={cn('gap-1 pr-2.5', className)} | className={cn('gap-1 pr-2.5', className)} | ||||
| {...props} | {...props} | ||||
| > | > | ||||
| <span>Next</span> | |||||
| <ChevronRight className="h-4 w-4" /> | <ChevronRight className="h-4 w-4" /> | ||||
| </PaginationLink> | </PaginationLink> | ||||
| ); | ); |
| }, [pageSize]); | }, [pageSize]); | ||||
| return ( | return ( | ||||
| <section className="flex items-center justify-end"> | |||||
| <section className="flex items-center justify-end text-text-sub-title-invert "> | |||||
| <span className="mr-4">Total {total}</span> | <span className="mr-4">Total {total}</span> | ||||
| <Pagination className="w-auto mx-0 mr-4"> | <Pagination className="w-auto mx-0 mr-4"> | ||||
| <PaginationContent> | <PaginationContent> | ||||
| {pages.map((x) => ( | {pages.map((x) => ( | ||||
| <PaginationItem | <PaginationItem | ||||
| key={x} | key={x} | ||||
| className={cn({ ['bg-accent rounded-md']: currentPage === x })} | |||||
| className={cn({ | |||||
| ['bg-background-header-bar rounded-md text-text-title']: | |||||
| currentPage === x, | |||||
| })} | |||||
| > | > | ||||
| <PaginationLink onClick={handlePageChange(x)}>{x}</PaginationLink> | |||||
| <PaginationLink onClick={handlePageChange(x)} className="size-8"> | |||||
| {x} | |||||
| </PaginationLink> | |||||
| </PaginationItem> | </PaginationItem> | ||||
| ))} | ))} | ||||
| <PaginationItem> | <PaginationItem> | ||||
| options={sizeChangerOptions} | options={sizeChangerOptions} | ||||
| value={currentPageSize} | value={currentPageSize} | ||||
| onChange={handlePageSizeChange} | onChange={handlePageSizeChange} | ||||
| triggerClassName="bg-background-header-bar" | |||||
| ></RAGFlowSelect> | ></RAGFlowSelect> | ||||
| )} | )} | ||||
| </section> | </section> |
| <SelectPrimitive.Trigger | <SelectPrimitive.Trigger | ||||
| ref={ref} | ref={ref} | ||||
| className={cn( | className={cn( | ||||
| 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', | |||||
| 'flex h-8 w-full items-center justify-between rounded-md border border-input bg-colors-background-inverse-weak px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', | |||||
| className, | className, | ||||
| )} | )} | ||||
| {...props} | {...props} | ||||
| allowClear?: boolean; | allowClear?: boolean; | ||||
| placeholder?: React.ReactNode; | placeholder?: React.ReactNode; | ||||
| contentProps?: React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>; | contentProps?: React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>; | ||||
| triggerClassName?: string; | |||||
| } & SelectPrimitive.SelectProps; | } & SelectPrimitive.SelectProps; | ||||
| /** | /** | ||||
| placeholder, | placeholder, | ||||
| contentProps = {}, | contentProps = {}, | ||||
| defaultValue, | defaultValue, | ||||
| triggerClassName, | |||||
| }, | }, | ||||
| ref, | ref, | ||||
| ) { | ) { | ||||
| <Select onValueChange={handleChange} value={value} key={key}> | <Select onValueChange={handleChange} value={value} key={key}> | ||||
| <FormControlWidget> | <FormControlWidget> | ||||
| <SelectTrigger | <SelectTrigger | ||||
| className="bg-colors-background-inverse-weak" | |||||
| value={value} | value={value} | ||||
| onReset={handleReset} | onReset={handleReset} | ||||
| allowClear={allowClear} | allowClear={allowClear} | ||||
| ref={ref} | ref={ref} | ||||
| className={triggerClassName} | |||||
| > | > | ||||
| <SelectValue placeholder={placeholder} /> | <SelectValue placeholder={placeholder} /> | ||||
| </SelectTrigger> | </SelectTrigger> |
| }, [navigate]); | }, [navigate]); | ||||
| return ( | return ( | ||||
| <section className="py-6 px-10 flex justify-between items-center "> | |||||
| <section className="p-5 pr-14 flex justify-between items-center "> | |||||
| <div className="flex items-center gap-4"> | <div className="flex items-center gap-4"> | ||||
| <img | <img | ||||
| src={'/logo.svg'} | src={'/logo.svg'} |
| import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | ||||
| import { RenameDialog } from '@/components/rename-dialog'; | import { RenameDialog } from '@/components/rename-dialog'; | ||||
| import { TableSkeleton } from '@/components/table-skeleton'; | |||||
| import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; | ||||
| import { | import { | ||||
| Table, | Table, | ||||
| setPagination, | setPagination, | ||||
| rowSelection, | rowSelection, | ||||
| setRowSelection, | setRowSelection, | ||||
| loading, | |||||
| }: DatasetTableProps) { | }: DatasetTableProps) { | ||||
| const [sorting, setSorting] = React.useState<SortingState>([]); | const [sorting, setSorting] = React.useState<SortingState>([]); | ||||
| const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( | ||||
| ))} | ))} | ||||
| </TableHeader> | </TableHeader> | ||||
| <TableBody className="relative"> | <TableBody className="relative"> | ||||
| {loading ? ( | |||||
| <TableSkeleton columnsLength={columns.length}></TableSkeleton> | |||||
| ) : table.getRowModel().rows?.length ? ( | |||||
| {table.getRowModel().rows?.length ? ( | |||||
| table.getRowModel().rows.map((row) => ( | table.getRowModel().rows.map((row) => ( | ||||
| <TableRow | <TableRow | ||||
| key={row.id} | key={row.id} |
| > | > | ||||
| <DropdownMenu> | <DropdownMenu> | ||||
| <DropdownMenuTrigger asChild> | <DropdownMenuTrigger asChild> | ||||
| <Button variant={'tertiary'} size={'sm'}> | |||||
| <Button size={'sm'}> | |||||
| <Upload /> | <Upload /> | ||||
| {t('knowledgeDetails.addFile')} | {t('knowledgeDetails.addFile')} | ||||
| </Button> | </Button> |
| </DialogHeader> | </DialogHeader> | ||||
| <InputForm onOk={onOk}></InputForm> | <InputForm onOk={onOk}></InputForm> | ||||
| <DialogFooter> | <DialogFooter> | ||||
| <Button type="submit" variant={'tertiary'} size={'sm'} form={FormId}> | |||||
| <Button type="submit" form={FormId}> | |||||
| {t('common.save')} | {t('common.save')} | ||||
| </Button> | </Button> | ||||
| </DialogFooter> | </DialogFooter> |
| ); | ); | ||||
| return ( | return ( | ||||
| <section className="py-8 text-foreground"> | |||||
| <section className="py-4 text-foreground"> | |||||
| <ListFilterBar | <ListFilterBar | ||||
| title="Datasets" | |||||
| title={t('header.knowledgeBase')} | |||||
| searchString={searchString} | searchString={searchString} | ||||
| onSearchChange={handleInputChange} | onSearchChange={handleInputChange} | ||||
| value={filterValue} | value={filterValue} | ||||
| filters={owners} | filters={owners} | ||||
| onChange={handleFilterSubmit} | onChange={handleFilterSubmit} | ||||
| className="px-8" | className="px-8" | ||||
| icon={'data'} | |||||
| > | > | ||||
| <Button size={'sm'} onClick={showModal}> | |||||
| <Plus className="mr-2 size-2.5" /> | |||||
| <Button onClick={showModal}> | |||||
| <Plus className=" size-2.5" /> | |||||
| {t('knowledgeList.createKnowledgeBase')} | {t('knowledgeList.createKnowledgeBase')} | ||||
| </Button> | </Button> | ||||
| </ListFilterBar> | </ListFilterBar> |
| 'text-badge': 'var(--text-badge)', | 'text-badge': 'var(--text-badge)', | ||||
| 'text-title': 'var(--text-title)', | 'text-title': 'var(--text-title)', | ||||
| 'text-sub-title': 'var(--text-sub-title)', | 'text-sub-title': 'var(--text-sub-title)', | ||||
| 'text-sub-title-invert': 'var(--text-sub-title-invert)', | |||||
| 'text-title-invert': 'var(--text-title-invert)', | 'text-title-invert': 'var(--text-title-invert)', | ||||
| 'background-header-bar': 'var(--background-header-bar)', | 'background-header-bar': 'var(--background-header-bar)', | ||||
| 'background-card': 'var(--background-card)', | 'background-card': 'var(--background-card)', |
| --text-title: rgba(22, 22, 24, 1); | --text-title: rgba(22, 22, 24, 1); | ||||
| --text-sub-title: rgba(151, 154, 171, 1); | --text-sub-title: rgba(151, 154, 171, 1); | ||||
| --text-sub-title-invert: rgba(91, 93, 106, 1); | |||||
| --background-header-bar: rgba(11, 11, 12, 1); | --background-header-bar: rgba(11, 11, 12, 1); | ||||
| --text-title-invert: rgba(255, 255, 255, 1); | --text-title-invert: rgba(255, 255, 255, 1); | ||||
| --text-title: rgba(255, 255, 255, 1); | --text-title: rgba(255, 255, 255, 1); | ||||
| --text-sub-title: rgba(91, 93, 106, 1); | --text-sub-title: rgba(91, 93, 106, 1); | ||||
| --text-sub-title-invert: rgba(151, 154, 171, 1); | |||||
| --background-header-bar: rgba(11, 11, 12, 1); | --background-header-bar: rgba(11, 11, 12, 1); | ||||
| --text-title-invert: rgba(22, 22, 24, 1); | --text-title-invert: rgba(22, 22, 24, 1); |