-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
364 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
272 changes: 272 additions & 0 deletions
272
web/app/components/workflow/header/view-workflow-history.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
import { | ||
memo, | ||
useCallback, | ||
useMemo, | ||
useState, | ||
} from 'react' | ||
import cn from 'classnames' | ||
import { useTranslation } from 'react-i18next' | ||
import { useShallow } from 'zustand/react/shallow' | ||
import { useStoreApi } from 'reactflow' | ||
import { | ||
useNodesReadOnly, | ||
useWorkflowHistory, | ||
} from '../hooks' | ||
import type { WorkflowHistoryState } from '../workflow-history-store' | ||
import { | ||
PortalToFollowElem, | ||
PortalToFollowElemContent, | ||
PortalToFollowElemTrigger, | ||
} from '@/app/components/base/portal-to-follow-elem' | ||
import TooltipPlus from '@/app/components/base/tooltip-plus' | ||
import { useStore as useAppStore } from '@/app/components/app/store' | ||
import { | ||
ClockFastForward, | ||
ClockPlaySlim, | ||
} from '@/app/components/base/icons/src/vender/line/time' | ||
import { XClose } from '@/app/components/base/icons/src/vender/line/general' | ||
|
||
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 | ||
return `(${index < 0 ? index * -1 : index} ${index > 0 ? 'step forward' : 'step back'})` | ||
} | ||
, []) | ||
|
||
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)}> | ||
<TooltipPlus | ||
triggerMethod={nodesReadOnly ? 'click' : 'hover'} | ||
popupContent={!nodesReadOnly && t('workflow.common.changeHistory')} | ||
> | ||
<div | ||
className={` | ||
flex items-center justify-center w-7 h-7 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) | ||
}} | ||
> | ||
<ClockFastForward className={`w-4 h-4 ${open ? 'text-primary-600' : 'text-gray-500'}`} /> | ||
</div> | ||
</TooltipPlus> | ||
</PortalToFollowElemTrigger> | ||
<PortalToFollowElemContent className='z-[12]'> | ||
<div | ||
className='flex flex-col ml-2 w-[240px] bg-white border-[0.5px] border-gray-200 shadow-xl rounded-xl overflow-y-auto' | ||
style={{ | ||
maxHeight: 'calc(2 / 3 * 100vh)', | ||
}} | ||
> | ||
<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.common.changeHistory')}</div> | ||
<div | ||
className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer' | ||
onClick={() => { | ||
setCurrentLogItem() | ||
setShowMessageLogModal(false) | ||
setOpen(false) | ||
}} | ||
> | ||
<XClose className='w-4 h-4 text-gray-500' /> | ||
</div> | ||
</div> | ||
{ | ||
( | ||
<div className='p-2'> | ||
{ | ||
!calculateChangeList.statesCount && ( | ||
<div className='py-12'> | ||
<ClockPlaySlim className='mx-auto mb-2 w-8 h-8 text-gray-300' /> | ||
<div className='text-center text-[13px] text-gray-400'> | ||
{'You haven\'t changed anything yet'} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
<div className='flex flex-col'> | ||
{ | ||
calculateChangeList.futureStates.map((item: ChangeHistoryEntry, index: number) => ( | ||
<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 || 'Session Start'} {calculateStepLabel(item?.index)} {item?.index === currentHistoryStateIndex && '(current state)'} | ||
</div> | ||
</div> | ||
</div> | ||
)) | ||
} | ||
{ | ||
calculateChangeList.pastStates.map((item: ChangeHistoryEntry, index: number) => ( | ||
<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 || 'Session Start'} {calculateStepLabel(item?.index)} | ||
</div> | ||
</div> | ||
</div> | ||
)) | ||
} | ||
{!!calculateChangeList.statesCount && ( | ||
<> | ||
<div className="h-[1px] bg-gray-100" /> | ||
<div | ||
className={cn( | ||
'flex mb-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]', | ||
)} | ||
> | ||
Clear History | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
|
||
)} | ||
</div> | ||
<div className="px-3 py-2 text-xs text-gray-500" > | ||
|
||
<div className="flex items-center mb-1 h-[22px] font-medium">HINT</div> | ||
<div className="mb-1 text-gray-700 leading-[18px]" >Your editing actions are tracked in a local history, which is stored on your device for the duration of this session. This history will be cleared when you leave the editor.</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
</div> | ||
</PortalToFollowElemContent> | ||
</PortalToFollowElem> | ||
) | ||
) | ||
} | ||
|
||
export default memo(ViewWorkflowHistory) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.