mirror of
https://github.com/langgenius/dify.git
synced 2026-08-02 10:46:38 +08:00
338 lines
13 KiB
TypeScript
338 lines
13 KiB
TypeScript
'use client'
|
|
import type { FC } from 'react'
|
|
import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogActions,
|
|
AlertDialogCancelButton,
|
|
AlertDialogConfirmButton,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogTitle,
|
|
} from '@langgenius/dify-ui/alert-dialog'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
|
import {
|
|
Drawer,
|
|
DrawerBackdrop,
|
|
DrawerCloseButton,
|
|
DrawerContent,
|
|
DrawerPopup,
|
|
DrawerPortal,
|
|
DrawerTitle,
|
|
DrawerViewport,
|
|
} from '@langgenius/dify-ui/drawer'
|
|
import { Switch } from '@langgenius/dify-ui/switch'
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
|
import { useBoolean, useHover } from 'ahooks'
|
|
import * as React from 'react'
|
|
import { useCallback, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Infotip } from '@/app/components/base/infotip'
|
|
import Input from '@/app/components/base/input'
|
|
import { CreateMetadataModal } from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
|
|
import { getIconClassName } from '../utils/get-icon'
|
|
import Field from './field'
|
|
|
|
const i18nPrefix = 'metadata.datasetMetadata'
|
|
|
|
type Props = Readonly<{
|
|
userMetadata: MetadataItemWithValueLength[]
|
|
builtInMetadata: BuiltInMetadataItem[]
|
|
isBuiltInEnabled: boolean
|
|
onIsBuiltInEnabledChange: (value: boolean) => void
|
|
onClose: () => void
|
|
onAdd: (payload: BuiltInMetadataItem) => void
|
|
onRename: (payload: MetadataItemWithValueLength) => void
|
|
onRemove: (metaDataId: string) => void
|
|
}>
|
|
|
|
type ItemProps = {
|
|
readonly?: boolean
|
|
disabled?: boolean
|
|
payload: MetadataItemWithValueLength
|
|
onRename?: () => void
|
|
onDelete?: () => void
|
|
}
|
|
const Item: FC<ItemProps> = ({ readonly, disabled, payload, onRename, onDelete }) => {
|
|
const { t } = useTranslation()
|
|
const iconClassName = getIconClassName(payload.type)
|
|
|
|
const handleRename = useCallback(() => {
|
|
onRename?.()
|
|
}, [onRename])
|
|
|
|
const deleteBtnRef = useRef<HTMLButtonElement>(null)
|
|
const isDeleteHovering = useHover(deleteBtnRef)
|
|
const [isShowDeleteConfirm, { setTrue: showDeleteConfirm, setFalse: hideDeleteConfirm }] =
|
|
useBoolean(false)
|
|
const handleDelete = useCallback(() => {
|
|
hideDeleteConfirm()
|
|
onDelete?.()
|
|
}, [hideDeleteConfirm, onDelete])
|
|
|
|
return (
|
|
<div
|
|
key={payload.name}
|
|
className={cn(
|
|
!readonly && !disabled && 'group/item cursor-pointer hover:shadow-xs',
|
|
'rounded-md border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg',
|
|
isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'flex h-8 items-center justify-between px-2',
|
|
disabled && 'opacity-30', // not include border and bg
|
|
)}
|
|
>
|
|
<div className="flex h-full items-center space-x-1 text-text-tertiary">
|
|
<span className={cn(iconClassName, 'size-4 shrink-0')} aria-hidden="true" />
|
|
<div className="max-w-[250px] truncate system-sm-medium text-text-primary">
|
|
{payload.name}
|
|
</div>
|
|
<div className="shrink-0 system-xs-regular">{payload.type}</div>
|
|
</div>
|
|
{(!readonly || disabled) && (
|
|
<div className="ml-2 shrink-0 system-xs-regular text-text-tertiary group-hover/item:hidden">
|
|
{disabled
|
|
? t(($) => $[`${i18nPrefix}.disabled`], { ns: 'dataset' })
|
|
: t(($) => $[`${i18nPrefix}.values`], { ns: 'dataset', num: payload.count || 0 })}
|
|
</div>
|
|
)}
|
|
<div className="ml-2 hidden items-center space-x-1 text-text-tertiary group-hover/item:flex">
|
|
<button
|
|
type="button"
|
|
aria-label={t(($) => $['operation.edit'], { ns: 'common' })}
|
|
className="cursor-pointer rounded-md border-none bg-transparent p-0.5 hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
|
onClick={handleRename}
|
|
>
|
|
<RiEditLine className="size-4" aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
ref={deleteBtnRef}
|
|
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
|
|
className="cursor-pointer rounded-md border-none bg-transparent p-0.5 hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:ring-1 focus-visible:ring-state-destructive-border focus-visible:outline-hidden"
|
|
onClick={showDeleteConfirm}
|
|
>
|
|
<RiDeleteBinLine className="size-4" aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
<AlertDialog
|
|
open={isShowDeleteConfirm}
|
|
onOpenChange={(open) => !open && hideDeleteConfirm()}
|
|
>
|
|
<AlertDialogContent>
|
|
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
|
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
|
|
{t(($) => $['metadata.datasetMetadata.deleteTitle'], { ns: 'dataset' })}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
|
{t(($) => $['metadata.datasetMetadata.deleteContent'], {
|
|
ns: 'dataset',
|
|
name: payload.name,
|
|
})}
|
|
</AlertDialogDescription>
|
|
</div>
|
|
<AlertDialogActions>
|
|
<AlertDialogCancelButton>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</AlertDialogCancelButton>
|
|
<AlertDialogConfirmButton onClick={handleDelete}>
|
|
{t(($) => $['operation.confirm'], { ns: 'common' })}
|
|
</AlertDialogConfirmButton>
|
|
</AlertDialogActions>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const DatasetMetadataDrawer: FC<Props> = ({
|
|
userMetadata,
|
|
builtInMetadata,
|
|
isBuiltInEnabled,
|
|
onIsBuiltInEnabledChange,
|
|
onClose,
|
|
onAdd,
|
|
onRename,
|
|
onRemove,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const [isShowRenameModal, setIsShowRenameModal] = useState(false)
|
|
const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
|
|
const [templeName, setTempleName] = useState('')
|
|
const handleRename = useCallback(
|
|
(payload: MetadataItemWithValueLength) => {
|
|
return () => {
|
|
setCurrPayload(payload)
|
|
setTempleName(payload.name)
|
|
setIsShowRenameModal(true)
|
|
}
|
|
},
|
|
[setCurrPayload, setIsShowRenameModal],
|
|
)
|
|
|
|
const [open, setOpen] = useState(false)
|
|
const handleAdd = useCallback(
|
|
async (data: BuiltInMetadataItem) => {
|
|
await onAdd(data)
|
|
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
|
|
setOpen(false)
|
|
},
|
|
[onAdd, t],
|
|
)
|
|
|
|
const handleRenamed = useCallback(async () => {
|
|
const item = userMetadata.find((p) => p.id === currPayload?.id)
|
|
if (item) {
|
|
await onRename({
|
|
...item,
|
|
name: templeName,
|
|
})
|
|
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
|
|
}
|
|
setIsShowRenameModal(false)
|
|
}, [userMetadata, currPayload?.id, onRename, templeName, t])
|
|
|
|
const handleDelete = useCallback(
|
|
(payload: MetadataItemWithValueLength) => {
|
|
return async () => {
|
|
await onRemove(payload.id)
|
|
toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' }))
|
|
}
|
|
},
|
|
[onRemove, t],
|
|
)
|
|
|
|
return (
|
|
<Drawer
|
|
open
|
|
modal
|
|
swipeDirection="right"
|
|
onOpenChange={(open) => {
|
|
if (!open) onClose()
|
|
}}
|
|
>
|
|
<DrawerPortal>
|
|
<DrawerBackdrop />
|
|
<DrawerViewport>
|
|
<DrawerPopup className="data-[swipe-direction=right]:top-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-[calc(100dvh-16px)] data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-[420px]">
|
|
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
|
<div className="flex shrink-0 justify-between px-4 pt-6 pb-4">
|
|
<DrawerTitle className="text-lg/6 font-medium text-text-primary">
|
|
{t(($) => $['metadata.metadata'], { ns: 'dataset' })}
|
|
</DrawerTitle>
|
|
<DrawerCloseButton
|
|
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
|
className="size-6 rounded-md"
|
|
/>
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-6">
|
|
<div className="system-sm-regular text-text-tertiary">
|
|
{t(($) => $[`${i18nPrefix}.description`], { ns: 'dataset' })}
|
|
</div>
|
|
<CreateMetadataModal
|
|
open={open}
|
|
setOpen={setOpen}
|
|
trigger={
|
|
<Button variant="primary" className="mt-3">
|
|
<RiAddLine className="mr-1" />
|
|
{t(($) => $[`${i18nPrefix}.addMetaData`], { ns: 'dataset' })}
|
|
</Button>
|
|
}
|
|
hasBack
|
|
onSave={handleAdd}
|
|
/>
|
|
|
|
<div className="mt-3 space-y-1">
|
|
{userMetadata.map((payload) => (
|
|
<Item
|
|
key={payload.id}
|
|
payload={payload}
|
|
onRename={handleRename(payload)}
|
|
onDelete={handleDelete(payload)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-3 flex h-6 items-center">
|
|
<Switch checked={isBuiltInEnabled} onCheckedChange={onIsBuiltInEnabledChange} />
|
|
<div className="mr-0.5 ml-2 system-sm-semibold text-text-secondary">
|
|
{t(($) => $[`${i18nPrefix}.builtIn`], { ns: 'dataset' })}
|
|
</div>
|
|
<Infotip
|
|
aria-label={t(($) => $[`${i18nPrefix}.builtInDescription`], { ns: 'dataset' })}
|
|
popupClassName="max-w-[100px]"
|
|
>
|
|
{t(($) => $[`${i18nPrefix}.builtInDescription`], { ns: 'dataset' })}
|
|
</Infotip>
|
|
</div>
|
|
|
|
<div className="mt-1 space-y-1">
|
|
{builtInMetadata.map((payload) => (
|
|
<Item
|
|
key={payload.name}
|
|
readonly
|
|
disabled={!isBuiltInEnabled}
|
|
payload={payload as MetadataItemWithValueLength}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{isShowRenameModal && (
|
|
<Dialog
|
|
open
|
|
onOpenChange={(open) => {
|
|
if (!open) setIsShowRenameModal(false)
|
|
}}
|
|
>
|
|
<DialogContent className="overflow-hidden! border-none text-left align-middle">
|
|
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
|
{t(($) => $[`${i18nPrefix}.rename`], { ns: 'dataset' })}
|
|
</DialogTitle>
|
|
|
|
<Field
|
|
label={t(($) => $[`${i18nPrefix}.name`], { ns: 'dataset' })}
|
|
className="mt-4"
|
|
>
|
|
<Input
|
|
aria-label={t(($) => $[`${i18nPrefix}.name`], { ns: 'dataset' })}
|
|
value={templeName}
|
|
onChange={(e) => setTempleName(e.target.value)}
|
|
placeholder={t(($) => $[`${i18nPrefix}.namePlaceholder`], {
|
|
ns: 'dataset',
|
|
})}
|
|
/>
|
|
</Field>
|
|
<div className="mt-4 flex justify-end">
|
|
<Button
|
|
className="mr-2"
|
|
onClick={() => {
|
|
setIsShowRenameModal(false)
|
|
setTempleName(currPayload!.name)
|
|
}}
|
|
>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</Button>
|
|
<Button onClick={handleRenamed} variant="primary" disabled={!templeName}>
|
|
{t(($) => $['operation.save'], { ns: 'common' })}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
</DrawerContent>
|
|
</DrawerPopup>
|
|
</DrawerViewport>
|
|
</DrawerPortal>
|
|
</Drawer>
|
|
)
|
|
}
|
|
export default React.memo(DatasetMetadataDrawer)
|