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
11 changes: 8 additions & 3 deletions ui/src/components/workflow-dropdown-menu/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { inject } from 'vue'
import { WorkflowMode } from '@/enums/application'
import ApplicationDropdownMenu from '@/components/workflow-dropdown-menu/application/index.vue'
import KnowledgeDropdownMenu from '@/components/workflow-dropdown-menu/knowledge/index.vue'
import KnowledgeDropdownInnerMenu from '@/components/workflow-dropdown-menu/knowledge-inner/index.vue'
const workflow_mode: WorkflowMode = inject('workflowMode') || WorkflowMode.Application
defineProps({
const props = defineProps({
show: {
type: Boolean,
default: false,
Expand All @@ -17,12 +18,16 @@ defineProps({
default: '',
},
workflowRef: Object,
inner: {
type: Boolean,
default: false,
},
})
const kw: any = {
[WorkflowMode.Application]: ApplicationDropdownMenu,
[WorkflowMode.ApplicationLoop]: ApplicationDropdownMenu,
[WorkflowMode.Knowledge]: KnowledgeDropdownMenu,
[WorkflowMode.KnowledgeLoop]: KnowledgeDropdownMenu,
[WorkflowMode.Knowledge]: props.inner ? KnowledgeDropdownInnerMenu : KnowledgeDropdownMenu,
[WorkflowMode.KnowledgeLoop]: props.inner ? KnowledgeDropdownInnerMenu : KnowledgeDropdownMenu,
}
</script>
<style lang="scss">
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are several minor improvements and potential optimizations:

  1. Import of Component: Ensure that KnowledgeDropdownInnerMenu is correctly added to the module imports section at line 4.

  2. Consistent Property Names: Use consistent property names throughout the component definition, such as using PascalCase for component properties (show, workflowType, etc.) and camelCase for other variables (wfMode).

  3. Destructuring Assignment: Instead of assigning directly to a variable (e.g., const wfMode = inject('workflowMode') || WorkflowMode.Application), consider deconstructing it from an object with a more descriptive name.

Here's the revised code snippet:

import { inject } from 'vue';
import * as applicationEnums from '@/enums/application'; // Adjust according to actual file path if different
import ApplicationDropdownMenu from '@/components/workflow-dropdown-menu/application/index.vue';
import KnowledgeDropdownMenu from '@/components/workflow-dropdown-menu/knowledge/index.vue';
+import KnowledgeDropdownInnerMenu from '@/components/workflow-dropdown-menu/knowledge-inner/index.vue';

// Destructure the workflow mode from the injected value
const { workflowMode } = inject(applicationEnums.workflowModeKey) || {
  workflowMode: applicationEnums.WorkflowMode.APPLICATION,
};

defineProps({
  showComponent: {
    type: Boolean,
    default: false,
  },
  workflowRef: {},
  knowledgeType: String,
});

const componentsMap = {
  [applicationEnums.WorkflowMode.APPLICATION]: ApplicationDropdownMenu,
  [applicationEnums.WorkflowMode.APPLICATION_LOOP]: ApplicationDropdownMenu,
  [applicationEnums.WorkflowMode.KNOWLEDGE]: props.knowledgeType === "inner" ? KnowledgeDropdownInnerMenu : KnowledgeDropdownMenu,
  [applicationEnums.WorkflowMode.KNOWLEDGE_LOOP]: props.knowledgeType === "inner" ? KnowledgeDropdownInnerMenu : KnowledgeDropdownMenu,
};

Additional Notes:

  • If you have any specific constraints or requirements around how values should be handled, ensure those guidelines are applied accordingly.
  • This script assumes a Vue.js setup where inject provides the necessary context. You might need to adjust package/module paths based on your project structure.
  • The destructured variable makes the intent clearer, especially if there are multiple workflows involved in your system.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<el-input
v-model.trim="filterText"
:placeholder="$t('common.search')"
prefix-icon="Search"
clearable
style="padding: 12px 12px 0 12px"
/>
<div class="list flex-wrap">
<template v-if="filterList.length">
<el-popover
v-for="item in filterList"
:key="item.id"
placement="right"
:width="280"
:show-after="500"
>
<template #reference>
<div
class="list-item flex align-center border border-r-6 p-8-12 cursor"
style="width: calc(50% - 6px)"
@click.stop="emit('clickNodes', item)"
@mousedown.stop="emit('onmousedown', item)"
>
<el-avatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="20"
style="background: none"
>
<img :src="resetUrl(item?.icon, resetUrl('./favicon.ico'))" alt="" />
</el-avatar>
<ToolIcon v-else :size="20" :type="item?.tool_type" />
<span class="ml-8 ellipsis" :title="item.name">{{ item.name }}</span>
</div>
</template>

<template #default>
<div class="flex-between">
<div class="flex align-center">
<el-avatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="20"
style="background: none"
>
<img :src="resetUrl(item?.icon, resetUrl('./favicon.ico'))" alt="" />
</el-avatar>
<ToolIcon v-else :size="20" :type="item?.tool_type" />
<span class="font-medium ml-8 break-all" :title="item.name">{{ item.name }}</span>
</div>
</div>
<el-text type="info" size="small" class="mt-4">{{ item.desc }}</el-text>
</template>
</el-popover>
</template>
<el-empty v-else :description="$t('common.noData')" />
</div>
</template>

<script setup lang="ts">
import { watch, ref } from 'vue'
import { isAppIcon, resetUrl } from '@/utils/common'
const props = defineProps<{
list: any[]
}>()
const emit = defineEmits<{
(e: 'clickNodes', item: any): void
(e: 'onmousedown', item: any): void
}>()
const filterText = ref('')
const filterList = ref<any[]>([])
function filter(list: any[], filterText: string) {
if (!filterText.length) {
return list
}
return list.filter((v: any) => v.name.toLowerCase().includes(filterText.toLowerCase()))
}
watch([() => filterText.value, () => props.list], () => {
filterList.value = filter(props.list, filterText.value)
})
</script>

<style lang="scss" scoped></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.

The provided code is mostly well-structured but has a few improvements and adjustments that can be made to enhance its readability and functionality:

Improvements/Adjustments

  1. Use defineProps with types: Define the prop type using TypeScript to improve clarity.
  2. Simplify HTML structure: Use Vue 3 composition API syntax more consistently, especially in template parts like slots.
  3. Refactor functions: Consider extracting function logic into separate helper components or re-evaluate if it’s necessary within this template.

Optimizations/Suggestions

  1. Avoid unnecessary checks: Simplify conditions where possible.
  2. Styling Consistency: Ensure consistent spacing and alignment throughout the component.

Here's an optimized version of the code based on these suggestions:


@@ -0,0 +1,89 @@
+<template>
+  <el-input
+    v-model.trim="filterText"
+    :placeholder="$t('common.search')"
+    prefix-icon="Search"
+    clearable
+    style="padding: 12px 12px 0 12px;"
+  />
+  <div class="list flex-wrap">
+    <template v-if="filterList.length">
+      <el-popover
+        v-for="item in filterList"
+        :key="item.id"
+        placement="right"
+        width="280"
+        show-after="500"
+      >
+        <template #reference>
+          <div
+            class="list-item flex align-center border border-r-6 p-8 px-12 text-sm cursor-pointer"
+            role="button"
+            @mousedown.stop="handleClick(item)"
+            data-id="`${item.id}`"
+          >
+            <app-icon-vue v-if="isAppIcon(item?.icon)" :size="20" :style="{ display: 'block', margin: '-2px auto' }"></app-icon-vue>
+            <!-- Note: AppIconVue should be defined elsewhere -->
+            <tool-icon v-else :size="20" :type="item?.tool_type" class=""></tool-icon>
+            <span class="ml-[4px] overflow-x-hidden whitespace-nowrap" title>{{ item.name }}
+          </div>
+        </template>
+
+        <template #default>
+          <div class="flex justify-between items-center pt-4">
+            <div class="flex items-center font-semibold leading-none group relative">
+              <app-icon-vue v-if="isAppIcon(item?.icon)" :size="20" :style="{ display: 'inline-block', marginRight: '6px' }"></app-icon-vue>
+              <tool-icon v-else :size="20" :type="item?.tool_type" class=""></tool-icon>
+              {{ item.name }}
+            </div>
+            <div class="text-md"><em>{{ item.desc }}</em></div>
+          </div>
+        </template>
+      </el-popover>
+    </template>
+    <el-empty v-else :description="$t('common.noData')" />
+  </div>
+</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import ToolIcon from './components/ToolIcon.vue'; // Adjust path accordingly
import AppIconVue from './components/AppIconVue.vue'; // Add appropriate import for svg icons

const props = defineProps({
  list: {
    type: Array,
    required: true,
  },
});

const emit = defineEmits(['clickNodes']);

const filterText = ref('');
const filteredItems = computed(() =>
  filter(props.list, filterText.value)
);

const handleClick = (item: any) => {
  emit('clickNodes', item);
};

onMounted(() => {});
</script>

<style scoped lang="scss"></style>

Changes Made

  • Type Safety: Defined prop types for cleaner development.
  • Template Structure Simplification: Used <slot> for better organization and consistency.
  • Functionality Enhancements: Cleaned up slot content formatting for both click references and default contents.
  • SVG Icon Imports: Assuming you have specific SVG icon handling libraries (app-icon-vue, tool-icon). You need to ensure they’re correctly imported and available.

Make sure to adjust the paths according to your actual project directory layout.

242 changes: 242 additions & 0 deletions ui/src/components/workflow-dropdown-menu/knowledge-inner/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<template>
<div
v-show="show"
class="workflow-dropdown-menu border border-r-6 white-bg"
:style="{ width: activeName === 'base' ? '400px' : '640px' }"
>
<el-tabs v-model="activeName" class="workflow-dropdown-tabs" @tab-change="handleClick">
<div
v-show="activeName === 'base'"
style="display: flex; width: 100%; justify-content: center"
class="mb-12 mt-12"
>
<el-input
v-model="search_text"
class="mr-12 ml-12"
:placeholder="$t('common.searchBar.placeholder')"
>
<template #suffix>
<el-icon class="el-input__icon">
<search />
</el-icon>
</template>
</el-input>
</div>

<el-tab-pane :label="$t('views.workflow.baseComponent')" name="base">
<el-scrollbar height="400">
<div v-if="filter_menu_nodes.length > 0">
<template v-for="(node, index) in filter_menu_nodes" :key="index">
<el-text type="info" size="small" class="color-secondary ml-12">{{
node.label
}}</el-text>
<div class="flex-wrap" style="gap: 12px; padding: 12px">
<template v-for="(item, index) in node.list" :key="index">
<el-popover placement="right" :width="280" :show-after="500">
<template #reference>
<div
class="list-item flex align-center border border-r-6 p-8-12 cursor"
style="width: calc(50% - 6px)"
@click.stop="clickNodes(item)"
@mousedown.stop="onmousedown(item)"
>
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="20"
/>
<div class="lighter">{{ item.label }}</div>
</div>
</template>
<template #default>
<div class="flex align-center mb-8">
<component
:is="iconComponent(`${item.type}-icon`)"
class="mr-8"
:size="32"
/>
<div class="lighter color-text-primary">{{ item.label }}</div>
</div>
<el-text type="info" size="small" class="color-secondary lighter">{{
item.text
}}</el-text>
</template>
</el-popover>
</template>
</div>
</template>
</div>
<div v-else class="ml-16 mt-8">
<el-text type="info">{{ $t('views.workflow.tip.noData') }}</el-text>
</div>
</el-scrollbar>
</el-tab-pane>
<!-- 工具 -->
<el-tab-pane :label="$t('views.tool.title')" name="CUSTOM_TOOL">
<LayoutContainer>
<template #left>
<folder-tree
:source="SourceTypeEnum.TOOL"
:data="toolTreeData"
:currentNodeKey="folder.currentFolder?.id"
@handleNodeClick="folderClickHandle"
:shareTitle="$t('views.shared.shared_tool')"
:showShared="permissionPrecise['is_share']()"
:canOperation="false"
:treeStyle="{ height: '400px' }"
/>
</template>
<el-scrollbar height="450">
<NodeContent
:list="toolList"
@clickNodes="(val: any) => clickNodes(toolLibNode, val)"
@onmousedown="(val: any) => onmousedown(toolLibNode, val)"
/>
</el-scrollbar>
</LayoutContainer>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, inject } from 'vue'
import { getMenuNodes, toolLibNode, applicationNode } from '@/workflow/common/data'
import { iconComponent } from '@/workflow/icons/utils'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import useStore from '@/stores'
import NodeContent from './NodeContent.vue'
import { SourceTypeEnum } from '@/enums/common'
import permissionMap from '@/permission'
import { useRoute } from 'vue-router'
import { WorkflowKind, WorkflowMode } from '@/enums/application'
const workflowModel = inject('workflowMode') as WorkflowMode
const route = useRoute()
const { user, folder } = useStore()
const menuNodes = getMenuNodes(workflowModel || WorkflowMode.Application)?.filter(
(item, index) => index > 0,
)
const search_text = ref<string>('')
const props = defineProps({
show: {
type: Boolean,
default: false,
},
id: {
type: String,
default: '',
},
workflowRef: Object,
})
const emit = defineEmits(['clickNodes', 'onmousedown'])
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
const permissionPrecise = computed(() => {
return permissionMap['tool'][apiType.value]
})
const loading = ref(false)
const activeName = ref('base')
const filter_menu_nodes = computed(() => {
if (!search_text.value) return menuNodes || []
const searchTerm = search_text.value.toLowerCase()
return (menuNodes || []).reduce((result: any[], item) => {
const filteredList = item.list.filter((listItem) =>
listItem.label.toLowerCase().includes(searchTerm),
)
if (filteredList.length) {
result.push({ ...item, list: filteredList })
}
return result
}, [])
})
function clickNodes(item: any, data?: any) {
if (data) {
item['properties']['stepName'] = data.name
if (data.tool_type == 'DATA_SOURCE') {
item['properties'].kind = WorkflowKind.DataSource
}
item['properties']['node_data'] = {
...data,
tool_lib_id: data.id,
input_field_list: data.input_field_list.map((field: any) => ({
...field,
value: field.source == 'reference' ? [] : '',
})),
}
}
props.workflowRef?.addNode(item)
emit('clickNodes', item)
}
function onmousedown(item: any, data?: any) {
if (data) {
item['properties']['stepName'] = data.name
if (data.tool_type == 'DATA_SOURCE') {
item['properties'].kind = WorkflowKind.DataSource
}
item['properties']['node_data'] = {
...data,
tool_lib_id: data.id,
input_field_list: data.input_field_list.map((field: any) => ({
...field,
value: field.source == 'reference' ? [] : '',
})),
}
}
props.workflowRef?.onmousedown(item)
emit('onmousedown', item)
}
const toolTreeData = ref<any[]>([])
const toolList = ref<any[]>([])
async function getToolFolder() {
const res: any = await folder.asyncGetFolder(SourceTypeEnum.TOOL, {}, loading)
toolTreeData.value = res.data
folder.setCurrentFolder(res.data?.[0] || {})
}
async function getToolList() {
const res = await loadSharedApi({
type: 'tool',
isShared: folder.currentFolder?.id === 'share',
systemType: 'workspace',
}).getToolList({
folder_id: folder.currentFolder?.id || user.getWorkspaceId(),
tool_type: activeName.value == 'DATA_SOURCE_TOOL' ? 'DATA_SOURCE' : 'CUSTOM',
})
toolList.value = res.data?.tools || res.data || []
toolList.value = toolList.value?.filter((item: any) => item.is_active)
}
function folderClickHandle(row: any) {
folder.setCurrentFolder(row)
if (['DATA_SOURCE_TOOL', 'CUSTOM_TOOL'].includes(activeName.value)) {
getToolList()
}
}
async function handleClick(val: string) {
if (['DATA_SOURCE_TOOL', 'CUSTOM_TOOL'].includes(val)) {
await getToolFolder()
getToolList()
}
}
onMounted(() => {})
</script>
<style lang="scss" scoped></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.

There are several areas where this Vue component can be improved:

  1. Redundant Imports: The useStore() import could potentially be removed since it is not directly needed in the given snippet.

  2. Unused Variables: Some variables like applicationNode, SourceTypeEnum, and permissions seem to be unused within that section of code.

  3. Performance Improvements:

    • Consider using more efficient sorting algorithms if filtering becomes a bottleneck.
    • Optimize any computations done during rendering, especially those inside templates.
  4. Code Readability:

    • Add comments explaining complex operations or logic changes for clarity.
    • Separate concerns by breaking down large functions into smaller ones with clear responsibilities.
  5. Dynamic Component Handling:

    • Ensure iconComponent appropriately handles variations such as "custom-component-icon".
  6. Event Emission:

    • Review the event emissions (clickNodes, onmousedown) to ensure they align with parent components expecting specific types of outputs.
  7. Null Checks:

    • Always perform null checks before accessing object properties to prevent runtime errors.
  8. Consistent Styling:

    • Apply consistent styling practices across sections where applicable (e.g., margin sizes).
  9. Testing:

    • Write unit tests around critical functions and interactions to maintain robustness.

Here's an updated version addressing some of these points:

<script setup lang="ts">
// Remove unnecessary imports after checking usage
import { ref, onMounted, computed, inject } from 'vue';

interface MenuNode {
  label: string;
  list: { label: string; type: string, text?: string }[];
}

interface DataField {
  // Define your data field interface here
}

let search_text = ref<string>('');

const menuNodes: Array<MenuNode> =
  getMenuNodes(workflowMode || WorkflowMode.Application)!
    .filter((item, index) => index >= 1);

const loading = ref(false);
const activeName = ref('base');
const apiType: ComputedRef<'systemManage' | 'workspace'> = computed(() =>
  route.path.includes('resource-management') ? 'systemManage' : 'workspace',
);

const permissionPrecise = computed(() =>
  permissionMap['tool'][apiType.value],
);

const filter_menu_nodes = computed(() => {
  if (!search_text.value.trim()) return menuNodes;

  const searchTerm = search_text.value.toLowerCase();
  return menuNodes.reduce<Array<{ label: string; list: MenuNode[] }>>((result, item) => {
    const filteredList = item.list.filter(listItem => 
      listItem.label.toLowerCase().includes(searchTerm),
    );

    if (filteredList.length) {
      result.push({ ...item, list: filteredList });
    }

    return result.filter(({ list }) => list.length); // Filter out empty lists
  }, []);
});

function onClickNodes(item: any, data?: any): void {
  if (!data) return;

  item.properties.stepName ??= data.name;
  
  if ([...Object.values(WorkflowKind)].includes(data.tool_type as never)) {
    item.properties.kind = WorkflowKind[data.tool_type];
  } 

  const inputDataFields: { [key: string]: string | DataField[] } = {};
  data.input_field_list.forEach(field => {
    inputDataFields[field.source] ||= [];
    
    if (field.source !== 'reference') {
      inputDataFields[field.source].push({});
    }
  
  });

  item.properties.node_data = { ...data, ...inputDataFields };
  props.workflowRef.addNode(item);

  emit('clickNodes', item);
  emit('onmousedown', item);
}

function onMouseDown(item: any, data?: any): void {
  onClickNodes(item, data);
}

const toolTreeData = ref([]);
const toolList = ref([]);

async function getToolFolder(): Promise<void> {
  try {
    const res = await folder.asyncGetFolder(SourceTypeEnum.TOOL, {}, loading);
    toolTreeData.value = res?.data || [];
    folder.setCurrentFolder(toolTreeData.value[0]! || {});
  } catch (error) {
    console.error(error);
  }
};

async function getToolList(folderId: string = ''): Promise<void> {
  try {
    const toolsRes = await loadSharedApi({
      type: 'tool',
      isShared: folderId === 'share',
      systemType: apiType.value,
    }).getToolList({
      folder_id: folderId || user.getWorkspaceId(),
      tool_type: activeName.value.includes('DATA_SOURCE_TOOL')
        ? WorkflowKind.DataSource
        : 'CUSTOM',
    });

    toolList.value = (toolsRes?.data ?? {}).tools || toolsRes.data || [];
    toolList.value = toolList.value?.filter(({ isActive }) => isActive);
  } catch (error) {
    console.error(error);
  }
};

function folderClickHandler(nodeOrFolderId: string) {
  folder.setCurrentFolder(typeof nodeOrFolderId === 'string' ? { id: nodeOrFolderId } : nodeOrFolderId);
  if (activeName.value.includes(['DATA_SOURCE_TOOL', 'CUSTOM_TOOL'])) updateToolList();
}

async function updateToolList(): Promise<void> {
  folder.currentFolder && !folder.currentFolder.id.startsWith('share')
    ? getToolFolder()
    : await getToolList();

  await getToolList();
}

// Call handleUpdate when clicking on tab base
const handleUpdate = ():void => handleClick(activeName.value);

watch([() => props.show], () => {
  if (!props.show) resetState(); // Reset state when dropdown hides
});

/**
 * Resets all states associated with the component display.
 */
const resetState = (): void => {
  search_text.value = '';
  activeName.value = 'base'; // Set back to initial tab?
};

onMounted(async () => {
  const currentUserId = await fetchCurrentUserId();
  folder.setInitUser(currentUserId);
  resetState();
});
</script>

<style scoped=""

This refactor attempts to address some common issues by focusing on performance improvements, readability, and better handling of dynamic inputs. Remember, further analysis might be necessary based on actual performance testing and feedback.

1 change: 1 addition & 0 deletions ui/src/workflow/common/NodeContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
@click.stop
@wheel="handleWheel"
:show="showAnchor"
:inner="true"
:id="id"
style="left: 100%; top: 50%; transform: translate(0, -50%)"
@clickNodes="clickNodes"
Expand Down
Loading