dify
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { MetadataItemWithEdit } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Label from './label'
|
||||
import InputCombined from './input-combined'
|
||||
import { RiIndeterminateCircleLine } from '@remixicon/react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
payload: MetadataItemWithEdit
|
||||
onChange: (value: MetadataItemWithEdit) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const AddRow: FC<Props> = ({
|
||||
className,
|
||||
payload,
|
||||
onChange,
|
||||
onRemove,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('flex h-6 items-center space-x-0.5', className)}>
|
||||
<Label text={payload.name} />
|
||||
<InputCombined
|
||||
type={payload.type}
|
||||
value={payload.value}
|
||||
onChange={value => onChange({ ...payload, value })}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer rounded-md p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive',
|
||||
)
|
||||
}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<RiIndeterminateCircleLine className='size-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AddRow)
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { type MetadataItemWithEdit, UpdateType } from '../types'
|
||||
import Label from './label'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import InputHasSetMultipleValue from './input-has-set-multiple-value'
|
||||
import InputCombined from './input-combined'
|
||||
import EditedBeacon from './edited-beacon'
|
||||
|
||||
type Props = {
|
||||
payload: MetadataItemWithEdit
|
||||
onChange: (payload: MetadataItemWithEdit) => void
|
||||
onRemove: (id: string) => void
|
||||
onReset: (id: string) => void
|
||||
}
|
||||
|
||||
const EditMetadatabatchItem: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
onRemove,
|
||||
onReset,
|
||||
}) => {
|
||||
const isUpdated = payload.isUpdated
|
||||
const isDeleted = payload.updateType === UpdateType.delete
|
||||
return (
|
||||
<div className='flex h-6 items-center space-x-0.5'>
|
||||
{isUpdated ? <EditedBeacon onReset={() => onReset(payload.id)} /> : <div className='size-4 shrink-0' />}
|
||||
<Label text={payload.name} isDeleted={isDeleted} />
|
||||
{payload.isMultipleValue
|
||||
? <InputHasSetMultipleValue
|
||||
onClear={() => onChange({ ...payload, value: null, isMultipleValue: false })}
|
||||
readOnly={isDeleted}
|
||||
/>
|
||||
: <InputCombined
|
||||
type={payload.type}
|
||||
value={payload.value}
|
||||
onChange={v => onChange({ ...payload, value: v as string })}
|
||||
readOnly={isDeleted}
|
||||
/>}
|
||||
|
||||
<div
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer rounded-md p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive',
|
||||
isDeleted && 'cursor-default bg-state-destructive-hover text-text-destructive')
|
||||
}
|
||||
onClick={() => onRemove(payload.id)}
|
||||
>
|
||||
<RiDeleteBinLine className='size-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditMetadatabatchItem)
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { RiResetLeftLine } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
const EditedBeacon: FC<Props> = ({
|
||||
onReset,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
|
||||
return (
|
||||
<div ref={ref} className='size-4 cursor-pointer'>
|
||||
{isHovering ? (
|
||||
<Tooltip popupContent={t('common.operation.reset')}>
|
||||
<div className='flex size-4 items-center justify-center rounded-full bg-text-accent-secondary' onClick={onReset}>
|
||||
<RiResetLeftLine className='size-[10px] text-text-primary-on-surface' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className='flex size-4 items-center justify-center'>
|
||||
<div className='size-1 rounded-full bg-text-accent-secondary'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditedBeacon)
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { DataType } from '../types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { InputNumber } from '@/app/components/base/input-number'
|
||||
import cn from '@/utils/classnames'
|
||||
import Datepicker from '../base/date-picker'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
type: DataType
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const InputCombined: FC<Props> = ({
|
||||
className: configClassName,
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
readOnly,
|
||||
}) => {
|
||||
const className = cn('h-6 grow p-0.5 text-xs')
|
||||
if (type === DataType.time) {
|
||||
return (
|
||||
<Datepicker
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === DataType.number) {
|
||||
return (
|
||||
<div className='grow text-[0]'>
|
||||
<InputNumber
|
||||
className={cn(className, 'rounded-l-md')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
size='regular'
|
||||
controlWrapClassName='overflow-hidden'
|
||||
controlClassName='pt-0 pb-0'
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
wrapperClassName={configClassName}
|
||||
className={cn(className, 'rounded-md')}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputCombined)
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
onClear: () => void
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const InputHasSetMultipleValue: FC<Props> = ({
|
||||
onClear,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='h-6 grow rounded-md bg-components-input-bg-normal p-0.5 text-[0]'>
|
||||
<div className={cn('inline-flex h-5 items-center space-x-0.5 rounded-[5px] border-[0.5px] border-components-panel-border bg-components-badge-white-to-dark pl-1.5 pr-0.5 shadow-xs', readOnly && 'pr-1.5')}>
|
||||
<div className='system-xs-regular text-text-secondary'>{t('dataset.metadata.batchEditMetadata.multipleValue')}</div>
|
||||
{!readOnly && (
|
||||
<div className='cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'>
|
||||
<RiCloseLine
|
||||
className='size-3.5 '
|
||||
onClick={onClear}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputHasSetMultipleValue)
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
isDeleted?: boolean,
|
||||
className?: string,
|
||||
text: string
|
||||
}
|
||||
|
||||
const Label: FC<Props> = ({
|
||||
isDeleted,
|
||||
className,
|
||||
text,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'system-xs-medium w-[136px] shrink-0 truncate text-text-tertiary',
|
||||
isDeleted && 'text-text-quaternary line-through',
|
||||
className,
|
||||
)}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Label)
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Modal from '../../../base/modal'
|
||||
import type { BuiltInMetadataItem, MetadataItemInBatchEdit } from '../types'
|
||||
import { type MetadataItemWithEdit, UpdateType } from '../types'
|
||||
import EditMetadataBatchItem from './edit-row'
|
||||
import AddedMetadataItem from './add-row'
|
||||
import Button from '../../../base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Checkbox from '../../../base/checkbox'
|
||||
import Tooltip from '../../../base/tooltip'
|
||||
import SelectMetadataModal from '../metadata-dataset/select-metadata-modal'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import AddMetadataButton from '../add-metadata-button'
|
||||
import { produce } from 'immer'
|
||||
import useCheckMetadataName from '../hooks/use-check-metadata-name'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
|
||||
|
||||
const i18nPrefix = 'dataset.metadata.batchEditMetadata'
|
||||
|
||||
type Props = {
|
||||
datasetId: string,
|
||||
documentNum: number
|
||||
list: MetadataItemInBatchEdit[]
|
||||
onSave: (editedList: MetadataItemInBatchEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean) => void
|
||||
onHide: () => void
|
||||
onShowManage: () => void
|
||||
}
|
||||
|
||||
const EditMetadataBatchModal: FC<Props> = ({
|
||||
datasetId,
|
||||
documentNum,
|
||||
list,
|
||||
onSave,
|
||||
onHide,
|
||||
onShowManage,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [templeList, setTempleList] = useState<MetadataItemWithEdit[]>(list)
|
||||
const handleTemplesChange = useCallback((payload: MetadataItemWithEdit) => {
|
||||
const newTempleList = produce(templeList, (draft) => {
|
||||
const index = draft.findIndex(i => i.id === payload.id)
|
||||
if (index !== -1) {
|
||||
draft[index] = payload
|
||||
draft[index].isUpdated = true
|
||||
draft[index].updateType = UpdateType.changeValue
|
||||
}
|
||||
},
|
||||
)
|
||||
setTempleList(newTempleList)
|
||||
}, [templeList])
|
||||
const handleTempleItemRemove = useCallback((id: string) => {
|
||||
const newTempleList = produce(templeList, (draft) => {
|
||||
const index = draft.findIndex(i => i.id === id)
|
||||
if (index !== -1) {
|
||||
draft[index].isUpdated = true
|
||||
draft[index].updateType = UpdateType.delete
|
||||
}
|
||||
})
|
||||
setTempleList(newTempleList)
|
||||
}, [templeList])
|
||||
|
||||
const handleItemReset = useCallback((id: string) => {
|
||||
const newTempleList = produce(templeList, (draft) => {
|
||||
const index = draft.findIndex(i => i.id === id)
|
||||
if (index !== -1) {
|
||||
draft[index] = { ...list[index] }
|
||||
draft[index].isUpdated = false
|
||||
delete draft[index].updateType
|
||||
}
|
||||
})
|
||||
setTempleList(newTempleList)
|
||||
}, [list, templeList])
|
||||
|
||||
const { checkName } = useCheckMetadataName()
|
||||
const { mutate: doAddMetaData } = useCreateMetaData(datasetId)
|
||||
const handleAddMetaData = useCallback(async (payload: BuiltInMetadataItem) => {
|
||||
const errorMsg = checkName(payload.name).errorMsg
|
||||
if (errorMsg) {
|
||||
Toast.notify({
|
||||
message: errorMsg,
|
||||
type: 'error',
|
||||
})
|
||||
return Promise.reject(new Error(errorMsg))
|
||||
}
|
||||
await doAddMetaData(payload)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
}, [checkName, doAddMetaData, t])
|
||||
|
||||
const [addedList, setAddedList] = useState<MetadataItemWithEdit[]>([])
|
||||
const handleAddedListChange = useCallback((payload: MetadataItemWithEdit) => {
|
||||
const newAddedList = addedList.map(i => i.id === payload.id ? payload : i)
|
||||
setAddedList(newAddedList)
|
||||
}, [addedList])
|
||||
const handleAddedItemRemove = useCallback((removeIndex: number) => {
|
||||
return () => {
|
||||
const newAddedList = addedList.filter((i, index) => index !== removeIndex)
|
||||
setAddedList(newAddedList)
|
||||
}
|
||||
}, [addedList])
|
||||
|
||||
const [isApplyToAllSelectDocument, setIsApplyToAllSelectDocument] = useState(false)
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onSave(templeList.filter(item => item.updateType !== UpdateType.delete), addedList, isApplyToAllSelectDocument)
|
||||
}, [templeList, addedList, isApplyToAllSelectDocument, onSave])
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.editMetadata`)}
|
||||
isShow
|
||||
closable
|
||||
onClose={onHide}
|
||||
className='!max-w-[640px]'
|
||||
>
|
||||
<div className='system-xs-medium mt-1 text-text-accent'>{t(`${i18nPrefix}.editDocumentsNum`, { num: documentNum })}</div>
|
||||
<div className='max-h-[305px] overflow-y-auto overflow-x-hidden'>
|
||||
<div className='mt-4 space-y-2'>
|
||||
{templeList.map(item => (
|
||||
<EditMetadataBatchItem
|
||||
key={item.id}
|
||||
payload={item}
|
||||
onChange={handleTemplesChange}
|
||||
onRemove={handleTempleItemRemove}
|
||||
onReset={handleItemReset}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-4 pl-[18px]'>
|
||||
<div className='flex items-center'>
|
||||
<div className='system-xs-medium-uppercase mr-2 shrink-0 text-text-tertiary'>{t('dataset.metadata.createMetadata.title')}</div>
|
||||
<Divider bgStyle='gradient' />
|
||||
</div>
|
||||
<div className='mt-2 space-y-2'>
|
||||
{addedList.map((item, i) => (
|
||||
<AddedMetadataItem
|
||||
key={i}
|
||||
payload={item}
|
||||
onChange={handleAddedListChange}
|
||||
onRemove={handleAddedItemRemove(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-3'>
|
||||
<SelectMetadataModal
|
||||
datasetId={datasetId}
|
||||
popupPlacement='top-start'
|
||||
popupOffset={{ mainAxis: 4, crossAxis: 0 }}
|
||||
trigger={
|
||||
<AddMetadataButton />
|
||||
}
|
||||
onSave={handleAddMetaData}
|
||||
onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])}
|
||||
onManage={onShowManage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div className='flex select-none items-center'>
|
||||
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} />
|
||||
<div className='system-xs-medium ml-2 mr-1 text-text-secondary'>{t(`${i18nPrefix}.applyToAllSelectDocument`)}</div>
|
||||
<Tooltip popupContent={
|
||||
<div className='max-w-[240px]'>{t(`${i18nPrefix}.applyToAllSelectDocumentTip`)}</div>
|
||||
} >
|
||||
<div className='cursor-pointer p-px'>
|
||||
<RiQuestionLine className='size-3.5 text-text-tertiary' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant='primary'
|
||||
>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditMetadataBatchModal)
|
||||
Reference in New Issue
Block a user