|
import type { |
|
FC, |
|
ReactElement, |
|
} from 'react' |
|
import { |
|
cloneElement, |
|
memo, |
|
useCallback, |
|
} from 'react' |
|
import { |
|
RiCloseLine, |
|
RiPlayLargeLine, |
|
} from '@remixicon/react' |
|
import { useShallow } from 'zustand/react/shallow' |
|
import { useTranslation } from 'react-i18next' |
|
import NextStep from './components/next-step' |
|
import PanelOperator from './components/panel-operator' |
|
import HelpLink from './components/help-link' |
|
import { |
|
DescriptionInput, |
|
TitleInput, |
|
} from './components/title-description-input' |
|
import { useResizePanel } from './hooks/use-resize-panel' |
|
import cn from '@/utils/classnames' |
|
import BlockIcon from '@/app/components/workflow/block-icon' |
|
import { |
|
WorkflowHistoryEvent, |
|
useAvailableBlocks, |
|
useNodeDataUpdate, |
|
useNodesInteractions, |
|
useNodesReadOnly, |
|
useNodesSyncDraft, |
|
useToolIcon, |
|
useWorkflow, |
|
useWorkflowHistory, |
|
} from '@/app/components/workflow/hooks' |
|
import { canRunBySingle } from '@/app/components/workflow/utils' |
|
import Tooltip from '@/app/components/base/tooltip' |
|
import type { Node } from '@/app/components/workflow/types' |
|
import { useStore as useAppStore } from '@/app/components/app/store' |
|
import { useStore } from '@/app/components/workflow/store' |
|
|
|
type BasePanelProps = { |
|
children: ReactElement |
|
} & Node |
|
|
|
const BasePanel: FC<BasePanelProps> = ({ |
|
id, |
|
data, |
|
children, |
|
}) => { |
|
const { t } = useTranslation() |
|
const { showMessageLogModal } = useAppStore(useShallow(state => ({ |
|
showMessageLogModal: state.showMessageLogModal, |
|
}))) |
|
const showSingleRunPanel = useStore(s => s.showSingleRunPanel) |
|
const panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420 |
|
const { |
|
setPanelWidth, |
|
} = useWorkflow() |
|
const { handleNodeSelect } = useNodesInteractions() |
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft() |
|
const { nodesReadOnly } = useNodesReadOnly() |
|
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration) |
|
const toolIcon = useToolIcon(data) |
|
|
|
const handleResize = useCallback((width: number) => { |
|
setPanelWidth(width) |
|
}, [setPanelWidth]) |
|
|
|
const { |
|
triggerRef, |
|
containerRef, |
|
} = useResizePanel({ |
|
direction: 'horizontal', |
|
triggerDirection: 'left', |
|
minWidth: 420, |
|
maxWidth: 720, |
|
onResize: handleResize, |
|
}) |
|
|
|
const { saveStateToHistory } = useWorkflowHistory() |
|
|
|
const { |
|
handleNodeDataUpdate, |
|
handleNodeDataUpdateWithSyncDraft, |
|
} = useNodeDataUpdate() |
|
|
|
const handleTitleBlur = useCallback((title: string) => { |
|
handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) |
|
saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) |
|
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) |
|
const handleDescriptionChange = useCallback((desc: string) => { |
|
handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) |
|
saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) |
|
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) |
|
|
|
return ( |
|
<div className={cn( |
|
'relative mr-2 h-full', |
|
showMessageLogModal && '!absolute !mr-0 w-[384px] overflow-hidden -top-[5px] right-[416px] z-0 shadow-lg border-[0.5px] border-components-panel-border rounded-2xl transition-all', |
|
)}> |
|
<div |
|
ref={triggerRef} |
|
className='absolute top-1/2 -translate-y-1/2 -left-2 w-3 h-6 cursor-col-resize resize-x'> |
|
<div className='w-1 h-6 bg-divider-regular rounded-sm'></div> |
|
</div> |
|
<div |
|
ref={containerRef} |
|
className={cn('h-full bg-components-panel-bg shadow-lg border-[0.5px] border-components-panel-border rounded-2xl', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} |
|
style={{ |
|
width: `${panelWidth}px`, |
|
}} |
|
> |
|
<div className='sticky top-0 bg-components-panel-bg border-b-[0.5px] border-black/5 z-10'> |
|
<div className='flex items-center px-4 pt-4 pb-1'> |
|
<BlockIcon |
|
className='shrink-0 mr-1' |
|
type={data.type} |
|
toolIcon={toolIcon} |
|
size='md' |
|
/> |
|
<TitleInput |
|
value={data.title || ''} |
|
onBlur={handleTitleBlur} |
|
/> |
|
<div className='shrink-0 flex items-center text-gray-500'> |
|
{ |
|
canRunBySingle(data.type) && !nodesReadOnly && ( |
|
<Tooltip |
|
popupContent={t('workflow.panel.runThisStep')} |
|
popupClassName='mr-1' |
|
> |
|
<div |
|
className='flex items-center justify-center mr-1 w-6 h-6 rounded-md hover:bg-black/5 cursor-pointer' |
|
onClick={() => { |
|
handleNodeDataUpdate({ id, data: { _isSingleRun: true } }) |
|
handleSyncWorkflowDraft(true) |
|
}} |
|
> |
|
<RiPlayLargeLine className='w-4 h-4 text-text-tertiary' /> |
|
</div> |
|
</Tooltip> |
|
) |
|
} |
|
<HelpLink nodeType={data.type} /> |
|
<PanelOperator id={id} data={data} showHelpLink={false} /> |
|
<div className='mx-3 w-[1px] h-3.5 bg-divider-regular' /> |
|
<div |
|
className='flex items-center justify-center w-6 h-6 cursor-pointer' |
|
onClick={() => handleNodeSelect(id, true)} |
|
> |
|
<RiCloseLine className='w-4 h-4 text-text-tertiary' /> |
|
</div> |
|
</div> |
|
</div> |
|
<div className='p-2'> |
|
<DescriptionInput |
|
value={data.desc || ''} |
|
onChange={handleDescriptionChange} |
|
/> |
|
</div> |
|
</div> |
|
<div className='py-2'> |
|
{cloneElement(children, { id, data })} |
|
</div> |
|
{ |
|
!!availableNextBlocks.length && ( |
|
<div className='p-4 border-t-[0.5px] border-t-black/5'> |
|
<div className='flex items-center mb-1 system-sm-semibold-uppercase text-text-secondary'> |
|
{t('workflow.panel.nextStep').toLocaleUpperCase()} |
|
</div> |
|
<div className='mb-2 system-xs-regular text-text-tertiary'> |
|
{t('workflow.panel.addNextStep')} |
|
</div> |
|
<NextStep selectedNode={{ id, data } as Node} /> |
|
</div> |
|
) |
|
} |
|
</div> |
|
</div> |
|
) |
|
} |
|
|
|
export default memo(BasePanel) |
|
|