checklist.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import {
  2. memo,
  3. useState,
  4. } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import {
  7. useEdges,
  8. useNodes,
  9. } from 'reactflow'
  10. import cn from 'classnames'
  11. import BlockIcon from '../block-icon'
  12. import {
  13. useChecklist,
  14. useNodesInteractions,
  15. } from '../hooks'
  16. import type {
  17. CommonEdgeType,
  18. CommonNodeType,
  19. } from '../types'
  20. import {
  21. PortalToFollowElem,
  22. PortalToFollowElemContent,
  23. PortalToFollowElemTrigger,
  24. } from '@/app/components/base/portal-to-follow-elem'
  25. import {
  26. Checklist,
  27. ChecklistSquare,
  28. XClose,
  29. } from '@/app/components/base/icons/src/vender/line/general'
  30. import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
  31. type WorkflowChecklistProps = {
  32. disabled: boolean
  33. }
  34. const WorkflowChecklist = ({
  35. disabled,
  36. }: WorkflowChecklistProps) => {
  37. const { t } = useTranslation()
  38. const [open, setOpen] = useState(false)
  39. const nodes = useNodes<CommonNodeType>()
  40. const edges = useEdges<CommonEdgeType>()
  41. const needWarningNodes = useChecklist(nodes, edges)
  42. const { handleNodeSelect } = useNodesInteractions()
  43. return (
  44. <PortalToFollowElem
  45. placement='bottom-end'
  46. offset={{
  47. mainAxis: 12,
  48. crossAxis: 4,
  49. }}
  50. open={open}
  51. onOpenChange={setOpen}
  52. >
  53. <PortalToFollowElemTrigger onClick={() => !disabled && setOpen(v => !v)}>
  54. <div
  55. className={cn(
  56. 'relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs',
  57. disabled && 'opacity-50 cursor-not-allowed',
  58. )}
  59. >
  60. <div
  61. className={`
  62. group flex items-center justify-center w-full h-full rounded-md cursor-pointer
  63. hover:bg-primary-50
  64. ${open && 'bg-primary-50'}
  65. `}
  66. >
  67. <Checklist
  68. className={`
  69. w-4 h-4 group-hover:text-primary-600
  70. ${open ? 'text-primary-600' : 'text-gray-500'}`
  71. }
  72. />
  73. </div>
  74. {
  75. !!needWarningNodes.length && (
  76. <div className='absolute -right-1.5 -top-1.5 flex items-center justify-center min-w-[18px] h-[18px] rounded-full border border-gray-100 text-white text-[11px] font-semibold bg-[#F79009]'>
  77. {needWarningNodes.length}
  78. </div>
  79. )
  80. }
  81. </div>
  82. </PortalToFollowElemTrigger>
  83. <PortalToFollowElemContent className='z-[12]'>
  84. <div
  85. className='w-[420px] rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg overflow-y-auto'
  86. style={{
  87. maxHeight: 'calc(2 / 3 * 100vh)',
  88. }}
  89. >
  90. <div className='sticky top-0 bg-white flex items-center pl-4 pr-3 pt-3 h-[44px] text-md font-semibold text-gray-900 z-[1]'>
  91. <div className='grow'>{t('workflow.panel.checklist')}{needWarningNodes.length ? `(${needWarningNodes.length})` : ''}</div>
  92. <div
  93. className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer'
  94. onClick={() => setOpen(false)}
  95. >
  96. <XClose className='w-4 h-4 text-gray-500' />
  97. </div>
  98. </div>
  99. <div className='py-2'>
  100. {
  101. !!needWarningNodes.length && (
  102. <>
  103. <div className='px-4 text-xs text-gray-400'>{t('workflow.panel.checklistTip')}</div>
  104. <div className='px-4 py-2'>
  105. {
  106. needWarningNodes.map(node => (
  107. <div
  108. key={node.id}
  109. className='mb-2 last-of-type:mb-0 border-[0.5px] border-gray-200 bg-white shadow-xs rounded-lg cursor-pointer'
  110. onClick={() => {
  111. handleNodeSelect(node.id)
  112. setOpen(false)
  113. }}
  114. >
  115. <div className='flex items-center p-2 h-9 text-xs font-medium text-gray-700'>
  116. <BlockIcon
  117. type={node.type}
  118. className='mr-1.5'
  119. toolIcon={node.toolIcon}
  120. />
  121. {node.title}
  122. </div>
  123. <div className='border-t-[0.5px] border-t-black/[0.02]'>
  124. {
  125. node.unConnected && (
  126. <div className='px-3 py-2 bg-gray-25 rounded-b-lg'>
  127. <div className='flex text-xs leading-[18px] text-gray-500'>
  128. <AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
  129. {t('workflow.common.needConnecttip')}
  130. </div>
  131. </div>
  132. )
  133. }
  134. {
  135. node.errorMessage && (
  136. <div className='px-3 py-2 bg-gray-25 rounded-b-lg'>
  137. <div className='flex text-xs leading-[18px] text-gray-500'>
  138. <AlertTriangle className='mt-[3px] mr-2 w-3 h-3 text-[#F79009]' />
  139. {node.errorMessage}
  140. </div>
  141. </div>
  142. )
  143. }
  144. </div>
  145. </div>
  146. ))
  147. }
  148. </div>
  149. </>
  150. )
  151. }
  152. {
  153. !needWarningNodes.length && (
  154. <div className='mx-4 mb-3 py-4 rounded-lg bg-gray-50 text-gray-400 text-xs text-center'>
  155. <ChecklistSquare className='mx-auto mb-[5px] w-8 h-8 text-gray-300' />
  156. {t('workflow.panel.checklistResolved')}
  157. </div>
  158. )
  159. }
  160. </div>
  161. </div>
  162. </PortalToFollowElemContent>
  163. </PortalToFollowElem>
  164. )
  165. }
  166. export default memo(WorkflowChecklist)