fix: 修复管理端前端 lint 与构建问题

- 收敛 easyflow-ui-admin 的 lint、格式和类型问题

- 修正 demo 页面与管理端前端构建失败点

- 验证 pnpm lint 与 pnpm build 均已通过
This commit is contained in:
2026-04-05 21:39:13 +08:00
parent bb72e19c84
commit 7e7c236c2a
240 changed files with 5151 additions and 4701 deletions

View File

@@ -4,7 +4,6 @@ import { useRoute } from 'vue-router';
import { usePreferences } from '@easyflow/preferences';
import { getOptions, sortNodes } from '@easyflow/utils';
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
import { ArrowLeft, CircleCheck, Close } from '@element-plus/icons-vue';
import { Tinyflow } from '@tinyflow-ai/vue';
@@ -14,6 +13,7 @@ import { api } from '#/api/request';
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
import { $t } from '#/locales';
import { router } from '#/router';
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
@@ -143,7 +143,7 @@ const issueFocusActive = ref(false);
const focusedIssueKey = ref('');
let focusPulseTimer: ReturnType<typeof setTimeout> | undefined;
type WorkflowCheckStage = 'SAVE' | 'PRE_EXECUTE';
type WorkflowCheckStage = 'PRE_EXECUTE' | 'SAVE';
const builtInNodeIconMap: Record<string, string> = {
startNode:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
@@ -395,6 +395,15 @@ function checkStageText(stage?: string) {
}
return '-';
}
function checkStageSummaryText(stage?: string) {
return `${$t('aiWorkflow.checkStageLabel')}${checkStageText(stage)}`;
}
function checkIssueCountText(count?: number) {
return `${$t('aiWorkflow.issueCount')}${count || 0}`;
}
function issueKey(issue: any, index: number) {
return `${issue.code || '-'}-${issue.nodeId || '-'}-${issue.edgeId || '-'}-${index}`;
}
@@ -616,16 +625,8 @@ function onAsyncExecute(info: any) {
/>
</div>
<div class="check-summary">
<span
>{{ $t('aiWorkflow.checkStageLabel') }}{{
checkStageText(checkResult?.stage)
}}</span
>
<span
>{{ $t('aiWorkflow.issueCount') }}{{
checkResult?.issueCount || 0
}}</span
>
<span v-text="checkStageSummaryText(checkResult?.stage)"></span>
<span v-text="checkIssueCountText(checkResult?.issueCount)"></span>
</div>
<div class="checklist-body">
<button
@@ -645,14 +646,16 @@ function onAsyncExecute(info: any) {
<span
class="check-level"
:class="issueLevelClass(item.issue.level)"
>{{ item.issue.level || '-' }}</span
>
v-text="item.issue.level || '-'"
></span>
<span class="check-node-display">
<!-- eslint-disable vue/no-v-html -->
<span
v-if="item.nodeDisplay.icon"
class="check-node-icon"
v-html="item.nodeDisplay.icon"
/>
></span>
<!-- eslint-enable vue/no-v-html -->
<span v-else class="check-node-icon-fallback">N</span>
<span class="check-node-title">{{
item.nodeDisplay.title
@@ -676,23 +679,24 @@ function onAsyncExecute(info: any) {
}
:deep(.tf-bottom-dock) {
left: 50% !important;
bottom: 16px !important;
transform: translateX(-50%) !important;
left: 50% !important;
z-index: 46 !important;
transform: translateX(-50%) !important;
}
.head-div {
--workflow-bottom-dock-height: 56px;
--workflow-checklist-offset: calc(var(--workflow-bottom-dock-height) + 24px);
position: relative;
background-color: var(--el-bg-color);
}
.workflow-head-actions {
display: flex;
align-items: center;
gap: 8px;
align-items: center;
}
.tiny-flow-container {
@@ -706,18 +710,18 @@ function onAsyncExecute(info: any) {
.checklist-panel {
position: absolute;
left: 20px;
right: 20px;
bottom: var(--workflow-checklist-offset);
left: 20px;
z-index: 60;
max-height: min(320px, 42vh);
display: flex;
flex-direction: column;
max-height: min(320px, 42vh);
padding: 12px;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color);
border-radius: 12px;
background: var(--el-bg-color-overlay);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.12);
box-shadow: 0 10px 28px rgb(0 0 0 / 12%);
}
.checklist-header {
@@ -742,18 +746,18 @@ function onAsyncExecute(info: any) {
}
.checklist-body {
overflow: auto;
display: flex;
flex-direction: column;
gap: 8px;
overflow: auto;
}
.check-item {
padding: 10px 12px;
text-align: left;
background: var(--el-fill-color-lighter);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
background: var(--el-fill-color-lighter);
padding: 10px 12px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
@@ -765,8 +769,8 @@ function onAsyncExecute(info: any) {
}
.check-item.is-clickable:hover {
border-color: var(--el-color-primary-light-5);
background: var(--el-fill-color-light);
border-color: var(--el-color-primary-light-5);
}
.check-item.is-active {
@@ -781,11 +785,11 @@ function onAsyncExecute(info: any) {
.check-item-meta {
display: flex;
align-items: center;
gap: 10px;
align-items: center;
margin-bottom: 6px;
font-size: 12px;
line-height: 1.2;
margin-bottom: 6px;
}
.check-level {
@@ -795,10 +799,10 @@ function onAsyncExecute(info: any) {
min-width: 56px;
height: 20px;
padding: 0 8px;
border-radius: 999px;
font-weight: 600;
color: var(--el-text-color-primary);
background: var(--el-fill-color-dark);
border-radius: 999px;
}
.check-level.is-error {
@@ -818,8 +822,8 @@ function onAsyncExecute(info: any) {
.check-node-display {
display: inline-flex;
align-items: center;
gap: 8px;
align-items: center;
min-width: 0;
}
@@ -843,24 +847,24 @@ function onAsyncExecute(info: any) {
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 10px;
font-weight: 700;
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
border-radius: 50%;
}
.check-node-title {
font-size: 14px;
color: var(--el-text-color-primary);
font-weight: 500;
color: var(--el-text-color-primary);
}
.check-item-message {
font-size: 13px;
line-height: 1.45;
color: var(--el-color-danger);
word-break: break-word;
overflow-wrap: anywhere;
}
.checklist-slide-enter-active,
@@ -876,14 +880,15 @@ function onAsyncExecute(info: any) {
.workflow-issue-focus :deep(.svelte-flow__node.selected) {
border-color: var(--el-color-danger) !important;
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.26);
box-shadow: 0 0 0 3px rgb(245 108 108 / 26%);
animation: issue-node-pulse 1.2s ease-out 1;
}
@keyframes issue-node-pulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 108, 108, 0.42);
box-shadow: 0 0 0 0 rgb(245 108 108 / 42%);
}
100% {
box-shadow: 0 0 0 10px transparent;
}

View File

@@ -1,23 +1,15 @@
<script setup lang="ts">
import type {FormInstance} from 'element-plus';
import {
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElPopover,
} from 'element-plus';
import type { FormInstance } from 'element-plus';
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
import CardList from '#/components/page/CardList.vue';
import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import {computed, markRaw, onMounted, ref} from 'vue';
import { computed, markRaw, onMounted, ref } from 'vue';
import {useAccess} from '@easyflow/access';
import {EasyFlowFormModal} from '@easyflow/common-ui';
import { useAccess } from '@easyflow/access';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import {
Check,
@@ -33,18 +25,29 @@ import {
Upload,
VideoPlay,
} from '@element-plus/icons-vue';
import {tryit} from 'radash';
import {
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElPopover,
} from 'element-plus';
import { tryit } from 'radash';
import {api} from '#/api/request';
import { api } from '#/api/request';
import workflowIcon from '#/assets/ai/workflow/workflowIcon.png';
// import workflowSvg from '#/assets/workflow.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import DesignIcon from '#/components/icons/DesignIcon.vue';
import CardList from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import {$t} from '#/locales';
import {router} from '#/router';
import {useDictStore} from '#/store';
import { $t } from '#/locales';
import { router } from '#/router';
import { useDictStore } from '#/store';
import WorkflowModal from './WorkflowModal.vue';
@@ -61,7 +64,7 @@ interface FieldDefinition {
placeholder?: string;
}
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
const primaryAction: CardPrimaryAction = {
icon: DesignIcon,
@@ -72,11 +75,11 @@ const primaryAction: CardPrimaryAction = {
},
};
const {hasAccessByCodes} = useAccess();
const { hasAccessByCodes } = useAccess();
const canManageWorkflow = computed(() =>
hasAccessByCodes(['/api/v1/workflow/save']),
);
const updatingScopeId = ref<string | number | null>(null);
const updatingScopeId = ref<null | number | string>(null);
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
const visibilityScopeMeta = computed(() => ({
PRIVATE: {
@@ -203,10 +206,12 @@ function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function resolveVisibilityScopeMeta(scope?: string) {
return visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
visibilityScopeMeta.value.PRIVATE;
return (
visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
visibilityScopeMeta.value.PRIVATE
);
}
function setVisibilityScopePopoverRef(id: string | number, el: any) {
function setVisibilityScopePopoverRef(id: number | string, el: any) {
const cacheKey = String(id);
if (el) {
visibilityScopePopoverRefs.value[cacheKey] = el;
@@ -214,7 +219,7 @@ function setVisibilityScopePopoverRef(id: string | number, el: any) {
}
delete visibilityScopePopoverRefs.value[cacheKey];
}
function closeVisibilityScopePopover(id: string | number) {
function closeVisibilityScopePopover(id: number | string) {
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
}
async function updateVisibilityScope(
@@ -509,11 +514,16 @@ function handleHeaderButtonClick(data: any) {
>
<ElIcon class="workflow-scope-chip__icon">
<component
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
:is="
resolveVisibilityScopeMeta(item.visibilityScope)
.icon
"
/>
</ElIcon>
<span class="workflow-scope-chip__label">
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
{{
resolveVisibilityScopeMeta(item.visibilityScope).label
}}
</span>
</button>
</template>
@@ -564,7 +574,9 @@ function handleHeaderButtonClick(data: any) {
>
<ElIcon class="workflow-scope-chip__icon">
<component
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
:is="
resolveVisibilityScopeMeta(item.visibilityScope).icon
"
/>
</ElIcon>
<span class="workflow-scope-chip__label">
@@ -629,7 +641,7 @@ function handleHeaderButtonClick(data: any) {
font-weight: 600;
line-height: 1;
color: hsl(var(--text-strong));
background: hsl(var(--surface-subtle) / 0.92);
background: hsl(var(--surface-subtle) / 92%);
border: 1px solid hsl(var(--line-subtle));
border-radius: 999px;
transition:
@@ -645,15 +657,15 @@ button.workflow-scope-chip {
}
button.workflow-scope-chip:hover {
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 32%);
transform: translateY(-1px);
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
}
button.workflow-scope-chip:focus-visible {
outline: none;
box-shadow:
0 0 0 4px hsl(var(--primary) / 0.12),
0 10px 22px -18px hsl(var(--foreground) / 0.32);
0 0 0 4px hsl(var(--primary) / 12%),
0 10px 22px -18px hsl(var(--foreground) / 32%);
}
button.workflow-scope-chip:disabled {
@@ -676,20 +688,20 @@ button.workflow-scope-chip:disabled {
.workflow-scope-chip--private {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.09);
border-color: hsl(var(--primary) / 0.2);
background: hsl(var(--primary) / 9%);
border-color: hsl(var(--primary) / 20%);
}
.workflow-scope-chip--dept {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
border-color: hsl(var(--warning) / 0.2);
background: hsl(var(--warning) / 12%);
border-color: hsl(var(--warning) / 20%);
}
.workflow-scope-chip--public {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
border-color: hsl(var(--success) / 0.2);
background: hsl(var(--success) / 12%);
border-color: hsl(var(--success) / 20%);
}
.workflow-scope-panel {
@@ -700,9 +712,9 @@ button.workflow-scope-chip:disabled {
.workflow-scope-option {
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 10px 8px;
text-align: left;
@@ -716,12 +728,12 @@ button.workflow-scope-chip:disabled {
}
.workflow-scope-option:hover {
background: hsl(var(--foreground) / 0.04);
background: hsl(var(--foreground) / 4%);
}
.workflow-scope-option:focus-visible {
outline: none;
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
box-shadow: 0 0 0 4px hsl(var(--primary) / 12%);
}
.workflow-scope-option:disabled {
@@ -742,8 +754,8 @@ button.workflow-scope-chip:disabled {
justify-content: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: transparent;
border-radius: 9px;
}
.workflow-scope-option__icon {
@@ -776,29 +788,29 @@ button.workflow-scope-chip:disabled {
.workflow-scope-option--private .workflow-scope-option__icon-wrap {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.1);
background: hsl(var(--primary) / 10%);
}
.workflow-scope-option--dept .workflow-scope-option__icon-wrap {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
background: hsl(var(--warning) / 12%);
}
.workflow-scope-option--public .workflow-scope-option__icon-wrap {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
background: hsl(var(--success) / 12%);
}
.workflow-scope-option--private.workflow-scope-option--active {
background: hsl(var(--primary) / 0.08);
background: hsl(var(--primary) / 8%);
}
.workflow-scope-option--dept.workflow-scope-option--active {
background: hsl(var(--warning) / 0.08);
background: hsl(var(--warning) / 8%);
}
.workflow-scope-option--public.workflow-scope-option--active {
background: hsl(var(--success) / 0.08);
background: hsl(var(--success) / 8%);
}
.workflow-scope-option--private .workflow-scope-option__check {
@@ -815,8 +827,8 @@ button.workflow-scope-chip:disabled {
:global(.workflow-visibility-popover.el-popover.el-popper) {
padding: 8px;
border-radius: 16px;
border-color: hsl(var(--line-subtle));
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
border-radius: 16px;
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 20%);
}
</style>

View File

@@ -84,7 +84,7 @@ function openDialog(row: any, importMode = false) {
isAdd.value = !row?.id;
entity.value = {
...createDefaultEntity(),
...(row || {}),
...row,
};
dialogVisible.value = true;
}

View File

@@ -65,7 +65,7 @@ const handleDownload = (url: string) => {
v-if="selectionDataType === 'text'"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 100%; flex-shrink: 0"
style="flex-shrink: 0; width: 100%"
@click="changeValue(item)"
>
{{ item }}
@@ -82,7 +82,7 @@ const handleDownload = (url: string) => {
<img
:src="item"
alt=""
style="width: 80px; height: 80px; border-radius: 8px; display: block"
style="display: block; width: 80px; height: 80px; border-radius: 8px"
/>
</div>
@@ -101,7 +101,7 @@ const handleDownload = (url: string) => {
v-else-if="selectionDataType === 'audio'"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 100%; flex-shrink: 0"
style="flex-shrink: 0; width: 100%"
@click="changeValue(item)"
>
<audio
@@ -118,17 +118,17 @@ const handleDownload = (url: string) => {
"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 100%; flex-shrink: 0"
style="flex-shrink: 0; width: 100%"
@click="changeValue(item)"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
"
>
<div style="width: 92%; display: flex; align-items: center">
<div style="display: flex; align-items: center; width: 92%">
<img
style="width: 20px; height: 20px; margin-right: 8px"
alt=""
@@ -151,19 +151,19 @@ const handleDownload = (url: string) => {
<style scoped>
.custom-radio-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
gap: 12px;
}
.custom-radio-option {
background-color: var(--el-bg-color);
padding: 8px;
border-radius: 8px;
cursor: pointer;
position: relative;
box-sizing: border-box; /* 确保 padding 不会撑大宽度 */
padding: 8px;
cursor: pointer;
background-color: var(--el-bg-color);
border-radius: 8px;
box-shadow: 0 0 0 1px var(--el-border-color);
transition: all 0.2s;
box-sizing: border-box; /* 确保 padding 不会撑大宽度 */
}
.custom-radio-option:hover {
@@ -171,41 +171,41 @@ const handleDownload = (url: string) => {
}
.custom-radio-option.selected {
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
padding: 8px;
background: var(--el-color-primary-light-9);
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
}
.custom-radio-option.selected::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 16px;
height: 16px;
content: '';
background-color: var(--el-color-primary);
border-radius: 6px 2px;
box-sizing: border-box;
}
.custom-radio-option.selected::before {
content: '';
position: absolute;
right: 3px;
bottom: 7px;
z-index: 1;
width: 9px;
height: 4px;
border-left: 1px solid white;
content: '';
border-bottom: 1px solid white;
border-left: 1px solid white;
transform: rotate(-45deg);
z-index: 1;
}
.download-icon-btn {
font-size: 18px;
cursor: pointer;
margin-right: 10px;
display: flex; /* 为了对齐图标 */
align-items: center;
margin-right: 10px;
font-size: 18px;
cursor: pointer;
}
</style>

View File

@@ -73,7 +73,7 @@ const handleDownload = (url: string) => {
v-if="selectionDataType === 'text'"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 100%; flex-shrink: 0"
style="flex-shrink: 0; width: 100%"
@click="changeValue(item)"
>
{{ item }}
@@ -90,7 +90,7 @@ const handleDownload = (url: string) => {
<img
:src="item"
alt=""
style="width: 80px; height: 80px; border-radius: 8px; display: block"
style="display: block; width: 80px; height: 80px; border-radius: 8px"
/>
</div>
@@ -109,7 +109,7 @@ const handleDownload = (url: string) => {
v-else-if="selectionDataType === 'audio'"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 300px; flex-shrink: 0"
style="flex-shrink: 0; width: 300px"
@click="changeValue(item)"
>
<audio controls :src="item" style="width: 100%; height: 40px"></audio>
@@ -122,17 +122,17 @@ const handleDownload = (url: string) => {
"
class="custom-radio-option"
:class="{ selected: isSelected(item) }"
style="width: 100%; flex-shrink: 0"
style="flex-shrink: 0; width: 100%"
@click="changeValue(item)"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
"
>
<div style="width: 92%; display: flex; align-items: center">
<div style="display: flex; align-items: center; width: 92%">
<img
style="width: 20px; height: 20px; margin-right: 8px"
alt=""
@@ -156,19 +156,19 @@ const handleDownload = (url: string) => {
/* 这里复用之前的 CSS样式完全一致 */
.custom-radio-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
gap: 12px;
}
.custom-radio-option {
background-color: var(--el-bg-color);
padding: 8px;
border-radius: 8px;
cursor: pointer;
position: relative;
box-sizing: border-box;
padding: 8px;
cursor: pointer;
background-color: var(--el-bg-color);
border-radius: 8px;
box-shadow: 0 0 0 1px var(--el-border-color);
transition: all 0.2s;
box-sizing: border-box;
}
.custom-radio-option:hover {
@@ -176,41 +176,41 @@ const handleDownload = (url: string) => {
}
.custom-radio-option.selected {
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
padding: 8px;
background: var(--el-color-primary-light-9);
box-shadow: 0 0 0 1px var(--el-color-primary-light-3);
}
.custom-radio-option.selected::after {
content: '';
position: absolute;
right: 0;
bottom: 0;
box-sizing: border-box;
width: 16px;
height: 16px;
content: '';
background-color: var(--el-color-primary);
border-radius: 6px 2px;
box-sizing: border-box;
}
.custom-radio-option.selected::before {
content: '';
position: absolute;
right: 3px;
bottom: 7px;
z-index: 1;
width: 9px;
height: 5px;
border-left: 1px solid white;
content: '';
border-bottom: 1px solid white;
border-left: 1px solid white;
transform: rotate(-45deg);
z-index: 1;
}
.download-icon-btn {
font-size: 18px;
cursor: pointer;
margin-right: 10px;
display: flex;
align-items: center;
margin-right: 10px;
font-size: 18px;
cursor: pointer;
}
</style>

View File

@@ -71,9 +71,7 @@ function submit() {
</ElButton>
</ElFormItem>
</ElForm>
<div class="mb-2.5 mt-2.5 font-semibold">
{{ $t('workflow.result') }}
</div>
<div class="mb-2.5 mt-2.5 font-semibold">{{ $t('workflow.result') }}</div>
<ShowJson :value="result" />
</div>
</template>

View File

@@ -3,9 +3,11 @@ import type {
ManagedDatasetOption,
ManagedDatasetSourceOption,
} from './datasetOptions';
import huaweiIcon from '#/assets/datacenter/huawei-icon.svg';
import mysqlIcon from '#/assets/datacenter/mysql-icon.svg';
import postgresqlIcon from '#/assets/datacenter/postgresql-icon.svg';
import {
groupManagedDatasetOptions,
loadManagedDatasetOptions,
@@ -24,15 +26,15 @@ const SOURCE_LOGO_MAP: Record<string, string> = {
};
type NodeLike = {
id: string;
data?: Record<string, any>;
id: string;
};
type UpdateNodeData = (
nodeId: string,
data:
| Record<string, any>
| ((node: Record<string, any>) => Record<string, any>),
| ((node: Record<string, any>) => Record<string, any>)
| Record<string, any>,
) => void;
type FlowInstance = {
@@ -42,10 +44,10 @@ type FlowInstance = {
type RenderContext = FlowInstance | undefined;
type RendererState = {
pickerOpen: boolean;
loadingOptions: boolean;
optionsLoaded: boolean;
options: ManagedDatasetOption[];
optionsLoaded: boolean;
pickerOpen: boolean;
sources: ManagedDatasetSourceOption[];
tableSearchText: string;
updateNodeData?: UpdateNodeData;
@@ -75,11 +77,13 @@ function getUpdateNodeData(parent: HTMLElement, flowInstance?: RenderContext) {
}
function getDatasetKey(datasetRef?: DatasetRefPayload | null) {
return datasetRef?.tableId == null ? '' : String(datasetRef.tableId);
const tableId = datasetRef?.tableId;
return tableId === null || tableId === undefined ? '' : String(tableId);
}
function getSourceKey(datasetRef?: DatasetRefPayload | null) {
return datasetRef?.sourceId == null ? '' : String(datasetRef.sourceId);
const sourceId = datasetRef?.sourceId;
return sourceId === null || sourceId === undefined ? '' : String(sourceId);
}
function createSourceOnlyDatasetRef(
@@ -95,7 +99,7 @@ function createSourceOnlyDatasetRef(
};
}
function escapeHtml(value?: string | number | null) {
function escapeHtml(value?: null | number | string) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@@ -410,7 +414,7 @@ function buildSourceList(
activeKey: string,
emptyText: string,
) {
if (!options.length) {
if (options.length === 0) {
return `<div class="dataset-node-empty">${emptyText}</div>`;
}
return options
@@ -432,7 +436,7 @@ function buildTableList(
activeKey: string,
emptyText: string,
) {
if (!options.length) {
if (options.length === 0) {
return `<div class="dataset-node-empty">${emptyText}</div>`;
}
return options
@@ -465,8 +469,10 @@ function bindInteractiveElements(parent: HTMLElement) {
parent
.querySelectorAll<HTMLElement>('button, input, select, textarea')
.forEach((element) => {
element.onpointerdown = (event) => event.stopPropagation();
element.onmousedown = (event) => event.stopPropagation();
element.addEventListener('pointerdown', (event) =>
event.stopPropagation(),
);
element.addEventListener('mousedown', (event) => event.stopPropagation());
});
}
@@ -539,7 +545,7 @@ export function rerenderSearchNode(
state.pickerOpen = false;
const nextDatasetRef = createSourceOnlyDatasetRef(source);
node.data = {
...(node.data || {}),
...node.data,
datasetRef: nextDatasetRef,
sourceName: source.sourceName,
sourceType: source.sourceType,
@@ -606,7 +612,7 @@ export function rerenderSaveNode(
return;
}
node.data = {
...(node.data || {}),
...node.data,
datasetRef: option.datasetRef,
sourceName: option.sourceName,
sourceType: option.sourceType,

View File

@@ -1,12 +1,12 @@
import { api } from '#/api/request';
export interface DatasetRefPayload {
sourceId: number | string | null;
catalogId?: number | string | null;
sourceId: null | number | string;
catalogId?: null | number | string;
catalogName?: string;
tableId: number | string | null;
tableId: null | number | string;
tableName: string;
versionId?: number | string | null;
versionId?: null | number | string;
}
export interface ManagedDatasetFieldOption {
@@ -57,21 +57,25 @@ function shouldSkipSourceError(error: any) {
function dedupeManagedDatasetOptions(options: ManagedDatasetOption[]) {
const uniqueOptions = new Map<string, ManagedDatasetOption>();
for (const option of options || []) {
const key = option.datasetRef?.tableId != null
? String(option.datasetRef.tableId)
: [
option.datasetRef?.sourceId ?? '',
option.datasetRef?.catalogId ?? '',
option.tableName ?? '',
].join(':');
const tableId = option.datasetRef?.tableId;
const key =
tableId === null || tableId === undefined
? [
option.datasetRef?.sourceId ?? '',
option.datasetRef?.catalogId ?? '',
option.tableName ?? '',
].join(':')
: String(tableId);
if (!uniqueOptions.has(key)) {
uniqueOptions.set(key, option);
}
}
return Array.from(uniqueOptions.values());
return [...uniqueOptions.values()];
}
export async function loadManagedDatasetOptions(): Promise<ManagedDatasetOption[]> {
export async function loadManagedDatasetOptions(): Promise<
ManagedDatasetOption[]
> {
const sourceRes = await api.get('/api/v1/datacenterSource/page', {
params: {
pageNumber: 1,
@@ -89,19 +93,23 @@ export async function loadManagedDatasetOptions(): Promise<ManagedDatasetOption[
});
const catalogs = catalogRes.data || [];
for (const catalog of catalogs) {
const tableRes = await api.get('/api/v1/datacenterDataset/managedTables', {
params: {
sourceId: source.id,
catalogId: catalog.id,
const tableRes = await api.get(
'/api/v1/datacenterDataset/managedTables',
{
params: {
sourceId: source.id,
catalogId: catalog.id,
},
},
});
);
const tables = tableRes.data || [];
for (const table of tables) {
const label = `${source.sourceName} / ${catalog.catalogName} / ${table.tableName}`;
options.push({
label,
value: table.id,
keywords: `${source.sourceName} ${catalog.catalogName} ${table.tableName}`.toLowerCase(),
keywords:
`${source.sourceName} ${catalog.catalogName} ${table.tableName}`.toLowerCase(),
sourceName: source.sourceName,
sourceType: source.sourceType,
catalogName: catalog.catalogName,
@@ -132,7 +140,7 @@ export function groupManagedDatasetOptions(
const grouped = new Map<number | string, ManagedDatasetSourceOption>();
for (const option of dedupeManagedDatasetOptions(options || [])) {
const sourceId = option.datasetRef?.sourceId;
if (sourceId == null) {
if (sourceId === null || sourceId === undefined) {
continue;
}
if (!grouped.has(sourceId)) {
@@ -145,9 +153,12 @@ export function groupManagedDatasetOptions(
tables: [],
});
}
grouped.get(sourceId)!.tables.push(option);
const group = grouped.get(sourceId);
if (group) {
group.tables.push(option);
}
}
return Array.from(grouped.values()).map((item) => ({
return [...grouped.values()].map((item) => ({
...item,
keywords: `${item.sourceName} ${item.tables
.map((table) => `${table.catalogName} ${table.tableName}`)