panel.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import type {
  2. FC,
  3. ReactElement,
  4. } from 'react'
  5. import {
  6. cloneElement,
  7. memo,
  8. useCallback,
  9. } from 'react'
  10. import {
  11. RiCloseLine,
  12. RiPlayLargeLine,
  13. } from '@remixicon/react'
  14. import cn from 'classnames'
  15. import { useShallow } from 'zustand/react/shallow'
  16. import { useTranslation } from 'react-i18next'
  17. import NextStep from './components/next-step'
  18. import PanelOperator from './components/panel-operator'
  19. import HelpLink from './components/help-link'
  20. import {
  21. DescriptionInput,
  22. TitleInput,
  23. } from './components/title-description-input'
  24. import { useResizePanel } from './hooks/use-resize-panel'
  25. import BlockIcon from '@/app/components/workflow/block-icon'
  26. import {
  27. useAvailableBlocks,
  28. useNodeDataUpdate,
  29. useNodesInteractions,
  30. useNodesReadOnly,
  31. useNodesSyncDraft,
  32. useToolIcon,
  33. useWorkflow,
  34. } from '@/app/components/workflow/hooks'
  35. import { canRunBySingle } from '@/app/components/workflow/utils'
  36. import TooltipPlus from '@/app/components/base/tooltip-plus'
  37. import type { Node } from '@/app/components/workflow/types'
  38. import { useStore as useAppStore } from '@/app/components/app/store'
  39. import { useStore } from '@/app/components/workflow/store'
  40. type BasePanelProps = {
  41. children: ReactElement
  42. } & Node
  43. const BasePanel: FC<BasePanelProps> = ({
  44. id,
  45. data,
  46. children,
  47. }) => {
  48. const { t } = useTranslation()
  49. const { showMessageLogModal } = useAppStore(useShallow(state => ({
  50. showMessageLogModal: state.showMessageLogModal,
  51. })))
  52. const showSingleRunPanel = useStore(s => s.showSingleRunPanel)
  53. const panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
  54. const {
  55. setPanelWidth,
  56. } = useWorkflow()
  57. const { handleNodeSelect } = useNodesInteractions()
  58. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  59. const { nodesReadOnly } = useNodesReadOnly()
  60. const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration)
  61. const toolIcon = useToolIcon(data)
  62. const handleResize = useCallback((width: number) => {
  63. setPanelWidth(width)
  64. }, [setPanelWidth])
  65. const {
  66. triggerRef,
  67. containerRef,
  68. } = useResizePanel({
  69. direction: 'horizontal',
  70. triggerDirection: 'left',
  71. minWidth: 420,
  72. maxWidth: 720,
  73. onResize: handleResize,
  74. })
  75. const {
  76. handleNodeDataUpdate,
  77. handleNodeDataUpdateWithSyncDraft,
  78. } = useNodeDataUpdate()
  79. const handleTitleBlur = useCallback((title: string) => {
  80. handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
  81. }, [handleNodeDataUpdateWithSyncDraft, id])
  82. const handleDescriptionChange = useCallback((desc: string) => {
  83. handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
  84. }, [handleNodeDataUpdateWithSyncDraft, id])
  85. return (
  86. <div className={cn(
  87. 'relative mr-2 h-full',
  88. showMessageLogModal && '!absolute !mr-0 w-[384px] overflow-hidden -top-[5px] right-[416px] z-0 shadow-lg border-[0.5px] border-gray-200 rounded-2xl transition-all',
  89. )}>
  90. <div
  91. ref={triggerRef}
  92. className='absolute top-1/2 -translate-y-1/2 -left-2 w-3 h-6 cursor-col-resize resize-x'>
  93. <div className='w-1 h-6 bg-gray-300 rounded-sm'></div>
  94. </div>
  95. <div
  96. ref={containerRef}
  97. className={cn('relative h-full bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
  98. style={{
  99. width: `${panelWidth}px`,
  100. }}
  101. >
  102. <div className='sticky top-0 bg-white border-b-[0.5px] border-black/5 z-10'>
  103. <div className='flex items-center px-4 pt-4 pb-1'>
  104. <BlockIcon
  105. className='shrink-0 mr-1'
  106. type={data.type}
  107. toolIcon={toolIcon}
  108. size='md'
  109. />
  110. <TitleInput
  111. value={data.title || ''}
  112. onBlur={handleTitleBlur}
  113. />
  114. <div className='shrink-0 flex items-center text-gray-500'>
  115. {
  116. canRunBySingle(data.type) && !nodesReadOnly && (
  117. <TooltipPlus
  118. popupContent={t('workflow.panel.runThisStep')}
  119. >
  120. <div
  121. className='flex items-center justify-center mr-1 w-6 h-6 rounded-md hover:bg-black/5 cursor-pointer'
  122. onClick={() => {
  123. handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
  124. handleSyncWorkflowDraft(true)
  125. }}
  126. >
  127. <RiPlayLargeLine className='w-4 h-4 text-gray-500' />
  128. </div>
  129. </TooltipPlus>
  130. )
  131. }
  132. <HelpLink nodeType={data.type} />
  133. <PanelOperator id={id} data={data} showHelpLink={false} />
  134. <div className='mx-3 w-[1px] h-3.5 bg-gray-200' />
  135. <div
  136. className='flex items-center justify-center w-6 h-6 cursor-pointer'
  137. onClick={() => handleNodeSelect(id, true)}
  138. >
  139. <RiCloseLine className='w-4 h-4' />
  140. </div>
  141. </div>
  142. </div>
  143. <div className='p-2'>
  144. <DescriptionInput
  145. value={data.desc || ''}
  146. onChange={handleDescriptionChange}
  147. />
  148. </div>
  149. </div>
  150. <div className='py-2'>
  151. {cloneElement(children, { id, data })}
  152. </div>
  153. {
  154. !!availableNextBlocks.length && (
  155. <div className='p-4 border-t-[0.5px] border-t-black/5'>
  156. <div className='flex items-center mb-1 text-gray-700 text-[13px] font-semibold'>
  157. {t('workflow.panel.nextStep').toLocaleUpperCase()}
  158. </div>
  159. <div className='mb-2 text-xs text-gray-400'>
  160. {t('workflow.panel.addNextStep')}
  161. </div>
  162. <NextStep selectedNode={{ id, data } as Node} />
  163. </div>
  164. )
  165. }
  166. </div>
  167. </div>
  168. )
  169. }
  170. export default memo(BasePanel)