Skip to content

Commit

Permalink
feat: add change history
Browse files Browse the repository at this point in the history
  • Loading branch information
perzeuss committed May 12, 2024
1 parent 3077653 commit 60f264d
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 41 deletions.
50 changes: 29 additions & 21 deletions web/app/components/workflow/header/undo-redo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,52 @@ import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'

import Tooltip from '../../base/tooltip'
import { FlipBackward, FlipForward } from '../../base/icons/src/vender/line/arrows'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { ArrowUturnLeft, ArrowUturnRight } from '@/app/components/base/icons/src/vender/solid/arrows'
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'

export type UndoRedoComponentProps = { handleUndo: () => void; handleRedo: () => void }
const UndoRedoComponent: FC<UndoRedoComponentProps> = ({ handleUndo, handleRedo }) => {
export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void }
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
const { t } = useTranslation()

const { nodesReadOnly } = useNodesReadOnly()

return (
<div className='flex items-center px-0.5 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs mx-2'>
<div className='flex items-center px-0.5 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'>

<div
className={`
<Tooltip selector={'workflow.common.undo'} content={t('workflow.common.undo')!} >
<div
data-tooltip-id='workflow.undo'
className={`
flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600
hover:bg-primary-50 cursor-pointer select-none
${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
`}
onClick={() => !nodesReadOnly && handleUndo()}
>
<ArrowUturnLeft className='mr-1 px-1 h-4' />
{t('workflow.common.undo')}
</div>

<div
className={`
onClick={() => !nodesReadOnly && handleUndo()}
>
<FlipBackward className='mr-1 px-1 h-4 w-6' />
</div>
</Tooltip>

<Tooltip selector={'workflow.redo'} content={t('workflow.common.redo')!} >
<div
data-tooltip-id='workflow.redo'
className={`
flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600
hover:bg-primary-50 cursor-pointer select-none
${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
`}
onClick={() => !nodesReadOnly && handleRedo()}
>
<ArrowUturnRight className='mr-1 px-1 h-4' />
{t('workflow.common.redo')}

</div>
onClick={() => !nodesReadOnly && handleRedo()}
>
<FlipForward className='mr-1 px-1 h-4 w-6' />

</div>
</Tooltip>
<div className='mx-0.5 w-[0.5px] h-8 bg-gray-200'></div>
<ViewWorkflowHistory />
</div>
)
}

export default memo(UndoRedoComponent)
export default memo(UndoRedo)
272 changes: 272 additions & 0 deletions web/app/components/workflow/header/view-workflow-history.tsx
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)
1 change: 1 addition & 0 deletions web/app/components/workflow/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './use-workflow-interactions'
export * from './use-selection-interactions'
export * from './use-panel-interactions'
export * from './use-workflow-start-run'
export * from './use-workflow-history'
5 changes: 4 additions & 1 deletion web/app/components/workflow/hooks/use-nodes-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ export const useNodesInteractions = () => {
setHelpLineHorizontal()
setHelpLineVertical()
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeDragStop)
}
saveStateToHistory(WorkflowHistoryEvent.NodeDragStop)
}, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft])

const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => {
Expand Down Expand Up @@ -444,6 +444,7 @@ export const useNodesInteractions = () => {
nextNodeTargetHandle,
},
) => {
console.log('handleNodeAdd')
if (getNodesReadOnly())
return

Expand All @@ -470,6 +471,7 @@ export const useNodesInteractions = () => {
y: 0,
},
})
console.log('new node', newNode.type)
if (prevNodeId && !nextNodeId) {
const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
const prevNode = nodes[prevNodeIndex]
Expand Down Expand Up @@ -651,6 +653,7 @@ export const useNodesInteractions = () => {
})
setEdges(newEdges)
}
console.log('saveStateToHistory')
handleSyncWorkflowDraft()
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, getAfterNodesInSameBranch])
Expand Down

0 comments on commit 60f264d

Please sign in to comment.