Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion ui/src/workflow/common/NodeContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -432,10 +432,34 @@ const closeNodeMenu = () => {
showAnchor.value = false
anchorData.value = undefined
}
/**
* 检索选中时候触发
* @param kw
*/
const selectOn = (kw: string) => {
props.nodeModel.setSelected(true)
console.log('selectOn', kw)
}
/**
* 定位时触发
* @param kw
*/
const focusOn = (kw: string) => {
console.log('focusOn', kw)
}
/**
* 清除时触发
*/
const clearSelectOn = () => {
console.log('onClearSearchSelect')
}
onMounted(() => {
set(props.nodeModel, 'openNodeMenu', (anchorData: any) => {
showAnchor.value ? closeNodeMenu() : openNodeMenu(anchorData)
})
set(props.nodeModel, 'selectOn', selectOn)
set(props.nodeModel, 'focusOn', focusOn)
set(props.nodeModel, 'clearSelectOn', clearSelectOn)
})
</script>
<style lang="scss" scoped>
Expand All @@ -455,5 +479,4 @@ onMounted(() => {
}
}
}

</style>
142 changes: 127 additions & 15 deletions ui/src/workflow/common/NodeSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,27 @@
<div class="workflow-search-container flex-between">
<el-input
ref="searchInputRef"
v-model="searchText"
v-bind:modelValue="searchText"
@update:modelValue="handleSearch"
:placeholder="$t('workflow.tip.searchPlaceholder')"
clearable
@keyup.enter="handleSearch"
@keyup.enter="next"
@keyup.esc="closeSearch"
>
</el-input>
<span>
<el-space :size="4">
<span class="lighter"> 2/3 </span>
<span class="lighter" v-if="selectedCount && selectedCount > 0">
{{ currentIndex + 1 }}/{{ selectedCount }}
</span>
<span class="lighter" v-else-if="searchText.length > 0"> 无结果 </span>
<el-divider direction="vertical" />

<el-button text>
<el-icon><ArrowUp /></el-icon>
<el-icon @click="up"><ArrowUp /></el-icon>
</el-button>
<el-button text>
<el-icon><ArrowDown /></el-icon>
<el-icon @click="next"><ArrowDown /></el-icon>
</el-button>
<el-button text @click="closeSearch()">
<el-icon><Close /></el-icon>
Expand All @@ -43,15 +47,13 @@
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'

import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { MsgSuccess, MsgWarning } from '@/utils/message'
// Props定义
interface Props {
onSearch?: (keyword: string) => void // 搜索回调
lf?: any
}
const props = withDefaults(defineProps<Props>(), {
onSearch: undefined,
})
const props = withDefaults(defineProps<Props>(), {})

// 状态
const showSearch = ref(false)
Expand All @@ -72,6 +74,99 @@ const handleKeyDown = (e: KeyboardEvent) => {
}
}

const focusOn = (node: any) => {
console.log(node)
props.lf?.graphModel.transformModel.focusOn(
node.x,
node.y,
props.lf?.container.clientWidth,
props.lf?.container.clientHeight,
)
}
const selectedNodes = ref<Array<any>>()
const currentIndex = ref<number>(0)
const selectedCount = computed(() => {
return selectedNodes.value?.length
})

const getSelectNodes = (kw: string) => {
const result: Array<any> = []
const graph_data = props.lf?.getGraphData()
graph_data.nodes.filter((node: any) => {
if (node.properties.stepName.includes(kw)) {
result.push({
...node,
order: 1,
focusOn: () => {
focusOn(node)
props.lf?.graphModel.getNodeModelById(node.id).focusOn(searchText.value)
},
selectOn: () => {
props.lf?.graphModel.getNodeModelById(node.id).selectOn(searchText.value)
},
clearSelectOn: () => {
props.lf?.graphModel.getNodeModelById(node.id).clearSelectOn(searchText.value)
},
})
}
if (node.type == 'loop-body-node') {
const nodeModel = props.lf?.graphModel
const childNodeModel = nodeModel.getNodeModelById(node.id)
childNodeModel.getSelectNodes(searchText.value).map((childNode: any) => {
result.push({
...childNode,
order: 2,
focusOn: () => {
focusOn(node)
childNodeModel.focusOn({ node: childNode, kw: searchText.value })
},
selectOn: () => {
childNodeModel.selectOn({ node: childNode, kw: searchText.value })
},
clearSelectOn: () => {
childNodeModel.clearSelectOn({ node: childNode, kw: searchText.value })
},
})
})
}
})
result.sort((a, b) => a.order - b.order || a.y - b.y || a.x - b.x)
return result
}
const selectNodes = (nodes: Array<any>) => {
nodes.forEach((node) => node.selectOn())
}
const next = () => {
if (selectedNodes.value && selectedNodes.value.length > 0) {
if (selectedNodes.value.length - 1 >= currentIndex.value + 1) {
currentIndex.value++
} else {
currentIndex.value = 0
}
selectedNodes.value[currentIndex.value].focusOn()
}
}
const up = () => {
if (selectedNodes.value && selectedNodes.value.length > 0) {
if (currentIndex.value - 1 <= 0) {
currentIndex.value = selectedNodes.value.length - 1
} else {
currentIndex.value--
}
selectedNodes.value[currentIndex.value].focusOn()
}
}

const onSearch = (kw: string) => {
if (selectedNodes.value === undefined) {
const selected = getSelectNodes(kw)
if (selected && selected.length > 0) {
selectedNodes.value = selected
selectNodes(selected)
selected[currentIndex.value].focusOn()
}
}
}
// 打开搜索
const openSearch = () => {
showSearch.value = true
Expand All @@ -84,14 +179,31 @@ const openSearch = () => {

// 关闭搜索
const closeSearch = () => {
clearSelect()
showSearch.value = false
searchText.value = ''
}

const clearSelect = () => {
if (selectedNodes.value) {
selectedNodes.value[currentIndex.value].clearSelectOn()
}
selectedNodes.value = undefined
currentIndex.value = 0
props.lf?.graphModel.clearSelectElements()
const graph_data = props.lf?.getGraphData()
graph_data.nodes.forEach((node: any) => {
if (node.type == 'loop-body-node') {
props.lf?.graphModel.getNodeModelById(node.id).clearSelectElements()
}
})
}
// 执行搜索
const handleSearch = () => {
const handleSearch = (kw: string) => {
searchText.value = kw
clearSelect()

if (searchText.value.trim()) {
props.onSearch?.(searchText.value)
onSearch?.(searchText.value)
}
}

Expand Down Expand Up @@ -123,7 +235,7 @@ onUnmounted(() => {
width: 360px;
:deep(.el-input__wrapper) {
box-shadow: none;
padding: 0 8px 0 1px!important;
padding: 0 8px 0 1px !important;
}
}
</style>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some corrections and improvements to your Vue.js code:

  1. Type Annotations: Ensure all props are properly annotated with their types.
  2. Arrow Function Usage: Use consistent arrow function notation (() => {...}) throughout the template and script setup.
  3. Computed Property: The selectedCount should be defined using computed.
  4. Logical Operators: Simplify logical operators in the template where possible.
  5. Comments: Add comments for better readability.
  6. Style Updates: Update style properties as needed.

Revised Code

<template>
  <div class="workflow-search-container flex-between">
    <el-input
      ref="searchInputRef"
      v-model:value="searchText"
      @input="handleInput"
      :placeholder="$t('workflow.tip.searchPlaceholder')"
      clearable
      @keyup.enter="next"
      @keyup.esc="closeSearch"
    ></el-input>
    <span>
      <el-space :size="4">
        <!-- Conditional display of selected count, empty search results, or "No Results" -->
        <span class="lighter">{{ showSelectedIndicator }}</span>
        <el-divider direction="vertical" />

        <el-button text click="up"><ArrowUp /></el-button>
        <el-button text click="next"><ArrowDown /></el-button>
        <el-button text @click="closeSearch()">
          <el-icon><Close /></el-icon>
        </el-button>
      </el-space>
    </span>
  </div>
</template>

<script lang="ts" setup>
import { ref, watchEffect, onMounted, onUnmounted } from 'vue';
import { ArrowUp, Close } from '@element-plus/icons-vue';

// Define props type
interface Props {
  lf?: any; // Example prop
}

const props = defineProps<Props>();
const searchText = ref('');
let currentIdx = ref(0);
let selectedNodes = ref<Array<any>>();

// Computed property to determine indicator based on search criteria
const showSelectedIndicator = computed(() =>
  currentIdx > 0 ? (selectedNodes.value ? `${currentIdx}/` + selectedNodes.value.length : '') : ''
);

// Focuses on a given node by transforming its position and focusing in graph viewmodel
async function focusOn(node: any): Promise<void> {
  
}

watchEffect(async (oldVal, newVal) => {
  if (
    (!newVal && prevVal) ||
    (newVal.trim() !== oldVal.trim()) &&
    currentIdx == 0 &&
    selectedNodes.value != undefined
  ) {
    await focusOn(newVal);
  }
});

onMounted(async () => {
  // Initialize data here if necessary
});

const handleInput = async (value: string) => {
  if (props.lf) {
    const result = getSelectNodes(value);
    selectedNodes.value = result;

    if (result && result.length > 0) {
      selectNodes(result);
      focusOn(currentIdx);
    }
  }
};

function next(): void {
  if (selectedNodes.value && selectedNodes.value.length > 0) {
    if (currentIdx < selectedNodes.value.length - 1) {
      currentIdx++;
      focusOn(currentIdx);
    } else {
      currentIdx = 0;
    }
  }
}

function up(): void {
  if (selectedNodes.value && selectedNodes.value.length > 0) {
    if (currentIdx > 0) {
      currentIdx--;
      focusOn(currentIdx);
    } else {
      currentIdx = selectedNodes.value.length - 1;
    }
  }
}

const resetState = (): void => {
  currentIdx = 0;
  focusedIdMap = {};
  if (lf.graph.viewModels) {
    lf.graph.viewModels[graphData.key]!.focusedIds.clear();
  }
};
</script>

<style scoped>
.workflow-search-container {
  /* Your styles */
}
/* Rest of your CSS */
</style>

Changes Made:

  1. Corrected type annotations for props.
  2. Changed arrow functions in the template to match Vue's syntax.
  3. Defined showSelectedIndicator as a computed property for cleaner logic.
  4. Added comments for clarity.
  5. Updated style properties where necessary without significant changes.

This revised version should address several issues found in the original code, making it more maintainable and robust. Make sure to update the rest of your components accordingly.

41 changes: 2 additions & 39 deletions ui/src/workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
<NodeSearch class="workflow-search" :on-search="onSearch"></NodeSearch>
<NodeSearch class="workflow-search" :lf="lf"></NodeSearch>
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
Expand All @@ -18,7 +18,7 @@ import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
import { MsgSuccess, MsgWarning } from '@/utils/message'

import NodeSearch from '@/workflow/common/NodeSearch.vue'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
const workflow_mode = inject('workflowMode') || WorkflowMode.Application
Expand Down Expand Up @@ -52,43 +52,6 @@ onUnmounted(() => {
const render = (data: any) => {
lf.value.render(data)
}
const searchQueue: Array<string> = []
const selectNode = (node: any) => {
lf.value.graphModel.selectNodeById(node.id)
lf.value.graphModel.transformModel.focusOn(
node.x,
node.y,
lf.value.container.clientWidth,
lf.value.container.clientHeight,
)
searchQueue.push(node.id)
}
const onSearch = (kw: string) => {
const graph_data = lf.value.getGraphData()
for (let index = 0; index < graph_data.nodes.length; index++) {
const node = graph_data.nodes[index]
let firstNode = null
if (node.properties.stepName.includes(kw)) {
if (!firstNode) {
firstNode = node
}

if (!searchQueue.includes(node.id)) {
selectNode(node)
break
}
}
if (index === graph_data.nodes.length - 1) {
searchQueue.length = 0
if (firstNode) {
selectNode(firstNode)
} else {
lf.value.graphModel.clearSelectElements()
MsgWarning('不存在的节点')
}
}
}
}

const renderGraphData = (data?: any) => {
const container: any = document.querySelector('#container')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

1. Logical Flow Initialization

<Control class="workflow-control" v-if="lf" :lf="lf"></Control>

There's no initialization of lf inside the <script setup> section.

Suggestion:
Add the initialiation line to ensure lf is not undefined before using it.

<script setup lang="ts">
import LogicFlow from '@logicflow/core'

const lf = ref(null)

// Rest of the code...
</script>

2. Node Search Functionality

The existing onSearch function attempts to iterate over all nodes and find one with a matching step name keyword. However, this implementation has several issues:

  • It assumes that only one node matches the search criteria.
  • If multiple nodes match, the first one selected will be lost due to resetting searchQueue.
  • The focus behavior can lead to unexpected results if there are many nodes to select and they all have the same keyword.

Suggestions:

  1. Update the logic to handle cases where multiple nodes might match the search criteria.
  2. Adjust the focus method to prevent unintentional selection or movement.
  3. Implement pagination or lazy loading to avoid performance bottlenecks with large numbers of nodes.

3. Node Importing (glob pattern)

const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })

This pattern imports all .ts files under directories named ./nodes. While convenient, it can increase the bundle size significantly depending on the number of files.

Suggestion:
Use dynamic imports (import(...).then(...)) instead of import.meta.glob() if you don't need to execute the imported modules immediately.

<script setup lang="ts">
import type { NodeComponent } from './types' // Assuming types exist in ./types
import * as nodeComponents from '@/components/nodes' //

const getNodes = async () => {
  const loadedNodeComponents: Record<string, NodeComponent> = {}
  await Promise.all(
    Object.entries(nodeComponents).map(async ([key, module]) => {
      try {
        const component = await module.default?.default ?? module.default
        if (component) {
          loadedNodeComponents[key] = component
        }
      } catch (err) {
        console.error(`Failed to load node ${key}`, err)
      }
    }),
  )

  return loadedNodeComponents
}

getNodes().then(nodes => {})
</script>

4. Template Errors

Replace deprecated tags:

<TeleportContainer :flow-id="flowId" />
<NodeSearch class="workflow-search" :on-search="onSearch"></NodeSearch>

with their updated versions:

<TeleportContainer :flow-id="flowId" />

<NodeSearch class="workflow-search" :lf="lf" @select-node="handleSelectNode"></NodeSearch>

Make sure to define handleSelectNode in your script.

Final Suggested Changes

1. Initialize Logic Flow Instance

<template>
<!-- ... -->

<Control class="workflow-control" v-if="lf" :lf="lf"></Control>

<!-- ... -->
</template>

<script setup lang="ts">
import LogicFlow from '@logicflow/core';
import Control from '@/components/Common/Control.vue';

const lf = ref();
...</script>

2. Update Node Search Method

<template>
<!-- ... -->

<NodeSearch class="workflow-search" :lf="lf" @select-node="handleSelectNode"></NodeSearch>

<!-- ... -->
</template>

<script setup lang="ts">
import logicFlowInstance from '@/path/to/logic-flow-instance'; // Replace with actual path

const handleSelectNode = (selectedNodesIds: string[], selectedNodeData: object[]) => {
  // Handle selected node actions here
};

const renderGraphDta = (data?:any) => {
  // ...
}
</script>

These changes should help make the code more robust, efficient, and maintainable. Always test after making these changes thoroughly to ensure everything functions correctly.

Expand Down
41 changes: 41 additions & 0 deletions ui/src/workflow/nodes/loop-body-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,53 @@ const loopLayout = () => {
LoopBodyContainerRef.value?.zoom()
lf.value?.extension?.dagre.layout()
}
const selectOn = (node: any, kw: string) => {
lf.value?.graphModel.getNodeModelById(node.id).selectOn(kw)
}
const focusOn = (node: any, kw: string) => {
lf.value?.graphModel.transformModel.focusOn(
node.x,
node.y,
lf.value?.container.clientWidth,
lf.value?.container.clientHeight,
)
lf.value?.graphModel.getNodeModelById(node.id).focusOn(kw)
}

const getSelectNodes = (kw: string) => {
const graph_data = lf.value?.getGraphData()
return graph_data.nodes.filter((node: any) => node.properties.stepName.includes(kw))
}
const onSearchSelect = (node: any, kw: string) => {
lf.value?.graphModel.getNodeModelById(node.id).selectOn(kw)
}
const onClearSearchSelect = (node: any, kw: string) => {
lf.value?.graphModel.getNodeModelById(node.id).clearSelectOn(kw)
}
const clearSelectElements = () => {
lf.value.graphModel.clearSelectElements()
}
onMounted(() => {
renderGraphData(cloneDeep(props.nodeModel.properties.workflow))
set(props.nodeModel, 'validate', validate)
set(props.nodeModel, 'set_loop_body', set_loop_body)
set(props.nodeModel, 'loopLayout', loopLayout)
set(props.nodeModel, 'getSelectNodes', getSelectNodes)
set(props.nodeModel, 'focusOn', (event: any) => {
focusOn(event.node, event.kw)
})
set(props.nodeModel, 'selectOn', (event: any) => {
selectOn(event.node, event.kw)
})
set(props.nodeModel, 'clearSelectOn', (event: any) => {
onSearchSelect(event.node, event.kw)
})
set(props.nodeModel, 'clearSelectElements', clearSelectElements)
set(props.nodeModel, 'onClearSearchSelect', (event: any) => {
onClearSearchSelect(event.node, event.kw)
})
})

onUnmounted(() => {
disconnectByFlow(lf.value.graphModel.flowId)
lf.value = null
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your code appears to be quite complex and includes several functions that seem to manipulate graph data using an unknown library (lf). However, I can make some general observations based on what you've provided:

Potential Issues and Improvements

  1. Type Safety:

    • The selectOn and focusOn methods accept generic types but use them inconsistently across different parameters.
  2. Readability:

    • Some function names could be clearer. For example, getSelectNodes is good for retrieving nodes with a specific keyword but lacks clarity about its purpose within this context.
  3. Event Handling:

    • The way events are handled seems a bit redundant and could be simplified by directly passing necessary arguments rather than wrapping everything in another wrapper object like event.
  4. Optimization Considerations:

    • Directly manipulating DOM properties such as clientWidth and clientHeight might not always reflect the actual content size if there are no layout adjustments happening at those points.
    • Ensure that all function calls are correctly bound or passed with the correct dependencies.

Suggestions for Refinement

  1. Function Renaming and Clarification:

    const filterByKeyword = (nodes: any[], kw: string): any[] =>
      // Return nodes whose stepName field contains kw
      nodes.filter(node => node.properties.stepName.includes(kw));
    
    const findNodeById = (id: string): any|null =>
      // Retrieve node by id from graph data
      lf.value.getGraphData().nodes.find(node => node.id === id);
    
    const selectAndFocus = (node: any, keyword: string): void => {
      const model = findNodeById(node.id);
      if (model) {
        model.selectOn(keyword); // Select on given key word
        model.focusOn({ x: model.x, y: model.y }, { width: lf.value.container.clientWidth || 800, height: lf.value.container.clientHeight || 600 }); // Focus on center of node
      }
    };
  2. Remove Event Wrapping:
    Instead of having dedicated wrapper functions (on, selectOn, etc.), bind these operations directly to event targets without unnecessary abstraction.

  3. Consistent Typing:
    Use more precise type annotations where possible. For instance, instead of relying on generics for everything, ensure type safety wherever feasible.


These suggestions aim to make the code more readable, maintainable, and potentially more efficient by simplifying logic and improving readability while maintaining consistency throughout the codebase.

Expand Down