| 
									
										
										
										
											2024-06-26 08:37:12 +02:00
										 |  |  | import { | 
					
						
							|  |  |  |   memo, | 
					
						
							|  |  |  |   useCallback, | 
					
						
							|  |  |  |   useMemo, | 
					
						
							|  |  |  |   useState, | 
					
						
							|  |  |  | } from 'react' | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   RiCloseLine, | 
					
						
							|  |  |  |   RiHistoryLine, | 
					
						
							|  |  |  | } from '@remixicon/react' | 
					
						
							|  |  |  | import { useTranslation } from 'react-i18next' | 
					
						
							|  |  |  | import { useShallow } from 'zustand/react/shallow' | 
					
						
							|  |  |  | import { useStoreApi } from 'reactflow' | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   useNodesReadOnly, | 
					
						
							|  |  |  |   useWorkflowHistory, | 
					
						
							|  |  |  | } from '../hooks' | 
					
						
							|  |  |  | import TipPopup from '../operator/tip-popup' | 
					
						
							|  |  |  | import type { WorkflowHistoryState } from '../workflow-history-store' | 
					
						
							| 
									
										
										
										
											2024-07-09 15:05:40 +08:00
										 |  |  | import cn from '@/utils/classnames' | 
					
						
							| 
									
										
										
										
											2024-06-26 08:37:12 +02:00
										 |  |  | import { | 
					
						
							|  |  |  |   PortalToFollowElem, | 
					
						
							|  |  |  |   PortalToFollowElemContent, | 
					
						
							|  |  |  |   PortalToFollowElemTrigger, | 
					
						
							|  |  |  | } from '@/app/components/base/portal-to-follow-elem' | 
					
						
							|  |  |  | import { useStore as useAppStore } from '@/app/components/app/store' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ChangeHistoryEntry = { | 
					
						
							|  |  |  |   label: string | 
					
						
							|  |  |  |   index: number | 
					
						
							|  |  |  |   state: Partial<WorkflowHistoryState> | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type ChangeHistoryList = { | 
					
						
							|  |  |  |   pastStates: ChangeHistoryEntry[] | 
					
						
							|  |  |  |   futureStates: ChangeHistoryEntry[] | 
					
						
							|  |  |  |   statesCount: number | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ViewWorkflowHistory = () => { | 
					
						
							|  |  |  |   const { t } = useTranslation() | 
					
						
							|  |  |  |   const [open, setOpen] = useState(false) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { nodesReadOnly } = useNodesReadOnly() | 
					
						
							|  |  |  |   const { setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ | 
					
						
							|  |  |  |     appDetail: state.appDetail, | 
					
						
							|  |  |  |     setCurrentLogItem: state.setCurrentLogItem, | 
					
						
							|  |  |  |     setShowMessageLogModal: state.setShowMessageLogModal, | 
					
						
							|  |  |  |   }))) | 
					
						
							|  |  |  |   const reactflowStore = useStoreApi() | 
					
						
							|  |  |  |   const { store, getHistoryLabel } = useWorkflowHistory() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { pastStates, futureStates, undo, redo, clear } = store.temporal.getState() | 
					
						
							|  |  |  |   const [currentHistoryStateIndex, setCurrentHistoryStateIndex] = useState<number>(0) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleClearHistory = useCallback(() => { | 
					
						
							|  |  |  |     clear() | 
					
						
							|  |  |  |     setCurrentHistoryStateIndex(0) | 
					
						
							|  |  |  |   }, [clear]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleSetState = useCallback(({ index }: ChangeHistoryEntry) => { | 
					
						
							|  |  |  |     const { setEdges, setNodes } = reactflowStore.getState() | 
					
						
							|  |  |  |     const diff = currentHistoryStateIndex + index | 
					
						
							|  |  |  |     if (diff === 0) | 
					
						
							|  |  |  |       return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (diff < 0) | 
					
						
							|  |  |  |       undo(diff * -1) | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       redo(diff) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const { edges, nodes } = store.getState() | 
					
						
							|  |  |  |     if (edges.length === 0 && nodes.length === 0) | 
					
						
							|  |  |  |       return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setEdges(edges) | 
					
						
							|  |  |  |     setNodes(nodes) | 
					
						
							|  |  |  |   }, [currentHistoryStateIndex, reactflowStore, redo, store, undo]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const calculateStepLabel = useCallback((index: number) => { | 
					
						
							|  |  |  |     if (!index) | 
					
						
							|  |  |  |       return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const count = index < 0 ? index * -1 : index | 
					
						
							|  |  |  |     return `${index > 0 ? t('workflow.changeHistory.stepForward', { count }) : t('workflow.changeHistory.stepBackward', { count })}` | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   , [t]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const calculateChangeList: ChangeHistoryList = useMemo(() => { | 
					
						
							|  |  |  |     const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent), | 
					
						
							|  |  |  |         index: reverse ? list.length - 1 - index - startIndex : index - startIndex, | 
					
						
							|  |  |  |         state, | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }).filter(Boolean) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const historyData = { | 
					
						
							|  |  |  |       pastStates: filterList(pastStates, pastStates.length).reverse(), | 
					
						
							|  |  |  |       futureStates: filterList([...futureStates, (!pastStates.length && !futureStates.length) ? undefined : store.getState()].filter(Boolean), 0, true), | 
					
						
							|  |  |  |       statesCount: 0, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     historyData.statesCount = pastStates.length + futureStates.length | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...historyData, | 
					
						
							|  |  |  |       statesCount: pastStates.length + futureStates.length, | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [futureStates, getHistoryLabel, pastStates, store]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     ( | 
					
						
							|  |  |  |       <PortalToFollowElem | 
					
						
							|  |  |  |         placement='bottom-end' | 
					
						
							|  |  |  |         offset={{ | 
					
						
							|  |  |  |           mainAxis: 4, | 
					
						
							|  |  |  |           crossAxis: 131, | 
					
						
							|  |  |  |         }} | 
					
						
							|  |  |  |         open={open} | 
					
						
							|  |  |  |         onOpenChange={setOpen} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         <PortalToFollowElemTrigger onClick={() => !nodesReadOnly && setOpen(v => !v)}> | 
					
						
							|  |  |  |           <TipPopup | 
					
						
							|  |  |  |             title={t('workflow.changeHistory.title')} | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <div | 
					
						
							|  |  |  |               className={`
 | 
					
						
							|  |  |  |                 flex items-center justify-center w-8 h-8 rounded-md hover:bg-black/5 cursor-pointer | 
					
						
							|  |  |  |                 ${open && 'bg-primary-50'} ${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'} | 
					
						
							|  |  |  |               `}
 | 
					
						
							|  |  |  |               onClick={() => { | 
					
						
							|  |  |  |                 if (nodesReadOnly) | 
					
						
							|  |  |  |                   return | 
					
						
							|  |  |  |                 setCurrentLogItem() | 
					
						
							|  |  |  |                 setShowMessageLogModal(false) | 
					
						
							|  |  |  |               }} | 
					
						
							|  |  |  |             > | 
					
						
							|  |  |  |               <RiHistoryLine className={`w-4 h-4 hover:bg-black/5 hover:text-gray-700 ${open ? 'text-primary-600' : 'text-gray-500'}`} /> | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |           </TipPopup> | 
					
						
							|  |  |  |         </PortalToFollowElemTrigger> | 
					
						
							|  |  |  |         <PortalToFollowElemContent className='z-[12]'> | 
					
						
							|  |  |  |           <div | 
					
						
							|  |  |  |             className='flex flex-col ml-2 min-w-[240px] max-w-[360px] bg-white border-[0.5px] border-gray-200 shadow-xl rounded-xl overflow-y-auto' | 
					
						
							|  |  |  |           > | 
					
						
							|  |  |  |             <div className='sticky top-0 bg-white flex items-center justify-between px-4 pt-3 text-base font-semibold text-gray-900'> | 
					
						
							|  |  |  |               <div className='grow'>{t('workflow.changeHistory.title')}</div> | 
					
						
							|  |  |  |               <div | 
					
						
							|  |  |  |                 className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer' | 
					
						
							|  |  |  |                 onClick={() => { | 
					
						
							|  |  |  |                   setCurrentLogItem() | 
					
						
							|  |  |  |                   setShowMessageLogModal(false) | 
					
						
							|  |  |  |                   setOpen(false) | 
					
						
							|  |  |  |                 }} | 
					
						
							|  |  |  |               > | 
					
						
							|  |  |  |                 <RiCloseLine className='w-4 h-4 text-gray-500' /> | 
					
						
							|  |  |  |               </div> | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               ( | 
					
						
							|  |  |  |                 <div | 
					
						
							|  |  |  |                   className='p-2 overflow-y-auto' | 
					
						
							|  |  |  |                   style={{ | 
					
						
							|  |  |  |                     maxHeight: 'calc(1 / 2 * 100vh)', | 
					
						
							|  |  |  |                   }} | 
					
						
							|  |  |  |                 > | 
					
						
							|  |  |  |                   { | 
					
						
							|  |  |  |                     !calculateChangeList.statesCount && ( | 
					
						
							|  |  |  |                       <div className='py-12'> | 
					
						
							|  |  |  |                         <RiHistoryLine className='mx-auto mb-2 w-8 h-8 text-gray-300' /> | 
					
						
							|  |  |  |                         <div className='text-center text-[13px] text-gray-400'> | 
					
						
							|  |  |  |                           {t('workflow.changeHistory.placeholder')} | 
					
						
							|  |  |  |                         </div> | 
					
						
							|  |  |  |                       </div> | 
					
						
							|  |  |  |                     ) | 
					
						
							|  |  |  |                   } | 
					
						
							|  |  |  |                   <div className='flex flex-col'> | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                       calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => ( | 
					
						
							|  |  |  |                         <div | 
					
						
							|  |  |  |                           key={item?.index} | 
					
						
							|  |  |  |                           className={cn( | 
					
						
							|  |  |  |                             'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', | 
					
						
							|  |  |  |                             item?.index === currentHistoryStateIndex && 'bg-primary-50', | 
					
						
							|  |  |  |                           )} | 
					
						
							|  |  |  |                           onClick={() => { | 
					
						
							|  |  |  |                             handleSetState(item) | 
					
						
							|  |  |  |                             setOpen(false) | 
					
						
							|  |  |  |                           }} | 
					
						
							|  |  |  |                         > | 
					
						
							|  |  |  |                           <div> | 
					
						
							|  |  |  |                             <div | 
					
						
							|  |  |  |                               className={cn( | 
					
						
							|  |  |  |                                 'flex items-center text-[13px] font-medium leading-[18px]', | 
					
						
							|  |  |  |                                 item?.index === currentHistoryStateIndex && 'text-primary-600', | 
					
						
							|  |  |  |                               )} | 
					
						
							|  |  |  |                             > | 
					
						
							|  |  |  |                               {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')}) | 
					
						
							|  |  |  |                             </div> | 
					
						
							|  |  |  |                           </div> | 
					
						
							|  |  |  |                         </div> | 
					
						
							|  |  |  |                       )) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                     { | 
					
						
							|  |  |  |                       calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => ( | 
					
						
							|  |  |  |                         <div | 
					
						
							|  |  |  |                           key={item?.index} | 
					
						
							|  |  |  |                           className={cn( | 
					
						
							|  |  |  |                             'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer', | 
					
						
							|  |  |  |                             item?.index === calculateChangeList.statesCount - 1 && 'bg-primary-50', | 
					
						
							|  |  |  |                           )} | 
					
						
							|  |  |  |                           onClick={() => { | 
					
						
							|  |  |  |                             handleSetState(item) | 
					
						
							|  |  |  |                             setOpen(false) | 
					
						
							|  |  |  |                           }} | 
					
						
							|  |  |  |                         > | 
					
						
							|  |  |  |                           <div> | 
					
						
							|  |  |  |                             <div | 
					
						
							|  |  |  |                               className={cn( | 
					
						
							|  |  |  |                                 'flex items-center text-[13px] font-medium leading-[18px]', | 
					
						
							|  |  |  |                                 item?.index === calculateChangeList.statesCount - 1 && 'text-primary-600', | 
					
						
							|  |  |  |                               )} | 
					
						
							|  |  |  |                             > | 
					
						
							|  |  |  |                               {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}) | 
					
						
							|  |  |  |                             </div> | 
					
						
							|  |  |  |                           </div> | 
					
						
							|  |  |  |                         </div> | 
					
						
							|  |  |  |                       )) | 
					
						
							|  |  |  |                     } | 
					
						
							|  |  |  |                   </div> | 
					
						
							|  |  |  |                 </div> | 
					
						
							|  |  |  |               ) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               !!calculateChangeList.statesCount && ( | 
					
						
							|  |  |  |                 <> | 
					
						
							|  |  |  |                   <div className="h-[1px] bg-gray-100" /> | 
					
						
							|  |  |  |                   <div | 
					
						
							|  |  |  |                     className={cn( | 
					
						
							|  |  |  |                       'flex my-0.5 px-2 py-[7px] rounded-lg cursor-pointer', | 
					
						
							|  |  |  |                       'hover:bg-red-50 hover:text-red-600', | 
					
						
							|  |  |  |                     )} | 
					
						
							|  |  |  |                     onClick={() => { | 
					
						
							|  |  |  |                       handleClearHistory() | 
					
						
							|  |  |  |                       setOpen(false) | 
					
						
							|  |  |  |                     }} | 
					
						
							|  |  |  |                   > | 
					
						
							|  |  |  |                     <div> | 
					
						
							|  |  |  |                       <div | 
					
						
							|  |  |  |                         className={cn( | 
					
						
							|  |  |  |                           'flex items-center text-[13px] font-medium leading-[18px]', | 
					
						
							|  |  |  |                         )} | 
					
						
							|  |  |  |                       > | 
					
						
							|  |  |  |                         {t('workflow.changeHistory.clearHistory')} | 
					
						
							|  |  |  |                       </div> | 
					
						
							|  |  |  |                     </div> | 
					
						
							|  |  |  |                   </div> | 
					
						
							|  |  |  |                 </> | 
					
						
							|  |  |  |               ) | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             <div className="px-3 w-[240px] py-2 text-xs text-gray-500" > | 
					
						
							|  |  |  |               <div className="flex items-center mb-1 h-[22px] font-medium uppercase">{t('workflow.changeHistory.hint')}</div> | 
					
						
							|  |  |  |               <div className="mb-1 text-gray-700 leading-[18px]">{t('workflow.changeHistory.hintText')}</div> | 
					
						
							|  |  |  |             </div> | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |         </PortalToFollowElemContent> | 
					
						
							|  |  |  |       </PortalToFollowElem> | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default memo(ViewWorkflowHistory) |