dataset-metadata-drawer.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useRef, useState } from 'react'
  4. import type { BuiltInMetadataItem, MetadataItemWithValueLength } from '../types'
  5. import Drawer from '@/app/components/base/drawer'
  6. import Button from '@/app/components/base/button'
  7. import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
  8. import { getIcon } from '../utils/get-icon'
  9. import cn from '@/utils/classnames'
  10. import Modal from '@/app/components/base/modal'
  11. import Field from './field'
  12. import Input from '@/app/components/base/input'
  13. import { useTranslation } from 'react-i18next'
  14. import Switch from '@/app/components/base/switch'
  15. import Tooltip from '@/app/components/base/tooltip'
  16. import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
  17. import { useBoolean, useHover } from 'ahooks'
  18. import Confirm from '@/app/components/base/confirm'
  19. import Toast from '@/app/components/base/toast'
  20. const i18nPrefix = 'dataset.metadata.datasetMetadata'
  21. type Props = {
  22. userMetadata: MetadataItemWithValueLength[]
  23. builtInMetadata: BuiltInMetadataItem[]
  24. isBuiltInEnabled: boolean
  25. onIsBuiltInEnabledChange: (value: boolean) => void
  26. onClose: () => void
  27. onAdd: (payload: BuiltInMetadataItem) => void
  28. onRename: (payload: MetadataItemWithValueLength) => void
  29. onRemove: (metaDataId: string) => void
  30. }
  31. type ItemProps = {
  32. readonly?: boolean
  33. disabled?: boolean
  34. payload: MetadataItemWithValueLength
  35. onRename?: () => void
  36. onDelete?: () => void
  37. }
  38. const Item: FC<ItemProps> = ({
  39. readonly,
  40. disabled,
  41. payload,
  42. onRename,
  43. onDelete,
  44. }) => {
  45. const { t } = useTranslation()
  46. const Icon = getIcon(payload.type)
  47. const handleRename = useCallback(() => {
  48. onRename?.()
  49. }, [onRename])
  50. const deleteBtnRef = useRef<HTMLDivElement>(null)
  51. const isDeleteHovering = useHover(deleteBtnRef)
  52. const [isShowDeleteConfirm, {
  53. setTrue: showDeleteConfirm,
  54. setFalse: hideDeleteConfirm,
  55. }] = useBoolean(false)
  56. const handleDelete = useCallback(() => {
  57. hideDeleteConfirm()
  58. onDelete?.()
  59. }, [hideDeleteConfirm, onDelete])
  60. return (
  61. <div
  62. key={payload.name}
  63. className={cn(
  64. !readonly && !disabled && 'group/item cursor-pointer hover:shadow-xs',
  65. 'rounded-md border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg',
  66. isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
  67. )}
  68. >
  69. <div
  70. className={cn(
  71. 'flex h-8 items-center justify-between px-2',
  72. disabled && 'opacity-30', // not include border and bg
  73. )}
  74. >
  75. <div className='flex h-full items-center space-x-1 text-text-tertiary'>
  76. <Icon className='size-4 shrink-0' />
  77. <div className='system-sm-medium max-w-[250px] truncate text-text-primary'>{payload.name}</div>
  78. <div className='system-xs-regular shrink-0'>{payload.type}</div>
  79. </div>
  80. {(!readonly || disabled) && (
  81. <div className='system-xs-regular ml-2 shrink-0 text-text-tertiary group-hover/item:hidden'>
  82. {disabled ? t(`${i18nPrefix}.disabled`) : t(`${i18nPrefix}.values`, { num: payload.count || 0 })}
  83. </div>
  84. )}
  85. <div className='ml-2 hidden items-center space-x-1 text-text-tertiary group-hover/item:flex'>
  86. <RiEditLine className='size-4 cursor-pointer' onClick={handleRename} />
  87. <div ref={deleteBtnRef} className='hover:text-text-destructive'>
  88. <RiDeleteBinLine className='size-4 cursor-pointer' onClick={showDeleteConfirm} />
  89. </div>
  90. </div>
  91. {isShowDeleteConfirm && (
  92. <Confirm
  93. isShow
  94. type='warning'
  95. title={t('dataset.metadata.datasetMetadata.deleteTitle')}
  96. content={t('dataset.metadata.datasetMetadata.deleteContent', { name: payload.name })}
  97. onConfirm={handleDelete}
  98. onCancel={hideDeleteConfirm}
  99. />
  100. )}
  101. </div>
  102. </div>
  103. )
  104. }
  105. const DatasetMetadataDrawer: FC<Props> = ({
  106. userMetadata,
  107. builtInMetadata,
  108. isBuiltInEnabled,
  109. onIsBuiltInEnabledChange,
  110. onClose,
  111. onAdd,
  112. onRename,
  113. onRemove,
  114. }) => {
  115. const { t } = useTranslation()
  116. const [isShowRenameModal, setIsShowRenameModal] = useState(false)
  117. const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
  118. const [templeName, setTempleName] = useState('')
  119. const handleRename = useCallback((payload: MetadataItemWithValueLength) => {
  120. return () => {
  121. setCurrPayload(payload)
  122. setTempleName(payload.name)
  123. setIsShowRenameModal(true)
  124. }
  125. }, [setCurrPayload, setIsShowRenameModal])
  126. const [open, setOpen] = useState(false)
  127. const handleAdd = useCallback(async (data: MetadataItemWithValueLength) => {
  128. await onAdd(data)
  129. Toast.notify({
  130. type: 'success',
  131. message: t('common.api.actionSuccess'),
  132. })
  133. setOpen(false)
  134. }, [onAdd, t])
  135. const handleRenamed = useCallback(async () => {
  136. const item = userMetadata.find(p => p.id === currPayload?.id)
  137. if (item) {
  138. await onRename({
  139. ...item,
  140. name: templeName,
  141. })
  142. Toast.notify({
  143. type: 'success',
  144. message: t('common.api.actionSuccess'),
  145. })
  146. }
  147. setIsShowRenameModal(false)
  148. }, [userMetadata, currPayload?.id, onRename, templeName, t])
  149. const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
  150. return async () => {
  151. await onRemove(payload.id)
  152. Toast.notify({
  153. type: 'success',
  154. message: t('common.api.actionSuccess'),
  155. })
  156. }
  157. }, [onRemove, t])
  158. return (
  159. <Drawer
  160. isOpen={true}
  161. onClose={onClose}
  162. showClose
  163. title={t('dataset.metadata.metadata')}
  164. footer={null}
  165. panelClassname='px-4 block !max-w-[420px] my-2 rounded-l-2xl'
  166. >
  167. <div className='h-full overflow-y-auto'>
  168. <div className='system-sm-regular text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
  169. <CreateModal
  170. open={open}
  171. setOpen={setOpen}
  172. trigger={<Button variant='primary' className='mt-3'>
  173. <RiAddLine className='mr-1' />
  174. {t(`${i18nPrefix}.addMetaData`)}
  175. </Button>} hasBack onSave={handleAdd}
  176. />
  177. <div className='mt-3 space-y-1'>
  178. {userMetadata.map(payload => (
  179. <Item
  180. key={payload.id}
  181. payload={payload}
  182. onRename={handleRename(payload)}
  183. onDelete={handleDelete(payload)}
  184. />
  185. ))}
  186. </div>
  187. <div className='mt-3 flex h-6 items-center'>
  188. <Switch
  189. defaultValue={isBuiltInEnabled}
  190. onChange={onIsBuiltInEnabledChange}
  191. />
  192. <div className='system-sm-semibold ml-2 mr-0.5 text-text-secondary'>{t(`${i18nPrefix}.builtIn`)}</div>
  193. <Tooltip popupContent={<div className='max-w-[100px]'>{t(`${i18nPrefix}.builtInDescription`)}</div>} />
  194. </div>
  195. <div className='mt-1 space-y-1'>
  196. {builtInMetadata.map(payload => (
  197. <Item
  198. key={payload.name}
  199. readonly
  200. disabled={!isBuiltInEnabled}
  201. payload={payload as MetadataItemWithValueLength}
  202. />
  203. ))}
  204. </div>
  205. {isShowRenameModal && (
  206. <Modal isShow title={t(`${i18nPrefix}.rename`)} onClose={() => setIsShowRenameModal(false)}>
  207. <Field label={t(`${i18nPrefix}.name`)} className='mt-4'>
  208. <Input
  209. value={templeName}
  210. onChange={e => setTempleName(e.target.value)}
  211. placeholder={t(`${i18nPrefix}.namePlaceholder`)}
  212. />
  213. </Field>
  214. <div className='mt-4 flex justify-end'>
  215. <Button
  216. className='mr-2'
  217. onClick={() => {
  218. setIsShowRenameModal(false)
  219. setTempleName(currPayload!.name)
  220. }}>{t('common.operation.cancel')}</Button>
  221. <Button
  222. onClick={handleRenamed}
  223. variant='primary'
  224. disabled={!templeName}
  225. >{t('common.operation.save')}</Button>
  226. </div>
  227. </Modal>
  228. )}
  229. </div>
  230. </Drawer>
  231. )
  232. }
  233. export default React.memo(DatasetMetadataDrawer)