panel.tsx 5.3 KB

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