### What problem does this PR solve? Feat: Add LinkToDatasetDialog #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.16.0
| @@ -16,7 +16,7 @@ const badgeVariants = cva( | |||
| 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', | |||
| outline: 'text-foreground', | |||
| tertiary: | |||
| 'border-transparent bg-colors-text-core-standard text-foreground hover:bg-colors-text-core-standard/80', | |||
| 'border-transparent bg-colors-background-core-strong text-colors-text-persist-light hover:bg-colors-background-core-strong/80', | |||
| }, | |||
| }, | |||
| defaultVariants: { | |||
| @@ -19,7 +19,7 @@ const buttonVariants = cva( | |||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | |||
| link: 'text-primary underline-offset-4 hover:underline', | |||
| tertiary: | |||
| 'bg-colors-text-core-standard text-foreground hover:bg-colors-text-core-standard/80', | |||
| 'bg-colors-background-sentiment-solid-primary text-colors-text-persist-light hover:bg-colors-background-sentiment-solid-primary/80', | |||
| icon: 'bg-colors-background-inverse-standard text-foreground hover:bg-colors-background-inverse-standard/80', | |||
| }, | |||
| size: { | |||
| @@ -27,6 +27,7 @@ const buttonVariants = cva( | |||
| sm: 'h-9 rounded-md px-3', | |||
| lg: 'h-11 rounded-md px-8', | |||
| icon: 'h-10 w-10', | |||
| auto: 'h-full px-1', | |||
| }, | |||
| }, | |||
| defaultVariants: { | |||
| @@ -20,6 +20,8 @@ const buttonVariants = cva( | |||
| 'bg-secondary text-secondary-foreground hover:bg-secondary/80', | |||
| ghost: 'hover:bg-accent hover:text-accent-foreground', | |||
| link: 'text-primary underline-offset-4 hover:underline', | |||
| tertiary: | |||
| 'bg-colors-background-sentiment-solid-primary text-colors-text-persist-light hover:bg-colors-background-sentiment-solid-primary/80', | |||
| }, | |||
| size: { | |||
| default: 'h-10 px-4 py-2', | |||
| @@ -72,6 +72,17 @@ export const useFetchKnowledgeList = ( | |||
| return { list: data, loading }; | |||
| }; | |||
| export const useSelectKnowledgeOptions = () => { | |||
| const { list } = useFetchKnowledgeList(); | |||
| const options = list?.map((item) => ({ | |||
| label: item.name, | |||
| value: item.id, | |||
| })); | |||
| return options; | |||
| }; | |||
| export const useInfiniteFetchKnowledgeList = () => { | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | |||
| @@ -0,0 +1,61 @@ | |||
| import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuSeparator, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { CellContext } from '@tanstack/react-table'; | |||
| import { EllipsisVertical, Link2, Trash2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { UseHandleConnectToKnowledgeReturnType } from './hooks'; | |||
| type IProps = Pick<CellContext<IFile, unknown>, 'row'> & | |||
| Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'>; | |||
| export function ActionCell({ row, showConnectToKnowledgeModal }: IProps) { | |||
| const record = row.original; | |||
| const handleShowConnectToKnowledgeModal = useCallback(() => { | |||
| showConnectToKnowledgeModal(record); | |||
| }, [record, showConnectToKnowledgeModal]); | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <Button | |||
| variant="secondary" | |||
| size={'icon'} | |||
| onClick={handleShowConnectToKnowledgeModal} | |||
| > | |||
| <Link2 /> | |||
| </Button> | |||
| <ConfirmDeleteDialog> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <Trash2 /> | |||
| </Button> | |||
| </ConfirmDeleteDialog> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <EllipsisVertical /> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuLabel>Actions</DropdownMenuLabel> | |||
| <DropdownMenuItem | |||
| onClick={() => navigator.clipboard.writeText(record.id)} | |||
| > | |||
| Copy payment ID | |||
| </DropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <DropdownMenuItem>View customer</DropdownMenuItem> | |||
| <DropdownMenuItem>View payment details</DropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -11,22 +11,14 @@ import { | |||
| getSortedRowModel, | |||
| useReactTable, | |||
| } from '@tanstack/react-table'; | |||
| import { ArrowUpDown, MoreHorizontal, Pencil } from 'lucide-react'; | |||
| import { ArrowUpDown } from 'lucide-react'; | |||
| import * as React from 'react'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { TableEmpty, TableSkeleton } from '@/components/table-skeleton'; | |||
| import { Badge } from '@/components/ui/badge'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuSeparator, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { | |||
| Table, | |||
| TableBody, | |||
| @@ -48,7 +40,9 @@ import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useNavigateToOtherFolder } from './hooks'; | |||
| import { ActionCell } from './action-cell'; | |||
| import { useHandleConnectToKnowledge, useNavigateToOtherFolder } from './hooks'; | |||
| import { LinkToDatasetDialog } from './link-to-dataset-dialog'; | |||
| export function FilesTable() { | |||
| const [sorting, setSorting] = React.useState<SortingState>([]); | |||
| @@ -62,6 +56,14 @@ export function FilesTable() { | |||
| keyPrefix: 'fileManager', | |||
| }); | |||
| const navigateToOtherFolder = useNavigateToOtherFolder(); | |||
| const { | |||
| connectToKnowledgeVisible, | |||
| hideConnectToKnowledgeModal, | |||
| showConnectToKnowledgeModal, | |||
| initialConnectedIds, | |||
| onConnectToKnowledgeOk, | |||
| connectToKnowledgeLoading, | |||
| } = useHandleConnectToKnowledge(); | |||
| const { pagination, data, loading, setPagination } = useFetchFileList(); | |||
| @@ -176,44 +178,37 @@ export function FilesTable() { | |||
| { | |||
| accessorKey: 'kbs_info', | |||
| header: t('knowledgeBase'), | |||
| cell: ({ row }) => ( | |||
| <Button variant="destructive" size={'sm'}> | |||
| {row.getValue('kbs_info')} | |||
| </Button> | |||
| ), | |||
| cell: ({ row }) => { | |||
| const value = row.getValue('kbs_info'); | |||
| return Array.isArray(value) ? ( | |||
| <section className="flex gap-2 items-center"> | |||
| {value?.slice(0, 2).map((x) => ( | |||
| <Badge key={x.kb_id} className="" variant={'tertiary'}> | |||
| {x.kb_name} | |||
| </Badge> | |||
| ))} | |||
| {value.length > 2 && ( | |||
| <Button variant={'icon'} size={'auto'}> | |||
| +{value.length - 2} | |||
| </Button> | |||
| )} | |||
| </section> | |||
| ) : ( | |||
| '' | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| id: 'actions', | |||
| header: t('action'), | |||
| enableHiding: false, | |||
| cell: ({ row }) => { | |||
| const payment = row.original; | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <Switch id="airplane-mode" /> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <Pencil /> | |||
| </Button> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant="secondary" size={'icon'}> | |||
| <MoreHorizontal /> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuLabel>Actions</DropdownMenuLabel> | |||
| <DropdownMenuItem | |||
| onClick={() => navigator.clipboard.writeText(payment.id)} | |||
| > | |||
| Copy payment ID | |||
| </DropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <DropdownMenuItem>View customer</DropdownMenuItem> | |||
| <DropdownMenuItem>View payment details</DropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| </section> | |||
| <ActionCell | |||
| row={row} | |||
| showConnectToKnowledgeModal={showConnectToKnowledgeModal} | |||
| ></ActionCell> | |||
| ); | |||
| }, | |||
| }, | |||
| @@ -338,6 +333,14 @@ export function FilesTable() { | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| {connectToKnowledgeVisible && ( | |||
| <LinkToDatasetDialog | |||
| hideModal={hideConnectToKnowledgeModal} | |||
| initialConnectedIds={initialConnectedIds} | |||
| onConnectToKnowledgeOk={onConnectToKnowledgeOk} | |||
| loading={connectToKnowledgeLoading} | |||
| ></LinkToDatasetDialog> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -224,7 +224,7 @@ export const useHandleConnectToKnowledge = () => { | |||
| ); | |||
| return { | |||
| initialValue, | |||
| initialConnectedIds: initialValue, | |||
| connectToKnowledgeLoading: loading, | |||
| onConnectToKnowledgeOk, | |||
| connectToKnowledgeVisible, | |||
| @@ -233,6 +233,10 @@ export const useHandleConnectToKnowledge = () => { | |||
| }; | |||
| }; | |||
| export type UseHandleConnectToKnowledgeReturnType = ReturnType< | |||
| typeof useHandleConnectToKnowledge | |||
| >; | |||
| export const useHandleBreadcrumbClick = () => { | |||
| const navigate = useNavigate(); | |||
| @@ -0,0 +1,130 @@ | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogFooter, | |||
| DialogHeader, | |||
| DialogTitle, | |||
| } from '@/components/ui/dialog'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { LoadingButton } from '@/components/ui/loading-button'; | |||
| import { MultiSelect } from '@/components/ui/multi-select'; | |||
| import { useSelectKnowledgeOptions } from '@/hooks/knowledge-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { Link2 } from 'lucide-react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { UseHandleConnectToKnowledgeReturnType } from './hooks'; | |||
| const FormId = 'LinkToDatasetForm'; | |||
| const FormSchema = z.object({ | |||
| knowledgeIds: z.array(z.string()).min(0, { | |||
| message: 'Username must be at least 1 characters.', | |||
| }), | |||
| }); | |||
| function LinkToDatasetForm({ | |||
| initialConnectedIds, | |||
| onConnectToKnowledgeOk, | |||
| }: Pick< | |||
| UseHandleConnectToKnowledgeReturnType, | |||
| 'initialConnectedIds' | 'onConnectToKnowledgeOk' | |||
| >) { | |||
| const { t } = useTranslation(); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: { | |||
| knowledgeIds: initialConnectedIds, | |||
| }, | |||
| }); | |||
| const options = useSelectKnowledgeOptions(); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| onConnectToKnowledgeOk(data.knowledgeIds); | |||
| } | |||
| // useEffect(() => { | |||
| // form.setValue('knowledgeIds', initialConnectedIds); // this is invalid | |||
| // }, [form, initialConnectedIds]); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| onSubmit={form.handleSubmit(onSubmit)} | |||
| className="space-y-6" | |||
| id={FormId} | |||
| > | |||
| <FormField | |||
| control={form.control} | |||
| name="knowledgeIds" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Name</FormLabel> | |||
| <FormControl> | |||
| <MultiSelect | |||
| options={options} | |||
| onValueChange={field.onChange} | |||
| defaultValue={field.value} | |||
| placeholder={t('fileManager.pleaseSelect')} | |||
| maxCount={100} | |||
| // {...field} | |||
| modalPopover | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| } | |||
| export function LinkToDatasetDialog({ | |||
| hideModal, | |||
| initialConnectedIds, | |||
| onConnectToKnowledgeOk, | |||
| loading, | |||
| }: IModalProps<any> & | |||
| Pick< | |||
| UseHandleConnectToKnowledgeReturnType, | |||
| 'initialConnectedIds' | 'onConnectToKnowledgeOk' | |||
| >) { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Dialog open onOpenChange={hideModal}> | |||
| <DialogContent className="sm:max-w-[425px]"> | |||
| <DialogHeader> | |||
| <DialogTitle>{t('fileManager.addToKnowledge')}</DialogTitle> | |||
| </DialogHeader> | |||
| <LinkToDatasetForm | |||
| initialConnectedIds={initialConnectedIds} | |||
| onConnectToKnowledgeOk={onConnectToKnowledgeOk} | |||
| ></LinkToDatasetForm> | |||
| <DialogFooter> | |||
| <LoadingButton | |||
| type="submit" | |||
| variant={'tertiary'} | |||
| form={FormId} | |||
| loading={loading} | |||
| > | |||
| <div className="flex gap-2 items-center"> | |||
| <Link2 /> Save | |||
| </div> | |||
| </LoadingButton> | |||
| </DialogFooter> | |||
| </DialogContent> | |||
| </Dialog> | |||
| ); | |||
| } | |||
| @@ -37,6 +37,7 @@ module.exports = { | |||
| 'colors-text-neutral-standard': 'var(--colors-text-neutral-standard)', | |||
| 'colors-text-functional-danger': 'var(--colors-text-functional-danger)', | |||
| 'colors-text-inverse-strong': 'var(--colors-text-inverse-strong)', | |||
| 'colors-text-persist-light': 'var(--colors-text-persist-light)', | |||
| primary: { | |||
| DEFAULT: 'hsl(var(--primary))', | |||
| @@ -156,6 +157,10 @@ module.exports = { | |||
| DEFAULT: 'var(--colors-background-neutral-weak)', | |||
| foreground: 'var(--background-inverse-standard-foreground)', | |||
| }, | |||
| 'colors-background-sentiment-solid-primary': { | |||
| DEFAULT: 'var(--colors-background-sentiment-solid-primary)', | |||
| foreground: 'var(--background-inverse-standard-foreground)', | |||
| }, | |||
| }, | |||
| borderRadius: { | |||
| lg: `var(--radius)`, | |||
| @@ -42,6 +42,8 @@ | |||
| --colors-background-inverse-weak: rgba(17, 16, 23, 0.1); | |||
| --colors-background-neutral-standard: white; | |||
| --colors-background-functional-solid-danger: rgba(222, 17, 53, 1); | |||
| --colors-background-core-strong: rgba(98, 72, 246, 1); | |||
| --colors-background-sentiment-solid-primary: rgba(127, 105, 255, 1); | |||
| --button-blue-text: rgb(22, 119, 255); | |||
| @@ -54,6 +56,7 @@ | |||
| --colors-text-neutral-standard: rgba(53, 51, 65, 1); | |||
| --colors-text-functional-danger: rgba(255, 81, 81, 1); | |||
| --colors-text-inverse-strong: rgba(255, 255, 255, 1); | |||
| --colors-text-persist-light: rgba(255, 255, 255, 1); | |||
| } | |||
| .dark { | |||
| @@ -123,6 +126,7 @@ | |||
| --colors-background-neutral-standard: rgba(11, 10, 18, 1); | |||
| --colors-background-neutral-strong: rgba(29, 26, 44, 1); | |||
| --colors-background-neutral-weak: rgba(17, 16, 23, 1); | |||
| --colors-background-sentiment-solid-primary: rgba(146, 118, 255, 1); | |||
| --colors-outline-sentiment-primary: rgba(146, 118, 255, 1); | |||
| --colors-outline-neutral-strong: rgba(255, 255, 255, 0.15); | |||
| @@ -133,6 +137,7 @@ | |||
| --colors-text-neutral-standard: rgba(230, 227, 246, 1); | |||
| --colors-text-functional-danger: rgba(255, 81, 81, 1); | |||
| --colors-text-inverse-strong: rgba(17, 16, 23, 1); | |||
| --colors-text-persist-light: rgba(255, 255, 255, 1); | |||
| } | |||
| } | |||