mirror of https://github.com/langgenius/dify.git
feat(workflow): implement ungroup functionality for group nodes
- Added `handleUngroup`, `getCanUngroup`, and `getSelectedGroupId` methods to manage ungrouping of selected group nodes. - Integrated ungrouping logic into the `useShortcuts` hook for keyboard shortcut support (Ctrl + Shift + G). - Updated UI to include ungroup option in the panel operator popup for group nodes. - Added translations for the ungroup action in multiple languages.
This commit is contained in:
parent
a6ce6a249b
commit
b7a2957340
|
|
@ -2529,6 +2529,77 @@ export const useNodesInteractions = () => {
|
|||
})
|
||||
}, [handleSyncWorkflowDraft, saveStateToHistory, store, t, workflowStore])
|
||||
|
||||
// check if the current selection can be ungrouped (single selected Group node)
|
||||
const getCanUngroup = useCallback(() => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const selectedNodes = nodes.filter(node => node.selected)
|
||||
|
||||
if (selectedNodes.length !== 1)
|
||||
return false
|
||||
|
||||
return selectedNodes[0].data.type === BlockEnum.Group
|
||||
}, [store])
|
||||
|
||||
// get the selected group node id for ungroup operation
|
||||
const getSelectedGroupId = useCallback(() => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const selectedNodes = nodes.filter(node => node.selected)
|
||||
|
||||
if (selectedNodes.length === 1 && selectedNodes[0].data.type === BlockEnum.Group)
|
||||
return selectedNodes[0].id
|
||||
|
||||
return undefined
|
||||
}, [store])
|
||||
|
||||
const handleUngroup = useCallback((groupId: string) => {
|
||||
const { getNodes, setNodes, edges, setEdges } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const groupNode = nodes.find(n => n.id === groupId)
|
||||
|
||||
if (!groupNode || groupNode.data.type !== BlockEnum.Group)
|
||||
return
|
||||
|
||||
const memberIds = new Set((groupNode.data.members || []).map((m: { id: string }) => m.id))
|
||||
|
||||
// restore hidden member nodes
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (memberIds.has(node.id)) {
|
||||
node.hidden = false
|
||||
delete node.data._hiddenInGroupId
|
||||
}
|
||||
})
|
||||
// remove group node
|
||||
const groupIndex = draft.findIndex(n => n.id === groupId)
|
||||
if (groupIndex !== -1)
|
||||
draft.splice(groupIndex, 1)
|
||||
})
|
||||
|
||||
// restore hidden edges and remove temp edges
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
// restore hidden edges that involve member nodes
|
||||
draft.forEach((edge) => {
|
||||
if (edge.hidden && (memberIds.has(edge.source) || memberIds.has(edge.target)))
|
||||
edge.hidden = false
|
||||
})
|
||||
// remove temp edges connected to group (iterate backwards to safely splice)
|
||||
for (let i = draft.length - 1; i >= 0; i--) {
|
||||
const edge = draft[i]
|
||||
if (edge.data?._isTemp && (edge.source === groupId || edge.target === groupId))
|
||||
draft.splice(i, 1)
|
||||
}
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeDelete, {
|
||||
nodeId: groupId,
|
||||
})
|
||||
}, [handleSyncWorkflowDraft, saveStateToHistory, store])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
|
|
@ -2550,6 +2621,7 @@ export const useNodesInteractions = () => {
|
|||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
handleMakeGroup,
|
||||
handleUngroup,
|
||||
handleNodeResize,
|
||||
handleNodeDisconnect,
|
||||
handleHistoryBack,
|
||||
|
|
@ -2558,5 +2630,7 @@ export const useNodesInteractions = () => {
|
|||
undimAllNodes,
|
||||
hasBundledNodes,
|
||||
getCanMakeGroup,
|
||||
getCanUngroup,
|
||||
getSelectedGroupId,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ export const useShortcuts = (): void => {
|
|||
hasBundledNodes,
|
||||
getCanMakeGroup,
|
||||
handleMakeGroup,
|
||||
getCanUngroup,
|
||||
getSelectedGroupId,
|
||||
handleUngroup,
|
||||
} = useNodesInteractions()
|
||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
|
@ -113,6 +116,16 @@ export const useShortcuts = (): void => {
|
|||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.g`, (e) => {
|
||||
// Only intercept when the selection can be ungrouped
|
||||
if (shouldHandleShortcut(e) && getCanUngroup()) {
|
||||
e.preventDefault()
|
||||
const groupId = getSelectedGroupId()
|
||||
if (groupId)
|
||||
handleUngroup(groupId)
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
|
|
|
|||
|
|
@ -41,13 +41,14 @@ const PanelOperatorPopup = ({
|
|||
handleNodesDuplicate,
|
||||
handleNodeSelect,
|
||||
handleNodesCopy,
|
||||
handleUngroup,
|
||||
} = useNodesInteractions()
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const nodeMetaData = useNodeMetaData({ id, data } as Node)
|
||||
const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly
|
||||
const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly && data.type !== BlockEnum.Group
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
|
|
@ -61,6 +62,25 @@ const PanelOperatorPopup = ({
|
|||
|
||||
return (
|
||||
<div className="w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl">
|
||||
{
|
||||
!nodesReadOnly && data.type === BlockEnum.Group && (
|
||||
<>
|
||||
<div className="p-1">
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleUngroup(id)
|
||||
}}
|
||||
>
|
||||
{t('panel.ungroup', { ns: 'workflow' })}
|
||||
<ShortcutsName keys={['ctrl', 'shift', 'g']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-px bg-divider-regular"></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -967,6 +967,7 @@
|
|||
"panel.scrollToSelectedNode": "Scroll to selected node",
|
||||
"panel.selectNextStep": "Select Next Step",
|
||||
"panel.startNode": "Start Node",
|
||||
"panel.ungroup": "Ungroup",
|
||||
"panel.userInputField": "User Input Field",
|
||||
"publishLimit.startNodeDesc": "You’ve reached the limit of 2 triggers per workflow for this plan. Upgrade to publish this workflow.",
|
||||
"publishLimit.startNodeTitlePrefix": "Upgrade to",
|
||||
|
|
|
|||
|
|
@ -964,6 +964,7 @@
|
|||
"panel.scrollToSelectedNode": "選択したノードまでスクロール",
|
||||
"panel.selectNextStep": "次ノード選択",
|
||||
"panel.startNode": "開始ノード",
|
||||
"panel.ungroup": "グループ解除",
|
||||
"panel.userInputField": "ユーザー入力欄",
|
||||
"publishLimit.startNodeDesc": "このプランでは、各ワークフローのトリガー数は最大 2 個まで設定できます。公開するにはアップグレードが必要です。",
|
||||
"publishLimit.startNodeTitlePrefix": "アップグレードして、",
|
||||
|
|
|
|||
|
|
@ -965,6 +965,7 @@
|
|||
"panel.scrollToSelectedNode": "滚动至选中节点",
|
||||
"panel.selectNextStep": "选择下一个节点",
|
||||
"panel.startNode": "开始节点",
|
||||
"panel.ungroup": "取消编组",
|
||||
"panel.userInputField": "用户输入字段",
|
||||
"publishLimit.startNodeDesc": "您已达到此计划上每个工作流最多 2 个触发器的限制。请升级后再发布此工作流。",
|
||||
"publishLimit.startNodeTitlePrefix": "升级以",
|
||||
|
|
|
|||
|
|
@ -964,6 +964,7 @@
|
|||
"panel.scrollToSelectedNode": "捲動至選取的節點",
|
||||
"panel.selectNextStep": "選擇下一個節點",
|
||||
"panel.startNode": "起始節點",
|
||||
"panel.ungroup": "取消群組",
|
||||
"panel.userInputField": "用戶輸入字段",
|
||||
"publishLimit.startNodeDesc": "目前方案最多允許 2 個開始節點,升級後才能發布此工作流程。",
|
||||
"publishLimit.startNodeTitlePrefix": "升級以",
|
||||
|
|
|
|||
Loading…
Reference in New Issue