panel.tsx 5.2 KB

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